@@ -7,6 +7,7 @@ import { EventEmitter } from "events"
77
88import { simpleGit , SimpleGit } from "simple-git"
99
10+ import { fileExistsAtPath } from "../../../utils/fs"
1011import { RepoPerTaskCheckpointService } from "../RepoPerTaskCheckpointService"
1112import { RepoPerWorkspaceCheckpointService } from "../RepoPerWorkspaceCheckpointService"
1213
@@ -16,7 +17,7 @@ jest.mock("globby", () => ({
1617
1718const tmpDir = path . join ( os . tmpdir ( ) , "CheckpointService" )
1819
19- const initRepo = async ( {
20+ const initWorkspaceRepo = async ( {
2021 workspaceDir,
2122 userName = "Roo Code" ,
2223@@ -64,7 +65,7 @@ describe.each([
6465
6566 const shadowDir = path . join ( tmpDir , `${ prefix } -${ Date . now ( ) } ` )
6667 const workspaceDir = path . join ( tmpDir , `workspace-${ Date . now ( ) } ` )
67- const repo = await initRepo ( { workspaceDir } )
68+ const repo = await initWorkspaceRepo ( { workspaceDir } )
6869
6970 workspaceGit = repo . git
7071 testFile = repo . testFile
@@ -298,6 +299,52 @@ describe.each([
298299 await expect ( fs . readFile ( testFile , "utf-8" ) ) . rejects . toThrow ( )
299300 await expect ( fs . readFile ( untrackedFile , "utf-8" ) ) . rejects . toThrow ( )
300301 } )
302+
303+ it ( "does not create a checkpoint for ignored files" , async ( ) => {
304+ // Create a file that matches an ignored pattern (e.g., .log file).
305+ const ignoredFile = path . join ( service . workspaceDir , "ignored.log" )
306+ await fs . writeFile ( ignoredFile , "Initial ignored content" )
307+
308+ const commit = await service . saveCheckpoint ( "Ignored file checkpoint" )
309+ expect ( commit ?. commit ) . toBeFalsy ( )
310+
311+ await fs . writeFile ( ignoredFile , "Modified ignored content" )
312+
313+ const commit2 = await service . saveCheckpoint ( "Ignored file modified checkpoint" )
314+ expect ( commit2 ?. commit ) . toBeFalsy ( )
315+
316+ expect ( await fs . readFile ( ignoredFile , "utf-8" ) ) . toBe ( "Modified ignored content" )
317+ } )
318+
319+ it ( "does not create a checkpoint for LFS files" , async ( ) => {
320+ // Create a .gitattributes file with LFS patterns.
321+ const gitattributesPath = path . join ( service . workspaceDir , ".gitattributes" )
322+ await fs . writeFile ( gitattributesPath , "*.lfs filter=lfs diff=lfs merge=lfs -text" )
323+
324+ // Re-initialize the service to trigger a write to .git/info/exclude.
325+ service = new klass ( service . taskId , service . checkpointsDir , service . workspaceDir , ( ) => { } )
326+ const excludesPath = path . join ( service . checkpointsDir , ".git" , "info" , "exclude" )
327+ expect ( ( await fs . readFile ( excludesPath , "utf-8" ) ) . split ( "\n" ) ) . not . toContain ( "*.lfs" )
328+ await service . initShadowGit ( )
329+ expect ( ( await fs . readFile ( excludesPath , "utf-8" ) ) . split ( "\n" ) ) . toContain ( "*.lfs" )
330+
331+ const commit0 = await service . saveCheckpoint ( "Add gitattributes" )
332+ expect ( commit0 ?. commit ) . toBeTruthy ( )
333+
334+ // Create a file that matches an LFS pattern.
335+ const lfsFile = path . join ( service . workspaceDir , "foo.lfs" )
336+ await fs . writeFile ( lfsFile , "Binary file content simulation" )
337+
338+ const commit = await service . saveCheckpoint ( "LFS file checkpoint" )
339+ expect ( commit ?. commit ) . toBeFalsy ( )
340+
341+ await fs . writeFile ( lfsFile , "Modified binary content" )
342+
343+ const commit2 = await service . saveCheckpoint ( "LFS file modified checkpoint" )
344+ expect ( commit2 ?. commit ) . toBeFalsy ( )
345+
346+ expect ( await fs . readFile ( lfsFile , "utf-8" ) ) . toBe ( "Modified binary content" )
347+ } )
301348 } )
302349
303350 describe ( `${ klass . name } #create` , ( ) => {
@@ -337,6 +384,106 @@ describe.each([
337384 } )
338385 } )
339386
387+ describe ( `${ klass . name } #renameNestedGitRepos` , ( ) => {
388+ it ( "handles nested git repositories during initialization" , async ( ) => {
389+ // Create a new temporary workspace and service for this test.
390+ const shadowDir = path . join ( tmpDir , `${ prefix } -nested-git-${ Date . now ( ) } ` )
391+ const workspaceDir = path . join ( tmpDir , `workspace-nested-git-${ Date . now ( ) } ` )
392+
393+ // Create a primary workspace repo.
394+ await fs . mkdir ( workspaceDir , { recursive : true } )
395+ const mainGit = simpleGit ( workspaceDir )
396+ await mainGit . init ( )
397+ await mainGit . addConfig ( "user.name" , "Roo Code" )
398+ await mainGit . addConfig ( "user.email" , "[email protected] " ) 399+
400+ // Create a nested repo inside the workspace.
401+ const nestedRepoPath = path . join ( workspaceDir , "nested-project" )
402+ await fs . mkdir ( nestedRepoPath , { recursive : true } )
403+ const nestedGit = simpleGit ( nestedRepoPath )
404+ await nestedGit . init ( )
405+ await nestedGit . addConfig ( "user.name" , "Roo Code" )
406+ await nestedGit . addConfig ( "user.email" , "[email protected] " ) 407+
408+ // Add a file to the nested repo.
409+ const nestedFile = path . join ( nestedRepoPath , "nested-file.txt" )
410+ await fs . writeFile ( nestedFile , "Content in nested repo" )
411+ await nestedGit . add ( "." )
412+ await nestedGit . commit ( "Initial commit in nested repo" )
413+
414+ // Create a test file in the main workspace.
415+ const mainFile = path . join ( workspaceDir , "main-file.txt" )
416+ await fs . writeFile ( mainFile , "Content in main repo" )
417+ await mainGit . add ( "." )
418+ await mainGit . commit ( "Initial commit in main repo" )
419+
420+ // Confirm nested git directory exists before initialization.
421+ const nestedGitDir = path . join ( nestedRepoPath , ".git" )
422+ const nestedGitDisabledDir = `${ nestedGitDir } _disabled`
423+ expect ( await fileExistsAtPath ( nestedGitDir ) ) . toBe ( true )
424+ expect ( await fileExistsAtPath ( nestedGitDisabledDir ) ) . toBe ( false )
425+
426+ // Configure globby mock to return our nested git repository.
427+ const relativeGitPath = path . relative ( workspaceDir , nestedGitDir )
428+
429+ jest . mocked ( require ( "globby" ) . globby ) . mockImplementation ( ( pattern : string | string [ ] ) => {
430+ if ( pattern === "**/.git" ) {
431+ return Promise . resolve ( [ relativeGitPath ] )
432+ } else if ( pattern === "**/.git_disabled" ) {
433+ return Promise . resolve ( [ `${ relativeGitPath } _disabled` ] )
434+ }
435+
436+ return Promise . resolve ( [ ] )
437+ } )
438+
439+ // Create a spy on fs.rename to track when it's called.
440+ const renameSpy = jest . spyOn ( fs , "rename" )
441+
442+ // Initialize the shadow git service.
443+ const service = new klass ( taskId , shadowDir , workspaceDir , ( ) => { } )
444+
445+ // Override renameNestedGitRepos to track calls.
446+ const originalRenameMethod = service [ "renameNestedGitRepos" ] . bind ( service )
447+ let disableCall = false
448+ let enableCall = false
449+
450+ service [ "renameNestedGitRepos" ] = async ( disable : boolean ) => {
451+ if ( disable ) {
452+ disableCall = true
453+ } else {
454+ enableCall = true
455+ }
456+
457+ return originalRenameMethod ( disable )
458+ }
459+
460+ // Initialize the shadow git repo.
461+ await service . initShadowGit ( )
462+
463+ // Verify both disable and enable were called.
464+ expect ( disableCall ) . toBe ( true )
465+ expect ( enableCall ) . toBe ( true )
466+
467+ // Verify rename was called with correct paths.
468+ const renameCallsArgs = renameSpy . mock . calls . map ( ( call ) => call [ 0 ] + " -> " + call [ 1 ] )
469+ expect (
470+ renameCallsArgs . some ( ( args ) => args . includes ( nestedGitDir ) && args . includes ( nestedGitDisabledDir ) ) ,
471+ ) . toBe ( true )
472+ expect (
473+ renameCallsArgs . some ( ( args ) => args . includes ( nestedGitDisabledDir ) && args . includes ( nestedGitDir ) ) ,
474+ ) . toBe ( true )
475+
476+ // Verify the nested git directory is back to normal after initialization.
477+ expect ( await fileExistsAtPath ( nestedGitDir ) ) . toBe ( true )
478+ expect ( await fileExistsAtPath ( nestedGitDisabledDir ) ) . toBe ( false )
479+
480+ // Clean up.
481+ renameSpy . mockRestore ( )
482+ await fs . rm ( shadowDir , { recursive : true , force : true } )
483+ await fs . rm ( workspaceDir , { recursive : true , force : true } )
484+ } )
485+ } )
486+
340487 describe ( `${ klass . name } #events` , ( ) => {
341488 it ( "emits initialize event when service is created" , async ( ) => {
342489 const shadowDir = path . join ( tmpDir , `${ prefix } 3-${ Date . now ( ) } ` )
0 commit comments