diff --git a/src/utils/__tests__/git.spec.ts b/src/utils/__tests__/git.spec.ts index 3dc0f6fe24..3ab306feec 100644 --- a/src/utils/__tests__/git.spec.ts +++ b/src/utils/__tests__/git.spec.ts @@ -12,6 +12,7 @@ import { extractRepositoryName, getWorkspaceGitInfo, GitRepositoryInfo, + convertGitUrlToHttps, } from "../git" import { truncateOutput } from "../../integrations/misc/extract-text" @@ -560,6 +561,89 @@ describe("getGitRepositoryInfo", () => { repositoryName: "RooCodeInc/Roo-Code", }) }) + + it("should convert SSH URLs to HTTPS format", async () => { + // Clear previous mocks + vitest.clearAllMocks() + + // Create a spy to track the implementation + const gitSpy = vitest.spyOn(fs.promises, "readFile") + + // Mock successful access to .git directory + vitest.mocked(fs.promises.access).mockResolvedValue(undefined) + + // Mock git config file with SSH URL + const mockConfig = ` +[core] + repositoryformatversion = 0 + filemode = true + bare = false +[remote "origin"] + url = git@github.com:RooCodeInc/Roo-Code.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "main"] + remote = origin + merge = refs/heads/main +` + // Mock HEAD file content + const mockHead = "ref: refs/heads/main" + + // Setup the readFile mock to return different values based on the path + gitSpy.mockImplementation((path: any, encoding: any) => { + if (path === configPath) { + return Promise.resolve(mockConfig) + } else if (path === headPath) { + return Promise.resolve(mockHead) + } + return Promise.reject(new Error(`Unexpected path: ${path}`)) + }) + + const result = await getGitRepositoryInfo(workspaceRoot) + + // Verify that the SSH URL was converted to HTTPS + expect(result).toEqual({ + repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git", + repositoryName: "RooCodeInc/Roo-Code", + defaultBranch: "main", + }) + }) +}) + +describe("convertGitUrlToHttps", () => { + it("should leave HTTPS URLs unchanged", () => { + const url = "https://github.com/RooCodeInc/Roo-Code.git" + const converted = convertGitUrlToHttps(url) + + expect(converted).toBe("https://github.com/RooCodeInc/Roo-Code.git") + }) + + it("should convert SSH URLs to HTTPS format", () => { + const url = "git@github.com:RooCodeInc/Roo-Code.git" + const converted = convertGitUrlToHttps(url) + + expect(converted).toBe("https://github.com/RooCodeInc/Roo-Code.git") + }) + + it("should convert SSH URLs with ssh:// prefix to HTTPS format", () => { + const url = "ssh://git@github.com/RooCodeInc/Roo-Code.git" + const converted = convertGitUrlToHttps(url) + + expect(converted).toBe("https://github.com/RooCodeInc/Roo-Code.git") + }) + + it("should handle URLs without git@ prefix", () => { + const url = "ssh://github.com/RooCodeInc/Roo-Code.git" + const converted = convertGitUrlToHttps(url) + + expect(converted).toBe("https://github.com/RooCodeInc/Roo-Code.git") + }) + + it("should handle invalid URLs gracefully", () => { + const url = "not-a-valid-url" + const converted = convertGitUrlToHttps(url) + + expect(converted).toBe("not-a-valid-url") + }) }) describe("sanitizeGitUrl", () => { diff --git a/src/utils/git.ts b/src/utils/git.ts index 7ecf33172b..fd6abfa309 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -51,7 +51,8 @@ export async function getGitRepositoryInfo(workspaceRoot: string): Promise https://github.com/user/repo.git + if (url.startsWith("git@")) { + const match = url.match(/git@([^:]+):(.+)/) + if (match && match.length === 3) { + const [, host, path] = match + return `https://${host}/${path}` + } + } + + // Handle SSH with protocol: ssh://git@github.com/user/repo.git -> https://github.com/user/repo.git + if (url.startsWith("ssh://")) { + const match = url.match(/ssh:\/\/(?:git@)?([^\/]+)\/(.+)/) + if (match && match.length === 3) { + const [, host, path] = match + return `https://${host}/${path}` + } + } + + // Return original URL if we can't convert it + return url + } catch { + // If parsing fails, return original + return url + } +} + /** * Sanitizes a git URL to remove sensitive information like tokens * @param url The original git URL