|
|
@@ -1,6 +1,19 @@
|
|
|
import { ExecException } from "child_process"
|
|
|
-
|
|
|
-import { searchCommits, getCommitInfo, getWorkingState } from "../git"
|
|
|
+import * as vscode from "vscode"
|
|
|
+import * as fs from "fs"
|
|
|
+import * as path from "path"
|
|
|
+
|
|
|
+import {
|
|
|
+ searchCommits,
|
|
|
+ getCommitInfo,
|
|
|
+ getWorkingState,
|
|
|
+ getGitRepositoryInfo,
|
|
|
+ sanitizeGitUrl,
|
|
|
+ extractRepositoryName,
|
|
|
+ getWorkspaceGitInfo,
|
|
|
+ GitRepositoryInfo,
|
|
|
+} from "../git"
|
|
|
+import { truncateOutput } from "../../integrations/misc/extract-text"
|
|
|
|
|
|
type ExecFunction = (
|
|
|
command: string,
|
|
|
@@ -15,6 +28,24 @@ vitest.mock("child_process", () => ({
|
|
|
exec: vitest.fn(),
|
|
|
}))
|
|
|
|
|
|
+// Mock fs.promises
|
|
|
+vitest.mock("fs", () => ({
|
|
|
+ promises: {
|
|
|
+ access: vitest.fn(),
|
|
|
+ readFile: vitest.fn(),
|
|
|
+ },
|
|
|
+}))
|
|
|
+
|
|
|
+// Create a mock for vscode
|
|
|
+const mockWorkspaceFolders = vitest.fn()
|
|
|
+vitest.mock("vscode", () => ({
|
|
|
+ workspace: {
|
|
|
+ get workspaceFolders() {
|
|
|
+ return mockWorkspaceFolders()
|
|
|
+ },
|
|
|
+ },
|
|
|
+}))
|
|
|
+
|
|
|
// Mock util.promisify to return our own mock function
|
|
|
vitest.mock("util", () => ({
|
|
|
promisify: vitest.fn((fn: ExecFunction): PromisifiedExec => {
|
|
|
@@ -169,7 +200,6 @@ describe("git utils", () => {
|
|
|
if (command === cmd) {
|
|
|
callback(null, response)
|
|
|
return {} as any
|
|
|
- return {} as any
|
|
|
}
|
|
|
}
|
|
|
callback(new Error("Unexpected command"))
|
|
|
@@ -217,7 +247,6 @@ describe("git utils", () => {
|
|
|
if (command.startsWith(cmd)) {
|
|
|
callback(null, response)
|
|
|
return {} as any
|
|
|
- return {} as any
|
|
|
}
|
|
|
}
|
|
|
callback(new Error("Unexpected command"))
|
|
|
@@ -229,6 +258,7 @@ describe("git utils", () => {
|
|
|
expect(result).toContain("Author: John Doe")
|
|
|
expect(result).toContain("Files Changed:")
|
|
|
expect(result).toContain("Full Changes:")
|
|
|
+ expect(vitest.mocked(truncateOutput)).toHaveBeenCalled()
|
|
|
})
|
|
|
|
|
|
it("should return error message when git is not installed", async () => {
|
|
|
@@ -297,6 +327,7 @@ describe("git utils", () => {
|
|
|
expect(result).toContain("Working directory changes:")
|
|
|
expect(result).toContain("src/file1.ts")
|
|
|
expect(result).toContain("src/file2.ts")
|
|
|
+ expect(vitest.mocked(truncateOutput)).toHaveBeenCalled()
|
|
|
})
|
|
|
|
|
|
it("should return message when working directory is clean", async () => {
|
|
|
@@ -311,7 +342,6 @@ describe("git utils", () => {
|
|
|
if (command === cmd) {
|
|
|
callback(null, response)
|
|
|
return {} as any
|
|
|
- return {} as any
|
|
|
}
|
|
|
}
|
|
|
callback(new Error("Unexpected command"))
|
|
|
@@ -361,3 +391,315 @@ describe("git utils", () => {
|
|
|
})
|
|
|
})
|
|
|
})
|
|
|
+
|
|
|
+describe("getGitRepositoryInfo", () => {
|
|
|
+ const workspaceRoot = "/test/workspace"
|
|
|
+ const gitDir = path.join(workspaceRoot, ".git")
|
|
|
+ const configPath = path.join(gitDir, "config")
|
|
|
+ const headPath = path.join(gitDir, "HEAD")
|
|
|
+
|
|
|
+ beforeEach(() => {
|
|
|
+ vitest.clearAllMocks()
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should return empty object when not a git repository", async () => {
|
|
|
+ // Mock fs.access to throw error (directory doesn't exist)
|
|
|
+ vitest.mocked(fs.promises.access).mockRejectedValueOnce(new Error("ENOENT"))
|
|
|
+
|
|
|
+ const result = await getGitRepositoryInfo(workspaceRoot)
|
|
|
+
|
|
|
+ expect(result).toEqual({})
|
|
|
+ expect(fs.promises.access).toHaveBeenCalledWith(gitDir)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should extract repository info from git config", 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 content
|
|
|
+ const mockConfig = `
|
|
|
+[core]
|
|
|
+ repositoryformatversion = 0
|
|
|
+ filemode = true
|
|
|
+ bare = false
|
|
|
+ logallrefupdates = true
|
|
|
+ ignorecase = true
|
|
|
+ precomposeunicode = true
|
|
|
+[remote "origin"]
|
|
|
+ url = https://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)
|
|
|
+
|
|
|
+ expect(result).toEqual({
|
|
|
+ repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git",
|
|
|
+ repositoryName: "RooCodeInc/Roo-Code",
|
|
|
+ defaultBranch: "main",
|
|
|
+ })
|
|
|
+
|
|
|
+ // Verify config file was read
|
|
|
+ expect(gitSpy).toHaveBeenCalledWith(configPath, "utf8")
|
|
|
+
|
|
|
+ // The implementation might not always read the HEAD file if it already found the branch in config
|
|
|
+ // So we don't assert that it was called
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should handle missing repository URL in config", 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 without URL
|
|
|
+ const mockConfig = `
|
|
|
+[core]
|
|
|
+ repositoryformatversion = 0
|
|
|
+ filemode = true
|
|
|
+ bare = false
|
|
|
+`
|
|
|
+ // 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)
|
|
|
+
|
|
|
+ expect(result).toEqual({
|
|
|
+ defaultBranch: "main",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should handle errors when reading git config", 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)
|
|
|
+
|
|
|
+ // Setup the readFile mock to return different values based on the path
|
|
|
+ gitSpy.mockImplementation((path: any, encoding: any) => {
|
|
|
+ if (path === configPath) {
|
|
|
+ return Promise.reject(new Error("Failed to read config"))
|
|
|
+ } else if (path === headPath) {
|
|
|
+ return Promise.resolve("ref: refs/heads/main")
|
|
|
+ }
|
|
|
+ return Promise.reject(new Error(`Unexpected path: ${path}`))
|
|
|
+ })
|
|
|
+
|
|
|
+ const result = await getGitRepositoryInfo(workspaceRoot)
|
|
|
+
|
|
|
+ expect(result).toEqual({
|
|
|
+ defaultBranch: "main",
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should handle errors when reading HEAD file", 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)
|
|
|
+
|
|
|
+ // Setup the readFile mock to return different values based on the path
|
|
|
+ gitSpy.mockImplementation((path: any, encoding: any) => {
|
|
|
+ if (path === configPath) {
|
|
|
+ return Promise.resolve(`
|
|
|
+[remote "origin"]
|
|
|
+ url = https://github.com/RooCodeInc/Roo-Code.git
|
|
|
+`)
|
|
|
+ } else if (path === headPath) {
|
|
|
+ return Promise.reject(new Error("Failed to read HEAD"))
|
|
|
+ }
|
|
|
+ return Promise.reject(new Error(`Unexpected path: ${path}`))
|
|
|
+ })
|
|
|
+
|
|
|
+ const result = await getGitRepositoryInfo(workspaceRoot)
|
|
|
+
|
|
|
+ expect(result).toEqual({
|
|
|
+ repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git",
|
|
|
+ repositoryName: "RooCodeInc/Roo-Code",
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe("sanitizeGitUrl", () => {
|
|
|
+ it("should sanitize HTTPS URLs with credentials", () => {
|
|
|
+ const url = "https://username:[email protected]/RooCodeInc/Roo-Code.git"
|
|
|
+ const sanitized = sanitizeGitUrl(url)
|
|
|
+
|
|
|
+ expect(sanitized).toBe("https://github.com/RooCodeInc/Roo-Code.git")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should leave SSH URLs unchanged", () => {
|
|
|
+ const url = "[email protected]:RooCodeInc/Roo-Code.git"
|
|
|
+ const sanitized = sanitizeGitUrl(url)
|
|
|
+
|
|
|
+ expect(sanitized).toBe("[email protected]:RooCodeInc/Roo-Code.git")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should leave SSH URLs with ssh:// prefix unchanged", () => {
|
|
|
+ const url = "ssh://[email protected]/RooCodeInc/Roo-Code.git"
|
|
|
+ const sanitized = sanitizeGitUrl(url)
|
|
|
+
|
|
|
+ expect(sanitized).toBe("ssh://[email protected]/RooCodeInc/Roo-Code.git")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should remove tokens from other URL formats", () => {
|
|
|
+ const url = "https://oauth2:[email protected]/RooCodeInc/Roo-Code.git"
|
|
|
+ const sanitized = sanitizeGitUrl(url)
|
|
|
+
|
|
|
+ expect(sanitized).toBe("https://github.com/RooCodeInc/Roo-Code.git")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should handle invalid URLs gracefully", () => {
|
|
|
+ const url = "not-a-valid-url"
|
|
|
+ const sanitized = sanitizeGitUrl(url)
|
|
|
+
|
|
|
+ expect(sanitized).toBe("not-a-valid-url")
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe("extractRepositoryName", () => {
|
|
|
+ it("should extract repository name from HTTPS URL", () => {
|
|
|
+ const url = "https://github.com/RooCodeInc/Roo-Code.git"
|
|
|
+ const repoName = extractRepositoryName(url)
|
|
|
+
|
|
|
+ expect(repoName).toBe("RooCodeInc/Roo-Code")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should extract repository name from HTTPS URL without .git suffix", () => {
|
|
|
+ const url = "https://github.com/RooCodeInc/Roo-Code"
|
|
|
+ const repoName = extractRepositoryName(url)
|
|
|
+
|
|
|
+ expect(repoName).toBe("RooCodeInc/Roo-Code")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should extract repository name from SSH URL", () => {
|
|
|
+ const url = "[email protected]:RooCodeInc/Roo-Code.git"
|
|
|
+ const repoName = extractRepositoryName(url)
|
|
|
+
|
|
|
+ expect(repoName).toBe("RooCodeInc/Roo-Code")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should extract repository name from SSH URL with ssh:// prefix", () => {
|
|
|
+ const url = "ssh://[email protected]/RooCodeInc/Roo-Code.git"
|
|
|
+ const repoName = extractRepositoryName(url)
|
|
|
+
|
|
|
+ expect(repoName).toBe("RooCodeInc/Roo-Code")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should return empty string for unrecognized URL formats", () => {
|
|
|
+ const url = "not-a-valid-git-url"
|
|
|
+ const repoName = extractRepositoryName(url)
|
|
|
+
|
|
|
+ expect(repoName).toBe("")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should handle URLs with credentials", () => {
|
|
|
+ const url = "https://username:[email protected]/RooCodeInc/Roo-Code.git"
|
|
|
+ const repoName = extractRepositoryName(url)
|
|
|
+
|
|
|
+ expect(repoName).toBe("RooCodeInc/Roo-Code")
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe("getWorkspaceGitInfo", () => {
|
|
|
+ const workspaceRoot = "/test/workspace"
|
|
|
+
|
|
|
+ beforeEach(() => {
|
|
|
+ vitest.clearAllMocks()
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should return empty object when no workspace folders", async () => {
|
|
|
+ // Mock workspace with no folders
|
|
|
+ mockWorkspaceFolders.mockReturnValue(undefined)
|
|
|
+
|
|
|
+ const result = await getWorkspaceGitInfo()
|
|
|
+
|
|
|
+ expect(result).toEqual({})
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should return git info for the first workspace folder", async () => {
|
|
|
+ // Clear previous mocks
|
|
|
+ vitest.clearAllMocks()
|
|
|
+
|
|
|
+ // Mock workspace with one folder
|
|
|
+ mockWorkspaceFolders.mockReturnValue([{ uri: { fsPath: workspaceRoot }, name: "workspace", index: 0 }])
|
|
|
+
|
|
|
+ // Create a spy to track the implementation
|
|
|
+ const gitSpy = vitest.spyOn(fs.promises, "access")
|
|
|
+ const readFileSpy = vitest.spyOn(fs.promises, "readFile")
|
|
|
+
|
|
|
+ // Mock successful access to .git directory
|
|
|
+ gitSpy.mockResolvedValue(undefined)
|
|
|
+
|
|
|
+ // Mock git config file content
|
|
|
+ const mockConfig = `
|
|
|
+[remote "origin"]
|
|
|
+ url = https://github.com/RooCodeInc/Roo-Code.git
|
|
|
+[branch "main"]
|
|
|
+ remote = origin
|
|
|
+ merge = refs/heads/main
|
|
|
+`
|
|
|
+
|
|
|
+ // Setup the readFile mock to return config content
|
|
|
+ readFileSpy.mockImplementation((path: any, encoding: any) => {
|
|
|
+ if (path.includes("config")) {
|
|
|
+ return Promise.resolve(mockConfig)
|
|
|
+ }
|
|
|
+ return Promise.reject(new Error(`Unexpected path: ${path}`))
|
|
|
+ })
|
|
|
+
|
|
|
+ const result = await getWorkspaceGitInfo()
|
|
|
+
|
|
|
+ expect(result).toEqual({
|
|
|
+ repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git",
|
|
|
+ repositoryName: "RooCodeInc/Roo-Code",
|
|
|
+ defaultBranch: "main",
|
|
|
+ })
|
|
|
+
|
|
|
+ // Verify the fs operations were called with the correct workspace path
|
|
|
+ expect(gitSpy).toHaveBeenCalled()
|
|
|
+ expect(readFileSpy).toHaveBeenCalled()
|
|
|
+ })
|
|
|
+})
|