diff --git a/packages/shared/src/cloneUrl.test.ts b/packages/shared/src/cloneUrl.test.ts index c053c95df4120..03a041a70cbbf 100644 --- a/packages/shared/src/cloneUrl.test.ts +++ b/packages/shared/src/cloneUrl.test.ts @@ -175,7 +175,7 @@ describe("cloneUrlForProtocol", () => { }); describe("cloneUrlRepositoryPath", () => { - it("rejects Windows device names and trailing dots or spaces in every folder", () => { + it("derives portable clone folders from URI and SCP paths", () => { for (const segment of [ "CON", "nul.txt", @@ -197,5 +197,19 @@ describe("cloneUrlRepositoryPath", () => { "gitlab.com/group/sub/repo", ); expect(cloneUrlRepositoryPath("C:/repos/project.git")).toBeNull(); + for (const prefix of ["https://host.example/", "ssh://git@host.example/"]) { + expect(cloneUrlRepositoryPath(`${prefix}Research%20Tools/r%C3%A9sum%C3%A9.git`)).toBe( + "host.example/Research Tools/résumé", + ); + expect(cloneUrlRepositoryPath(`${prefix}team/repo%2520name.git`)).toBe( + "host.example/team/repo%20name", + ); + expect(cloneUrlRepositoryPath(`${prefix}team/%2e%2e/repo.git`)).toBeNull(); + expect(cloneUrlRepositoryPath(`${prefix}team/repo%20.git`)).toBeNull(); + expect(cloneUrlRepositoryPath(`${prefix}team/bad%FF.git`)).toBeNull(); + } + expect(cloneUrlRepositoryPath("git@host.example:team/repo%20name.git")).toBe( + "host.example/team/repo%20name", + ); }); }); diff --git a/packages/shared/src/cloneUrl.ts b/packages/shared/src/cloneUrl.ts index aa259c2ce4c8a..37ffdb54f1652 100644 --- a/packages/shared/src/cloneUrl.ts +++ b/packages/shared/src/cloneUrl.ts @@ -80,7 +80,13 @@ export function cloneUrlRepositoryPath(url: string): string | null { if (isWindowsDrivePath(url.trim())) return null; const parsed = parseCloneUrl(url); if (!parsed) return null; - const segments = [parsed.host, ...parsed.path.split(/[?#]/)[0]!.split("/").filter(Boolean)]; + let repositoryPath = parsed.path.split(/[?#]/)[0]!; + try { + if (!parsed.scp) repositoryPath = decodeURIComponent(repositoryPath); + } catch { + return null; + } + const segments = [parsed.host, ...repositoryPath.split("/").filter(Boolean)]; const last = segments.at(-1)!; if (last.endsWith(".git")) segments[segments.length - 1] = last.slice(0, -".git".length); if (