diff --git a/lib/gstack-memory-helpers.ts b/lib/gstack-memory-helpers.ts index 11a3f22398..a6f076b634 100644 --- a/lib/gstack-memory-helpers.ts +++ b/lib/gstack-memory-helpers.ts @@ -106,10 +106,12 @@ export function canonicalizeRemote(url: string | null | undefined): string { // strip user@ prefix on URL-style remotes 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 s = s.replace(/\.git$/i, ""); - // strip trailing slash - s = s.replace(/\/+$/, ""); // collapse multiple slashes (after path normalization) s = s.replace(/\/{2,}/g, "/"); return s.toLowerCase(); diff --git a/test/gstack-memory-helpers.test.ts b/test/gstack-memory-helpers.test.ts index 7b356a99c7..d7ea0bec43 100644 --- a/test/gstack-memory-helpers.test.ts +++ b/test/gstack-memory-helpers.test.ts @@ -67,6 +67,21 @@ describe("canonicalizeRemote", () => { it("collapses redundant slashes", () => { 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") + ); + }); }); // ── secretScanFile ─────────────────────────────────────────────────────────