Support opening workspace branches on GitHub (#583)
* Add GitHub branch open target to workspace button (#96) * refactor: simplify workspace open targets to flat shape Collapse the editor/url discriminated union into a single OpenTarget { id, label, icon, onOpen } so the button renders targets uniformly and the mutation is shape-agnostic. Drop the heavily-mocked button test in favor of the URL builder tests, restore the decodeURIComponent guard for valid-syntax-but-invalid-UTF-8 pathnames, and note the SSH host alias limitation. --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
34626e3b8f
commit
c78c30ef54
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildGitHubBranchTreeUrl, parseGitHubRepoFromRemote } from "./github-repo-url";
|
||||
|
||||
describe("parseGitHubRepoFromRemote", () => {
|
||||
it.each([
|
||||
["https://github.com/acme/repo.git", "acme/repo"],
|
||||
["https://github.com/acme/repo", "acme/repo"],
|
||||
["git@github.com:acme/repo.git", "acme/repo"],
|
||||
["ssh://git@github.com/acme/repo.git", "acme/repo"],
|
||||
["https://github.com/acme/repo/", "acme/repo"],
|
||||
])("extracts the repo from %s", (remoteUrl, expected) => {
|
||||
expect(parseGitHubRepoFromRemote(remoteUrl)).toBe(expected);
|
||||
});
|
||||
|
||||
it("returns null for non-GitHub remotes", () => {
|
||||
expect(parseGitHubRepoFromRemote("git@gitlab.com:acme/repo.git")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for invalid URLs", () => {
|
||||
expect(parseGitHubRepoFromRemote("not a url")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildGitHubBranchTreeUrl", () => {
|
||||
it("builds a branch-specific GitHub tree URL", () => {
|
||||
expect(
|
||||
buildGitHubBranchTreeUrl({
|
||||
remoteUrl: "git@github.com:acme/repo.git",
|
||||
branch: "feature/workspace-button",
|
||||
}),
|
||||
).toBe("https://github.com/acme/repo/tree/feature/workspace-button");
|
||||
});
|
||||
|
||||
it("encodes reserved branch characters while preserving slash-separated branch names", () => {
|
||||
expect(
|
||||
buildGitHubBranchTreeUrl({
|
||||
remoteUrl: "https://github.com/acme/repo.git",
|
||||
branch: "feature/ship #42",
|
||||
}),
|
||||
).toBe("https://github.com/acme/repo/tree/feature/ship%20%2342");
|
||||
});
|
||||
|
||||
it("returns null when the current branch is unavailable", () => {
|
||||
expect(
|
||||
buildGitHubBranchTreeUrl({
|
||||
remoteUrl: "https://github.com/acme/repo.git",
|
||||
branch: "HEAD",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
// TODO: this duplicates parseGitHubRepoFromRemote in packages/server/src/services/github-service.ts.
|
||||
// Consolidate into a shared package once we have a third caller.
|
||||
|
||||
// Note: SSH host aliases (e.g. `git@github-work:acme/repo.git` resolved via ~/.ssh/config)
|
||||
// are not detected here, so the GitHub action will silently not appear for those remotes.
|
||||
export function parseGitHubRepoFromRemote(remoteUrl: string | null | undefined): string | null {
|
||||
const trimmed = remoteUrl?.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let cleaned = trimmed;
|
||||
if (cleaned.startsWith("git@github.com:")) {
|
||||
cleaned = cleaned.slice("git@github.com:".length);
|
||||
} else {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(cleaned);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (parsed.hostname !== "github.com") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
cleaned = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
cleaned = cleaned.replace(/\/+$/, "");
|
||||
if (cleaned.endsWith(".git")) {
|
||||
cleaned = cleaned.slice(0, -".git".length);
|
||||
}
|
||||
if (!cleaned.includes("/")) {
|
||||
return null;
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
export function buildGitHubBranchTreeUrl(input: {
|
||||
remoteUrl: string | null | undefined;
|
||||
branch: string | null | undefined;
|
||||
}): string | null {
|
||||
const repo = parseGitHubRepoFromRemote(input.remoteUrl);
|
||||
const branch = input.branch?.trim();
|
||||
if (!repo || !branch || branch === "HEAD") {
|
||||
return null;
|
||||
}
|
||||
const encodedBranch = branch.split("/").map(encodeURIComponent).join("/");
|
||||
return `https://github.com/${repo}/tree/${encodedBranch}`;
|
||||
}
|
||||
Reference in New Issue
Block a user