Skip to content

Commit e91b426

Browse files
committed
added tests for git functions
1 parent c3b6f57 commit e91b426

File tree

2 files changed

+346
-4
lines changed

2 files changed

+346
-4
lines changed

src/utils/__tests__/git.spec.ts

Lines changed: 344 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import { ExecException } from "child_process"
2-
3-
import { searchCommits, getCommitInfo, getWorkingState } from "../git"
2+
import * as vscode from "vscode"
3+
import * as fs from "fs"
4+
import * as path from "path"
5+
6+
import {
7+
searchCommits,
8+
getCommitInfo,
9+
getWorkingState,
10+
getGitRepositoryInfo,
11+
sanitizeGitUrl,
12+
extractRepositoryName,
13+
getWorkspaceGitInfo,
14+
GitRepositoryInfo,
15+
} from "../git"
416
import { truncateOutput } from "../../integrations/misc/extract-text"
517

618
type ExecFunction = (
@@ -16,6 +28,24 @@ vitest.mock("child_process", () => ({
1628
exec: vitest.fn(),
1729
}))
1830

31+
// Mock fs.promises
32+
vitest.mock("fs", () => ({
33+
promises: {
34+
access: vitest.fn(),
35+
readFile: vitest.fn(),
36+
},
37+
}))
38+
39+
// Create a mock for vscode
40+
const mockWorkspaceFolders = vitest.fn()
41+
vitest.mock("vscode", () => ({
42+
workspace: {
43+
get workspaceFolders() {
44+
return mockWorkspaceFolders()
45+
},
46+
},
47+
}))
48+
1949
// Mock util.promisify to return our own mock function
2050
vitest.mock("util", () => ({
2151
promisify: vitest.fn((fn: ExecFunction): PromisifiedExec => {
@@ -361,3 +391,315 @@ describe("git utils", () => {
361391
})
362392
})
363393
})
394+
395+
describe("getGitRepositoryInfo", () => {
396+
const workspaceRoot = "/test/workspace"
397+
const gitDir = path.join(workspaceRoot, ".git")
398+
const configPath = path.join(gitDir, "config")
399+
const headPath = path.join(gitDir, "HEAD")
400+
401+
beforeEach(() => {
402+
vitest.clearAllMocks()
403+
})
404+
405+
it("should return empty object when not a git repository", async () => {
406+
// Mock fs.access to throw error (directory doesn't exist)
407+
vitest.mocked(fs.promises.access).mockRejectedValueOnce(new Error("ENOENT"))
408+
409+
const result = await getGitRepositoryInfo(workspaceRoot)
410+
411+
expect(result).toEqual({})
412+
expect(fs.promises.access).toHaveBeenCalledWith(gitDir)
413+
})
414+
415+
it("should extract repository info from git config", async () => {
416+
// Clear previous mocks
417+
vitest.clearAllMocks()
418+
419+
// Create a spy to track the implementation
420+
const gitSpy = vitest.spyOn(fs.promises, "readFile")
421+
422+
// Mock successful access to .git directory
423+
vitest.mocked(fs.promises.access).mockResolvedValue(undefined)
424+
425+
// Mock git config file content
426+
const mockConfig = `
427+
[core]
428+
repositoryformatversion = 0
429+
filemode = true
430+
bare = false
431+
logallrefupdates = true
432+
ignorecase = true
433+
precomposeunicode = true
434+
[remote "origin"]
435+
url = https://github.com/RooCodeInc/Roo-Code.git
436+
fetch = +refs/heads/*:refs/remotes/origin/*
437+
[branch "main"]
438+
remote = origin
439+
merge = refs/heads/main
440+
`
441+
// Mock HEAD file content
442+
const mockHead = "ref: refs/heads/main"
443+
444+
// Setup the readFile mock to return different values based on the path
445+
gitSpy.mockImplementation((path: any, encoding: any) => {
446+
if (path === configPath) {
447+
return Promise.resolve(mockConfig)
448+
} else if (path === headPath) {
449+
return Promise.resolve(mockHead)
450+
}
451+
return Promise.reject(new Error(`Unexpected path: ${path}`))
452+
})
453+
454+
const result = await getGitRepositoryInfo(workspaceRoot)
455+
456+
expect(result).toEqual({
457+
repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git",
458+
repositoryName: "RooCodeInc/Roo-Code",
459+
defaultBranch: "main",
460+
})
461+
462+
// Verify config file was read
463+
expect(gitSpy).toHaveBeenCalledWith(configPath, "utf8")
464+
465+
// The implementation might not always read the HEAD file if it already found the branch in config
466+
// So we don't assert that it was called
467+
})
468+
469+
it("should handle missing repository URL in config", async () => {
470+
// Clear previous mocks
471+
vitest.clearAllMocks()
472+
473+
// Create a spy to track the implementation
474+
const gitSpy = vitest.spyOn(fs.promises, "readFile")
475+
476+
// Mock successful access to .git directory
477+
vitest.mocked(fs.promises.access).mockResolvedValue(undefined)
478+
479+
// Mock git config file without URL
480+
const mockConfig = `
481+
[core]
482+
repositoryformatversion = 0
483+
filemode = true
484+
bare = false
485+
`
486+
// Mock HEAD file content
487+
const mockHead = "ref: refs/heads/main"
488+
489+
// Setup the readFile mock to return different values based on the path
490+
gitSpy.mockImplementation((path: any, encoding: any) => {
491+
if (path === configPath) {
492+
return Promise.resolve(mockConfig)
493+
} else if (path === headPath) {
494+
return Promise.resolve(mockHead)
495+
}
496+
return Promise.reject(new Error(`Unexpected path: ${path}`))
497+
})
498+
499+
const result = await getGitRepositoryInfo(workspaceRoot)
500+
501+
expect(result).toEqual({
502+
defaultBranch: "main",
503+
})
504+
})
505+
506+
it("should handle errors when reading git config", async () => {
507+
// Clear previous mocks
508+
vitest.clearAllMocks()
509+
510+
// Create a spy to track the implementation
511+
const gitSpy = vitest.spyOn(fs.promises, "readFile")
512+
513+
// Mock successful access to .git directory
514+
vitest.mocked(fs.promises.access).mockResolvedValue(undefined)
515+
516+
// Setup the readFile mock to return different values based on the path
517+
gitSpy.mockImplementation((path: any, encoding: any) => {
518+
if (path === configPath) {
519+
return Promise.reject(new Error("Failed to read config"))
520+
} else if (path === headPath) {
521+
return Promise.resolve("ref: refs/heads/main")
522+
}
523+
return Promise.reject(new Error(`Unexpected path: ${path}`))
524+
})
525+
526+
const result = await getGitRepositoryInfo(workspaceRoot)
527+
528+
expect(result).toEqual({
529+
defaultBranch: "main",
530+
})
531+
})
532+
533+
it("should handle errors when reading HEAD file", async () => {
534+
// Clear previous mocks
535+
vitest.clearAllMocks()
536+
537+
// Create a spy to track the implementation
538+
const gitSpy = vitest.spyOn(fs.promises, "readFile")
539+
540+
// Mock successful access to .git directory
541+
vitest.mocked(fs.promises.access).mockResolvedValue(undefined)
542+
543+
// Setup the readFile mock to return different values based on the path
544+
gitSpy.mockImplementation((path: any, encoding: any) => {
545+
if (path === configPath) {
546+
return Promise.resolve(`
547+
[remote "origin"]
548+
url = https://github.com/RooCodeInc/Roo-Code.git
549+
`)
550+
} else if (path === headPath) {
551+
return Promise.reject(new Error("Failed to read HEAD"))
552+
}
553+
return Promise.reject(new Error(`Unexpected path: ${path}`))
554+
})
555+
556+
const result = await getGitRepositoryInfo(workspaceRoot)
557+
558+
expect(result).toEqual({
559+
repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git",
560+
repositoryName: "RooCodeInc/Roo-Code",
561+
})
562+
})
563+
})
564+
565+
describe("sanitizeGitUrl", () => {
566+
it("should sanitize HTTPS URLs with credentials", () => {
567+
const url = "https://username:[email protected]/RooCodeInc/Roo-Code.git"
568+
const sanitized = sanitizeGitUrl(url)
569+
570+
expect(sanitized).toBe("https://github.com/RooCodeInc/Roo-Code.git")
571+
})
572+
573+
it("should leave SSH URLs unchanged", () => {
574+
const url = "[email protected]:RooCodeInc/Roo-Code.git"
575+
const sanitized = sanitizeGitUrl(url)
576+
577+
expect(sanitized).toBe("[email protected]:RooCodeInc/Roo-Code.git")
578+
})
579+
580+
it("should leave SSH URLs with ssh:// prefix unchanged", () => {
581+
const url = "ssh://[email protected]/RooCodeInc/Roo-Code.git"
582+
const sanitized = sanitizeGitUrl(url)
583+
584+
expect(sanitized).toBe("ssh://[email protected]/RooCodeInc/Roo-Code.git")
585+
})
586+
587+
it("should remove tokens from other URL formats", () => {
588+
const url = "https://oauth2:[email protected]/RooCodeInc/Roo-Code.git"
589+
const sanitized = sanitizeGitUrl(url)
590+
591+
expect(sanitized).toBe("https://github.com/RooCodeInc/Roo-Code.git")
592+
})
593+
594+
it("should handle invalid URLs gracefully", () => {
595+
const url = "not-a-valid-url"
596+
const sanitized = sanitizeGitUrl(url)
597+
598+
expect(sanitized).toBe("not-a-valid-url")
599+
})
600+
})
601+
602+
describe("extractRepositoryName", () => {
603+
it("should extract repository name from HTTPS URL", () => {
604+
const url = "https://github.com/RooCodeInc/Roo-Code.git"
605+
const repoName = extractRepositoryName(url)
606+
607+
expect(repoName).toBe("RooCodeInc/Roo-Code")
608+
})
609+
610+
it("should extract repository name from HTTPS URL without .git suffix", () => {
611+
const url = "https://github.com/RooCodeInc/Roo-Code"
612+
const repoName = extractRepositoryName(url)
613+
614+
expect(repoName).toBe("RooCodeInc/Roo-Code")
615+
})
616+
617+
it("should extract repository name from SSH URL", () => {
618+
const url = "[email protected]:RooCodeInc/Roo-Code.git"
619+
const repoName = extractRepositoryName(url)
620+
621+
expect(repoName).toBe("RooCodeInc/Roo-Code")
622+
})
623+
624+
it("should extract repository name from SSH URL with ssh:// prefix", () => {
625+
const url = "ssh://[email protected]/RooCodeInc/Roo-Code.git"
626+
const repoName = extractRepositoryName(url)
627+
628+
expect(repoName).toBe("RooCodeInc/Roo-Code")
629+
})
630+
631+
it("should return empty string for unrecognized URL formats", () => {
632+
const url = "not-a-valid-git-url"
633+
const repoName = extractRepositoryName(url)
634+
635+
expect(repoName).toBe("")
636+
})
637+
638+
it("should handle URLs with credentials", () => {
639+
const url = "https://username:[email protected]/RooCodeInc/Roo-Code.git"
640+
const repoName = extractRepositoryName(url)
641+
642+
expect(repoName).toBe("RooCodeInc/Roo-Code")
643+
})
644+
})
645+
646+
describe("getWorkspaceGitInfo", () => {
647+
const workspaceRoot = "/test/workspace"
648+
649+
beforeEach(() => {
650+
vitest.clearAllMocks()
651+
})
652+
653+
it("should return empty object when no workspace folders", async () => {
654+
// Mock workspace with no folders
655+
mockWorkspaceFolders.mockReturnValue(undefined)
656+
657+
const result = await getWorkspaceGitInfo()
658+
659+
expect(result).toEqual({})
660+
})
661+
662+
it("should return git info for the first workspace folder", async () => {
663+
// Clear previous mocks
664+
vitest.clearAllMocks()
665+
666+
// Mock workspace with one folder
667+
mockWorkspaceFolders.mockReturnValue([{ uri: { fsPath: workspaceRoot }, name: "workspace", index: 0 }])
668+
669+
// Create a spy to track the implementation
670+
const gitSpy = vitest.spyOn(fs.promises, "access")
671+
const readFileSpy = vitest.spyOn(fs.promises, "readFile")
672+
673+
// Mock successful access to .git directory
674+
gitSpy.mockResolvedValue(undefined)
675+
676+
// Mock git config file content
677+
const mockConfig = `
678+
[remote "origin"]
679+
url = https://github.com/RooCodeInc/Roo-Code.git
680+
[branch "main"]
681+
remote = origin
682+
merge = refs/heads/main
683+
`
684+
685+
// Setup the readFile mock to return config content
686+
readFileSpy.mockImplementation((path: any, encoding: any) => {
687+
if (path.includes("config")) {
688+
return Promise.resolve(mockConfig)
689+
}
690+
return Promise.reject(new Error(`Unexpected path: ${path}`))
691+
})
692+
693+
const result = await getWorkspaceGitInfo()
694+
695+
expect(result).toEqual({
696+
repositoryUrl: "https://github.com/RooCodeInc/Roo-Code.git",
697+
repositoryName: "RooCodeInc/Roo-Code",
698+
defaultBranch: "main",
699+
})
700+
701+
// Verify the fs operations were called with the correct workspace path
702+
expect(gitSpy).toHaveBeenCalled()
703+
expect(readFileSpy).toHaveBeenCalled()
704+
})
705+
})

src/utils/git.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export async function getGitRepositoryInfo(workspaceRoot: string): Promise<GitRe
9393
* @param url The original git URL
9494
* @returns Sanitized URL
9595
*/
96-
function sanitizeGitUrl(url: string): string {
96+
export function sanitizeGitUrl(url: string): string {
9797
try {
9898
// Remove credentials from HTTPS URLs
9999
if (url.startsWith("https://")) {
@@ -122,7 +122,7 @@ function sanitizeGitUrl(url: string): string {
122122
* @param url The git URL
123123
* @returns Repository name or undefined
124124
*/
125-
function extractRepositoryName(url: string): string {
125+
export function extractRepositoryName(url: string): string {
126126
try {
127127
// Handle different URL formats
128128
const patterns = [

0 commit comments

Comments
 (0)