11import { 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"
416import { truncateOutput } from "../../integrations/misc/extract-text"
517
618type 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
2050vitest . 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+ } )
0 commit comments