fix(gbrain): canonicalize remotes with trailing slashes

This commit is contained in:
t
2026-07-14 12:56:09 -07:00
parent 7e23aecfc5
commit 1aa5283ddd
2 changed files with 31 additions and 1 deletions
+7 -1
View File
@@ -106,9 +106,15 @@ export function canonicalizeRemote(url: string | null | undefined): string {
// strip user@ prefix on URL-style remotes // strip user@ prefix on URL-style remotes
s = s.replace(/^[^@\/]+@/, ""); s = s.replace(/^[^@\/]+@/, "");
} }
// strip trailing slash(es) first, so a URL written with a trailing slash
// still matches the `.git$` suffix below (e.g. ".../repo.git/" must
// canonicalize to ".../repo", not ".../repo.git").
s = s.replace(/\/+$/, "");
// strip trailing .git // strip trailing .git
s = s.replace(/\.git$/i, ""); s = s.replace(/\.git$/i, "");
// strip trailing slash // re-strip trailing slash(es): a path remote ending in a `.git` directory
// component ("/repo/.git") exposes a new trailing slash once `.git` is
// stripped, which would split the repo into a second identity.
s = s.replace(/\/+$/, ""); s = s.replace(/\/+$/, "");
// collapse multiple slashes (after path normalization) // collapse multiple slashes (after path normalization)
s = s.replace(/\/{2,}/g, "/"); s = s.replace(/\/{2,}/g, "/");
+24
View File
@@ -67,6 +67,30 @@ describe("canonicalizeRemote", () => {
it("collapses redundant slashes", () => { it("collapses redundant slashes", () => {
expect(canonicalizeRemote("https://github.com//foo//bar")).toBe("github.com/foo/bar"); expect(canonicalizeRemote("https://github.com//foo//bar")).toBe("github.com/foo/bar");
}); });
it("strips .git even when the URL has a trailing slash", () => {
// A remote configured with both a .git suffix and a trailing slash must
// canonicalize to the same key as one without — otherwise the same repo
// gets two dedup/source-id keys across machines.
expect(canonicalizeRemote("https://github.com/garrytan/gstack.git/")).toBe("github.com/garrytan/gstack");
expect(canonicalizeRemote("git@github.com:garrytan/gstack.git/")).toBe("github.com/garrytan/gstack");
expect(canonicalizeRemote("https://github.com/foo/bar.git///")).toBe("github.com/foo/bar");
});
it("produces the same key with or without a trailing slash", () => {
expect(canonicalizeRemote("https://github.com/garrytan/gstack.git/")).toBe(
canonicalizeRemote("https://github.com/garrytan/gstack.git")
);
});
it("canonicalizes a path remote ending in a .git directory component", () => {
// Stripping the `.git` suffix exposes a new trailing slash
// ("/repo/.git" → "/repo/") which must also be stripped, or the same
// repo splits into two identities.
expect(canonicalizeRemote("file:///Users/x/repo/.git")).toBe(
canonicalizeRemote("file:///Users/x/repo")
);
});
}); });
// ── secretScanFile ───────────────────────────────────────────────────────── // ── secretScanFile ─────────────────────────────────────────────────────────