@@ -7,7 +7,7 @@ import { EventEmitter } from "events"
77
88import { simpleGit , SimpleGit } from "simple-git"
99
10- import { fileExistsAtPath } from "../../../utils/fs"
10+ import * as fsUtils from "../../../utils/fs"
1111import * as fileSearch from "../../../services/search/file-search"
1212
1313import { RepoPerTaskCheckpointService } from "../RepoPerTaskCheckpointService"
@@ -415,7 +415,7 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
415415 const nestedGitDir = path . join ( nestedRepoPath , ".git" )
416416 const headFile = path . join ( nestedGitDir , "HEAD" )
417417 await fs . writeFile ( headFile , "HEAD" )
418- expect ( await fileExistsAtPath ( nestedGitDir ) ) . toBe ( true )
418+ expect ( await fsUtils . fileExistsAtPath ( nestedGitDir ) ) . toBe ( true )
419419
420420 vitest . spyOn ( fileSearch , "executeRipgrep" ) . mockImplementation ( ( { args } ) => {
421421 const searchPattern = args [ 4 ]
@@ -483,6 +483,78 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
483483 await fs . rm ( shadowDir , { recursive : true , force : true } )
484484 await fs . rm ( workspaceDir , { recursive : true , force : true } )
485485 } )
486+
487+ it ( "allows git repositories in non-git workspace (issue #8164)" , async ( ) => {
488+ // This test addresses the specific issue where a workspace that is NOT a git repository
489+ // contains cloned git repositories as subdirectories. This should be allowed.
490+
491+ const shadowDir = path . join ( tmpDir , `${ prefix } -non-git-workspace-${ Date . now ( ) } ` )
492+ const workspaceDir = path . join ( tmpDir , `workspace-non-git-${ Date . now ( ) } ` )
493+
494+ // Create a workspace directory WITHOUT initializing it as a git repo
495+ await fs . mkdir ( workspaceDir , { recursive : true } )
496+
497+ // Create a cloned repository inside the workspace (simulating the user's scenario)
498+ const clonedRepoPath = path . join ( workspaceDir , "cloned-repository" )
499+ await fs . mkdir ( clonedRepoPath , { recursive : true } )
500+ const clonedGit = simpleGit ( clonedRepoPath )
501+ await clonedGit . init ( )
502+ await clonedGit . addConfig ( "user.name" , "Roo Code" )
503+ await clonedGit . addConfig ( "user.email" , "[email protected] " ) 504+
505+ // Add a file to the cloned repo
506+ const clonedFile = path . join ( clonedRepoPath , "cloned-file.txt" )
507+ await fs . writeFile ( clonedFile , "Content in cloned repo" )
508+ await clonedGit . add ( "." )
509+ await clonedGit . commit ( "Initial commit in cloned repo" )
510+
511+ // Create a regular file in the workspace root
512+ const workspaceFile = path . join ( workspaceDir , "workspace-file.txt" )
513+ await fs . writeFile ( workspaceFile , "Content in workspace" )
514+
515+ // Mock executeRipgrep to return the cloned repo's .git/HEAD
516+ vitest . spyOn ( fileSearch , "executeRipgrep" ) . mockImplementation ( ( { args } ) => {
517+ const searchPattern = args [ 4 ]
518+
519+ if ( searchPattern . includes ( ".git/HEAD" ) ) {
520+ // Return the HEAD file path for the cloned repository
521+ const headFilePath = path . join ( path . relative ( workspaceDir , clonedRepoPath ) , ".git" , "HEAD" )
522+ return Promise . resolve ( [
523+ {
524+ path : headFilePath ,
525+ type : "file" ,
526+ label : "HEAD" ,
527+ } ,
528+ ] )
529+ } else {
530+ return Promise . resolve ( [ ] )
531+ }
532+ } )
533+
534+ // Mock fileExistsAtPath to return false for workspace/.git (workspace is not a git repo)
535+ vitest . spyOn ( fsUtils , "fileExistsAtPath" ) . mockImplementation ( ( filePath : string ) => {
536+ if ( filePath === path . join ( workspaceDir , ".git" ) ) {
537+ return Promise . resolve ( false ) // Workspace is NOT a git repo
538+ }
539+ // For other paths, use the real implementation
540+ return fs
541+ . access ( filePath )
542+ . then ( ( ) => true )
543+ . catch ( ( ) => false )
544+ } )
545+
546+ const service = new klass ( taskId , shadowDir , workspaceDir , ( ) => { } )
547+
548+ // This should NOT throw an error because the workspace is not a git repository,
549+ // so the cloned repository is not considered "nested"
550+ await expect ( service . initShadowGit ( ) ) . resolves . not . toThrow ( )
551+ expect ( service . isInitialized ) . toBe ( true )
552+
553+ // Clean up
554+ vitest . restoreAllMocks ( )
555+ await fs . rm ( shadowDir , { recursive : true , force : true } )
556+ await fs . rm ( workspaceDir , { recursive : true , force : true } )
557+ } )
486558 } )
487559
488560 describe ( `${ klass . name } #events` , ( ) => {
0 commit comments