From 0adce69b3bdf2aae2df1f33f285ab489ed4cbb79 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 12 Feb 2026 21:28:06 +0100 Subject: [PATCH] fix(coding-agent): tighten git source parsing and local path normalization (fixes #1426) --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/README.md | 5 + packages/coding-agent/docs/packages.md | 12 +- packages/coding-agent/src/main.ts | 3 +- packages/coding-agent/src/utils/git.ts | 19 +- .../coding-agent/test/git-ssh-url.test.ts | 145 ++++++---------- .../test/package-manager-ssh.test.ts | 164 +++++------------- .../coding-agent/test/package-manager.test.ts | 45 +++-- 8 files changed, 152 insertions(+), 242 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 36a5537c..abcbe9fd 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Breaking Changes - `ContextUsage.tokens` and `ContextUsage.percent` are now `number | null`. After compaction, context token count is unknown until the next LLM response, so these fields return `null`. Extensions that read `ContextUsage` must handle the `null` case. Removed `usageTokens`, `trailingTokens`, and `lastUsageIndex` fields from `ContextUsage` (implementation details that should not have been public) ([#1382](https://github.com/badlogic/pi-mono/pull/1382) by [@ferologics](https://github.com/ferologics)) +- Git source parsing is now strict without `git:` prefix: only protocol URLs are treated as git (`https://`, `http://`, `ssh://`, `git://`). Shorthand sources like `github.com/org/repo` and `git@github.com:org/repo` now require the `git:` prefix. ([#1426](https://github.com/badlogic/pi-mono/issues/1426)) ### Added diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 98f632c6..4ee5d130 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -341,7 +341,12 @@ pi install npm:@foo/pi-tools pi install npm:@foo/pi-tools@1.2.3 # pinned version pi install git:github.com/user/repo pi install git:github.com/user/repo@v1 # tag or commit +pi install git:git@github.com:user/repo +pi install git:git@github.com:user/repo@v1 # tag or commit pi install https://github.com/user/repo +pi install https://github.com/user/repo@v1 # tag or commit +pi install ssh://git@github.com/user/repo +pi install ssh://git@github.com/user/repo@v1 # tag or commit pi remove npm:@foo/pi-tools pi list pi update # skips pinned packages diff --git a/packages/coding-agent/docs/packages.md b/packages/coding-agent/docs/packages.md index 4d522fbf..bf801fdd 100644 --- a/packages/coding-agent/docs/packages.md +++ b/packages/coding-agent/docs/packages.md @@ -59,32 +59,30 @@ npm:pkg ``` git:github.com/user/repo@v1 +git:git@github.com:user/repo@v1 https://github.com/user/repo@v1 -git@github.com:user/repo@v1 ssh://git@github.com/user/repo@v1 ``` +- Without `git:` prefix, only protocol URLs are accepted (`https://`, `http://`, `ssh://`, `git://`). +- With `git:` prefix, shorthand formats are accepted, including `github.com/user/repo` and `git@github.com:user/repo`. - HTTPS and SSH URLs are both supported. - SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`). - For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast. -- Raw `https://` URLs work without the `git:` prefix. - Refs pin the package and skip `pi update`. - Cloned to `~/.pi/agent/git//` (global) or `.pi/git//` (project). - Runs `npm install` after clone or pull if `package.json` exists. **SSH examples:** ```bash -# Standard git@host:path format -pi install git@github.com:user/repo - -# With git: prefix +# git@host:path shorthand (requires git: prefix) pi install git:git@github.com:user/repo # ssh:// protocol format pi install ssh://git@github.com/user/repo # With version ref -pi install git@github.com:user/repo@v1.0.0 +pi install git:git@github.com:user/repo@v1.0.0 ``` ### Local Paths diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 17ae99e1..ecd1242d 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -92,8 +92,9 @@ Options: Examples: ${APP_NAME} install npm:@foo/bar ${APP_NAME} install git:github.com/user/repo + ${APP_NAME} install git:git@github.com:user/repo ${APP_NAME} install https://github.com/user/repo - ${APP_NAME} install git@github.com:user/repo + ${APP_NAME} install ssh://git@github.com/user/repo ${APP_NAME} install ./local/path `); return; diff --git a/packages/coding-agent/src/utils/git.ts b/packages/coding-agent/src/utils/git.ts index cb9e0cdb..9652e401 100644 --- a/packages/coding-agent/src/utils/git.ts +++ b/packages/coding-agent/src/utils/git.ts @@ -86,7 +86,8 @@ function parseGenericGitUrl(url: string): GitSource | null { } else if ( repoWithoutRef.startsWith("https://") || repoWithoutRef.startsWith("http://") || - repoWithoutRef.startsWith("ssh://") + repoWithoutRef.startsWith("ssh://") || + repoWithoutRef.startsWith("git://") ) { try { const parsed = new URL(repoWithoutRef); @@ -124,10 +125,21 @@ function parseGenericGitUrl(url: string): GitSource | null { } /** - * Parse any git URL (SSH or HTTPS) into a GitSource. + * Parse git source into a GitSource. + * + * Rules: + * - With git: prefix, accept all historical shorthand forms. + * - Without git: prefix, only accept explicit protocol URLs. */ export function parseGitUrl(source: string): GitSource | null { - const url = source.startsWith("git:") ? source.slice(4).trim() : source; + const trimmed = source.trim(); + const hasGitPrefix = trimmed.startsWith("git:"); + const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed; + + if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) { + return null; + } + const split = splitRef(url); const hostedCandidates = [split.ref ? `${split.repo}#${split.ref}` : undefined, url].filter( @@ -143,6 +155,7 @@ export function parseGitUrl(source: string): GitSource | null { !split.repo.startsWith("http://") && !split.repo.startsWith("https://") && !split.repo.startsWith("ssh://") && + !split.repo.startsWith("git://") && !split.repo.startsWith("git@"); return { type: "git", diff --git a/packages/coding-agent/test/git-ssh-url.test.ts b/packages/coding-agent/test/git-ssh-url.test.ts index 543fed62..a7d24267 100644 --- a/packages/coding-agent/test/git-ssh-url.test.ts +++ b/packages/coding-agent/test/git-ssh-url.test.ts @@ -1,89 +1,8 @@ import { describe, expect, it } from "vitest"; import { parseGitUrl } from "../src/utils/git.js"; -describe("SSH Git URL Parsing", () => { - describe("ssh:// protocol", () => { - it("should parse basic ssh:// URL", () => { - const result = parseGitUrl("ssh://git@github.com/user/repo"); - expect(result).toMatchObject({ - host: "github.com", - path: "user/repo", - repo: "ssh://git@github.com/user/repo", - }); - expect(result?.ref).toBeUndefined(); - }); - - it("should parse ssh:// URL with port", () => { - const result = parseGitUrl("ssh://git@github.com:22/user/repo"); - expect(result).toMatchObject({ - host: "github.com", - path: "user/repo", - repo: "ssh://git@github.com:22/user/repo", - }); - }); - - it("should parse ssh:// URL with .git suffix", () => { - const result = parseGitUrl("ssh://git@github.com/user/repo.git"); - expect(result).toMatchObject({ - host: "github.com", - path: "user/repo", - repo: "ssh://git@github.com/user/repo.git", - }); - }); - - it("should parse ssh:// URL with ref", () => { - const result = parseGitUrl("ssh://git@github.com/user/repo@v1.0.0"); - expect(result).toMatchObject({ - host: "github.com", - path: "user/repo", - ref: "v1.0.0", - repo: "ssh://git@github.com/user/repo", - }); - }); - }); - - describe("git@host:path pattern", () => { - it("should parse basic git@host:path", () => { - const result = parseGitUrl("git@github.com:user/repo"); - expect(result).toMatchObject({ - host: "github.com", - path: "user/repo", - repo: "git@github.com:user/repo", - }); - expect(result?.ref).toBeUndefined(); - }); - - it("should parse git@host:path with .git", () => { - const result = parseGitUrl("git@github.com:user/repo.git"); - expect(result).toMatchObject({ - host: "github.com", - path: "user/repo", - repo: "git@github.com:user/repo.git", - }); - }); - - it("should parse git@host:path with ref", () => { - const result = parseGitUrl("git@github.com:user/repo@v1.0.0"); - expect(result).toMatchObject({ - host: "github.com", - path: "user/repo", - ref: "v1.0.0", - repo: "git@github.com:user/repo", - }); - }); - - it("should parse git@host:path with ref and .git", () => { - const result = parseGitUrl("git@github.com:user/repo.git@main"); - expect(result).toMatchObject({ - host: "github.com", - path: "user/repo", - ref: "main", - repo: "git@github.com:user/repo.git", - }); - }); - }); - - describe("HTTPS URLs", () => { +describe("Git URL Parsing", () => { + describe("protocol URLs (accepted without git: prefix)", () => { it("should parse HTTPS URL", () => { const result = parseGitUrl("https://github.com/user/repo"); expect(result).toMatchObject({ @@ -93,25 +12,67 @@ describe("SSH Git URL Parsing", () => { }); }); - it("should parse shorthand URL", () => { - const result = parseGitUrl("github.com/user/repo"); + it("should parse ssh:// URL", () => { + const result = parseGitUrl("ssh://git@github.com/user/repo"); + expect(result).toMatchObject({ + host: "github.com", + path: "user/repo", + repo: "ssh://git@github.com/user/repo", + }); + }); + + it("should parse protocol URL with ref", () => { + const result = parseGitUrl("https://github.com/user/repo@v1.0.0"); + expect(result).toMatchObject({ + host: "github.com", + path: "user/repo", + ref: "v1.0.0", + repo: "https://github.com/user/repo", + }); + }); + }); + + describe("shorthand URLs (accepted only with git: prefix)", () => { + it("should parse git@host:path with git: prefix", () => { + const result = parseGitUrl("git:git@github.com:user/repo"); + expect(result).toMatchObject({ + host: "github.com", + path: "user/repo", + repo: "git@github.com:user/repo", + }); + }); + + it("should parse host/path shorthand with git: prefix", () => { + const result = parseGitUrl("git:github.com/user/repo"); expect(result).toMatchObject({ host: "github.com", path: "user/repo", repo: "https://github.com/user/repo", }); }); + + it("should parse shorthand with ref and git: prefix", () => { + const result = parseGitUrl("git:git@github.com:user/repo@v1.0.0"); + expect(result).toMatchObject({ + host: "github.com", + path: "user/repo", + ref: "v1.0.0", + repo: "git@github.com:user/repo", + }); + }); }); - describe("edge cases", () => { - it("should return null for invalid URLs", () => { - expect(parseGitUrl("git@github.com")).toBeNull(); - expect(parseGitUrl("not-a-url")).toBeNull(); + describe("unsupported without git: prefix", () => { + it("should reject git@host:path without git: prefix", () => { + expect(parseGitUrl("git@github.com:user/repo")).toBeNull(); }); - it("should handle different hosts", () => { - expect(parseGitUrl("git@gitlab.com:user/repo")?.host).toBe("gitlab.com"); - expect(parseGitUrl("git@bitbucket.org:user/repo")?.host).toBe("bitbucket.org"); + it("should reject host/path shorthand without git: prefix", () => { + expect(parseGitUrl("github.com/user/repo")).toBeNull(); + }); + + it("should reject user/repo shorthand", () => { + expect(parseGitUrl("user/repo")).toBeNull(); }); }); }); diff --git a/packages/coding-agent/test/package-manager-ssh.test.ts b/packages/coding-agent/test/package-manager-ssh.test.ts index 5c681c10..3dc04ab1 100644 --- a/packages/coding-agent/test/package-manager-ssh.test.ts +++ b/packages/coding-agent/test/package-manager-ssh.test.ts @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DefaultPackageManager } from "../src/core/package-manager.js"; import { SettingsManager } from "../src/core/settings-manager.js"; -describe("Package Manager SSH URL Support", () => { +describe("Package Manager git source parsing", () => { let tempDir: string; let agentDir: string; let settingsManager: SettingsManager; @@ -29,9 +29,26 @@ describe("Package Manager SSH URL Support", () => { rmSync(tempDir, { recursive: true, force: true }); }); - describe("parseSource with SSH URLs", () => { + describe("protocol URLs without git: prefix", () => { + it("should parse https:// URL", () => { + const parsed = (packageManager as any).parseSource("https://github.com/user/repo"); + expect(parsed.type).toBe("git"); + expect(parsed.host).toBe("github.com"); + expect(parsed.path).toBe("user/repo"); + }); + + it("should parse ssh:// URL", () => { + const parsed = (packageManager as any).parseSource("ssh://git@github.com/user/repo"); + expect(parsed.type).toBe("git"); + expect(parsed.host).toBe("github.com"); + expect(parsed.path).toBe("user/repo"); + expect(parsed.repo).toBe("ssh://git@github.com/user/repo"); + }); + }); + + describe("shorthand URLs with git: prefix", () => { it("should parse git@host:path format", () => { - const parsed = (packageManager as any).parseSource("git@github.com:user/repo"); + const parsed = (packageManager as any).parseSource("git:git@github.com:user/repo"); expect(parsed.type).toBe("git"); expect(parsed.host).toBe("github.com"); expect(parsed.path).toBe("user/repo"); @@ -39,137 +56,42 @@ describe("Package Manager SSH URL Support", () => { expect(parsed.pinned).toBe(false); }); - it("should parse git@host:path with ref", () => { - const parsed = (packageManager as any).parseSource("git@github.com:user/repo@v1.0.0"); + it("should parse host/path shorthand", () => { + const parsed = (packageManager as any).parseSource("git:github.com/user/repo"); expect(parsed.type).toBe("git"); expect(parsed.host).toBe("github.com"); expect(parsed.path).toBe("user/repo"); + }); + + it("should parse shorthand with ref", () => { + const parsed = (packageManager as any).parseSource("git:git@github.com:user/repo@v1.0.0"); + expect(parsed.type).toBe("git"); expect(parsed.ref).toBe("v1.0.0"); - expect(parsed.repo).toBe("git@github.com:user/repo"); expect(parsed.pinned).toBe(true); }); + }); - it("should parse ssh:// protocol format", () => { - const parsed = (packageManager as any).parseSource("ssh://git@github.com/user/repo"); - expect(parsed.type).toBe("git"); - expect(parsed.host).toBe("github.com"); - expect(parsed.path).toBe("user/repo"); - expect(parsed.repo).toBe("ssh://git@github.com/user/repo"); + describe("unsupported without git: prefix", () => { + it("should treat git@host:path as local without git: prefix", () => { + const parsed = (packageManager as any).parseSource("git@github.com:user/repo"); + expect(parsed.type).toBe("local"); }); - it("should parse ssh:// with port", () => { - const parsed = (packageManager as any).parseSource("ssh://git@github.com:22/user/repo"); - expect(parsed.type).toBe("git"); - expect(parsed.host).toBe("github.com"); - expect(parsed.path).toBe("user/repo"); - expect(parsed.repo).toBe("ssh://git@github.com:22/user/repo"); - }); - - it("should parse git: prefix with SSH URL", () => { - const parsed = (packageManager as any).parseSource("git:git@github.com:user/repo"); - expect(parsed.type).toBe("git"); - expect(parsed.host).toBe("github.com"); - expect(parsed.path).toBe("user/repo"); - expect(parsed.repo).toBe("git@github.com:user/repo"); - }); - - it("should parse .git suffix", () => { - const parsed = (packageManager as any).parseSource("git@github.com:user/repo.git"); - expect(parsed.type).toBe("git"); - expect(parsed.path).toBe("user/repo"); - expect(parsed.repo).toBe("git@github.com:user/repo.git"); + it("should treat host/path shorthand as local without git: prefix", () => { + const parsed = (packageManager as any).parseSource("github.com/user/repo"); + expect(parsed.type).toBe("local"); }); }); - describe("getPackageIdentity with SSH URLs", () => { - it("should normalize SSH URL to same identity as HTTPS", () => { - const sshIdentity = (packageManager as any).getPackageIdentity("git@github.com:user/repo"); - const httpsIdentity = (packageManager as any).getPackageIdentity("https://github.com/user/repo"); - expect(sshIdentity).toBe(httpsIdentity); - expect(sshIdentity).toBe("git:github.com/user/repo"); - }); + describe("identity normalization", () => { + it("should normalize protocol and shorthand-prefixed URLs to same identity", () => { + const prefixed = (packageManager as any).getPackageIdentity("git:git@github.com:user/repo"); + const https = (packageManager as any).getPackageIdentity("https://github.com/user/repo"); + const ssh = (packageManager as any).getPackageIdentity("ssh://git@github.com/user/repo"); - it("should normalize ssh:// to same identity as git@", () => { - const sshProtocol = (packageManager as any).getPackageIdentity("ssh://git@github.com/user/repo"); - const gitAt = (packageManager as any).getPackageIdentity("git@github.com:user/repo"); - expect(sshProtocol).toBe(gitAt); - }); - - it("should ignore ref in identity", () => { - const withRef = (packageManager as any).getPackageIdentity("git@github.com:user/repo@v1.0.0"); - const withoutRef = (packageManager as any).getPackageIdentity("git@github.com:user/repo"); - expect(withRef).toBe(withoutRef); - }); - }); - - describe("SSH URL install behavior", () => { - it("should emit start event for SSH URL install", async () => { - const events: any[] = []; - packageManager.setProgressCallback((event) => events.push(event)); - - try { - await packageManager.install("git@github.com:nonexistent/repo"); - } catch { - // Expected to fail - repo doesn't exist or no SSH access - } - - expect(events.some((e) => e.type === "start" && e.action === "install")).toBe(true); - }); - - it("should emit start event for ssh:// URL install", async () => { - const events: any[] = []; - packageManager.setProgressCallback((event) => events.push(event)); - - try { - await packageManager.install("ssh://git@github.com/nonexistent/repo"); - } catch { - // Expected to fail - } - - expect(events.some((e) => e.type === "start" && e.action === "install")).toBe(true); - }); - }); - - describe("different git hosts", () => { - it("should parse GitLab SSH URL", () => { - const parsed = (packageManager as any).parseSource("git@gitlab.com:group/project"); - expect(parsed.type).toBe("git"); - expect(parsed.host).toBe("gitlab.com"); - expect(parsed.path).toBe("group/project"); - expect(parsed.repo).toBe("git@gitlab.com:group/project"); - }); - - it("should parse Bitbucket SSH URL", () => { - const parsed = (packageManager as any).parseSource("git@bitbucket.org:team/repo"); - expect(parsed.type).toBe("git"); - expect(parsed.host).toBe("bitbucket.org"); - expect(parsed.path).toBe("team/repo"); - expect(parsed.repo).toBe("git@bitbucket.org:team/repo"); - }); - - it("should parse unknown enterprise host shorthand", () => { - const parsed = (packageManager as any).parseSource("git:github.tools.sap/agent-dev/sap-pie"); - expect(parsed.type).toBe("git"); - expect(parsed.host).toBe("github.tools.sap"); - expect(parsed.path).toBe("agent-dev/sap-pie"); - expect(parsed.repo).toBe("https://github.tools.sap/agent-dev/sap-pie"); - }); - - it("should parse unknown enterprise host with ref", () => { - const parsed = (packageManager as any).parseSource("git:github.tools.sap/agent-dev/sap-pie@v1"); - expect(parsed.type).toBe("git"); - expect(parsed.host).toBe("github.tools.sap"); - expect(parsed.path).toBe("agent-dev/sap-pie"); - expect(parsed.ref).toBe("v1"); - expect(parsed.repo).toBe("https://github.tools.sap/agent-dev/sap-pie"); - expect(parsed.pinned).toBe(true); - }); - - it("should normalize unknown enterprise host identities", () => { - const withPrefix = (packageManager as any).getPackageIdentity("git:github.tools.sap/agent-dev/sap-pie"); - const withHttps = (packageManager as any).getPackageIdentity("https://github.tools.sap/agent-dev/sap-pie"); - expect(withPrefix).toBe("git:github.tools.sap/agent-dev/sap-pie"); - expect(withPrefix).toBe(withHttps); + expect(prefixed).toBe("git:github.com/user/repo"); + expect(prefixed).toBe(https); + expect(prefixed).toBe(ssh); }); }); }); diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index d93da4be..5fcb043b 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -287,7 +287,7 @@ Content`, expect((packageManager as any).parseSource("git:github.com/user/repo@v1").type).toBe("git"); expect((packageManager as any).parseSource("https://github.com/user/repo@v1").type).toBe("git"); - expect((packageManager as any).parseSource("git@github.com:user/repo@v1").type).toBe("git"); + expect((packageManager as any).parseSource("git:git@github.com:user/repo@v1").type).toBe("git"); expect((packageManager as any).parseSource("ssh://git@github.com/user/repo@v1").type).toBe("git"); expect((packageManager as any).parseSource("/absolute/path/to/package").type).toBe("local"); @@ -316,7 +316,9 @@ Content`, expect(added).toBe(true); const settings = settingsManager.getGlobalSettings(); - expect(settings.packages?.[0]).toBe(relative(agentDir, pkgDir) || "."); + const rel = relative(agentDir, pkgDir); + const expected = rel.startsWith(".") ? rel : `./${rel}`; + expect(settings.packages?.[0]).toBe(expected); }); it("should store project local packages relative to .pi settings base", () => { @@ -328,7 +330,9 @@ Content`, expect(added).toBe(true); const settings = settingsManager.getProjectSettings(); - expect(settings.packages?.[0]).toBe(relative(join(tempDir, ".pi"), projectPkgDir) || "."); + const rel = relative(join(tempDir, ".pi"), projectPkgDir); + const expected = rel.startsWith(".") ? rel : `./${rel}`; + expect(settings.packages?.[0]).toBe(expected); }); it("should remove local package entries using equivalent path forms", () => { @@ -368,13 +372,18 @@ Content`, expect(parsed.pinned).toBe(true); }); - it("should parse HTTPS URLs without protocol", async () => { - const parsed = (packageManager as any).parseSource("github.com/user/repo"); + it("should parse host/path shorthand only with git: prefix", async () => { + const parsed = (packageManager as any).parseSource("git:github.com/user/repo"); expect(parsed.type).toBe("git"); expect(parsed.host).toBe("github.com"); expect(parsed.path).toBe("user/repo"); }); + it("should treat host/path shorthand as local without git: prefix", async () => { + const parsed = (packageManager as any).parseSource("github.com/user/repo"); + expect(parsed.type).toBe("local"); + }); + it("should parse HTTPS URLs with .git suffix", async () => { const parsed = (packageManager as any).parseSource("https://github.com/user/repo.git"); expect(parsed.type).toBe("git"); @@ -403,10 +412,10 @@ Content`, expect(parsed.path).toBe("user/repo"); }); - it("should generate correct package identity for HTTPS URLs", async () => { + it("should generate correct package identity for protocol and git:-prefixed URLs", async () => { const identity1 = (packageManager as any).getPackageIdentity("https://github.com/user/repo"); const identity2 = (packageManager as any).getPackageIdentity("https://github.com/user/repo@v1.0.0"); - const identity3 = (packageManager as any).getPackageIdentity("github.com/user/repo"); + const identity3 = (packageManager as any).getPackageIdentity("git:github.com/user/repo"); const identity4 = (packageManager as any).getPackageIdentity("https://github.com/user/repo.git"); // All should have the same identity (normalized) @@ -416,7 +425,7 @@ Content`, expect(identity4).toBe("git:github.com/user/repo"); }); - it("should deduplicate HTTPS URLs with different formats", async () => { + it("should deduplicate git URLs with different supported formats", async () => { const pkgDir = join(tempDir, "https-dedup-pkg"); mkdirSync(join(pkgDir, "extensions"), { recursive: true }); writeFileSync(join(pkgDir, "extensions", "test.ts"), "export default function() {}"); @@ -425,14 +434,14 @@ Content`, // In reality, these would all point to the same local dir after install settingsManager.setPackages([ "https://github.com/user/repo", - "github.com/user/repo", + "git:github.com/user/repo", "https://github.com/user/repo.git", ]); // Since these URLs don't actually exist and we can't clone them, // we verify they produce the same identity const id1 = (packageManager as any).getPackageIdentity("https://github.com/user/repo"); - const id2 = (packageManager as any).getPackageIdentity("github.com/user/repo"); + const id2 = (packageManager as any).getPackageIdentity("git:github.com/user/repo"); const id3 = (packageManager as any).getPackageIdentity("https://github.com/user/repo.git"); expect(id1).toBe(id2); @@ -915,7 +924,7 @@ Content`, it("should dedupe SSH and HTTPS URLs for same repo", async () => { // Same repository, different URL formats const httpsUrl = "https://github.com/user/repo"; - const sshUrl = "git@github.com:user/repo"; + const sshUrl = "git:git@github.com:user/repo"; const httpsIdentity = (packageManager as any).getPackageIdentity(httpsUrl); const sshIdentity = (packageManager as any).getPackageIdentity(sshUrl); @@ -928,7 +937,7 @@ Content`, it("should dedupe SSH and HTTPS with refs", async () => { const httpsUrl = "https://github.com/user/repo@v1.0.0"; - const sshUrl = "git@github.com:user/repo@v1.0.0"; + const sshUrl = "git:git@github.com:user/repo@v1.0.0"; const httpsIdentity = (packageManager as any).getPackageIdentity(httpsUrl); const sshIdentity = (packageManager as any).getPackageIdentity(sshUrl); @@ -941,7 +950,7 @@ Content`, it("should dedupe SSH URL with ssh:// protocol and git@ format", async () => { const sshProtocol = "ssh://git@github.com/user/repo"; - const gitAt = "git@github.com:user/repo"; + const gitAt = "git:git@github.com:user/repo"; const sshProtocolIdentity = (packageManager as any).getPackageIdentity(sshProtocol); const gitAtIdentity = (packageManager as any).getPackageIdentity(gitAt); @@ -952,15 +961,15 @@ Content`, expect(sshProtocolIdentity).toBe(gitAtIdentity); }); - it("should dedupe all URL formats for same repo (HTTPS, SSH, git@)", async () => { + it("should dedupe all supported URL formats for same repo", async () => { const urls = [ "https://github.com/user/repo", - "github.com/user/repo", "https://github.com/user/repo.git", - "git@github.com:user/repo", - "git@github.com:user/repo.git", "ssh://git@github.com/user/repo", "git:https://github.com/user/repo", + "git:github.com/user/repo", + "git:git@github.com:user/repo", + "git:git@github.com:user/repo.git", ]; const identities = urls.map((url) => (packageManager as any).getPackageIdentity(url)); @@ -973,7 +982,7 @@ Content`, it("should keep different repos separate (HTTPS vs SSH)", async () => { const repo1Https = "https://github.com/user/repo1"; - const repo2Ssh = "git@github.com:user/repo2"; + const repo2Ssh = "git:git@github.com:user/repo2"; const id1 = (packageManager as any).getPackageIdentity(repo1Https); const id2 = (packageManager as any).getPackageIdentity(repo2Ssh);