@@ -527,86 +527,184 @@ private static bool PathsEqual(string left, string right)
527527 return string . Equals ( l , r , StringComparison . OrdinalIgnoreCase ) ;
528528 }
529529
530- #region EnsureGitIgnoreEntry Tests
530+ #region EnsureGitExcludeEntry Tests
531531
532532 [ Fact ]
533- public void EnsureGitIgnoreEntry_CreatesGitIgnoreIfMissing ( )
533+ public void EnsureGitExcludeEntry_CreatesExcludeIfMissing ( )
534534 {
535535 var tmpDir = Directory . CreateTempSubdirectory ( "polypilot-test-" ) . FullName ;
536536 try
537537 {
538- var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitIgnoreEntry" ,
538+ // Create a .git/info directory to simulate a real repo
539+ var infoDir = Path . Combine ( tmpDir , ".git" , "info" ) ;
540+ Directory . CreateDirectory ( infoDir ) ;
541+
542+ var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitExcludeEntry" ,
539543 System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Static ) ! ;
540544 method . Invoke ( null , [ tmpDir , ".polypilot/" ] ) ;
541545
542- var gitignorePath = Path . Combine ( tmpDir , ".gitignore " ) ;
543- Assert . True ( File . Exists ( gitignorePath ) ) ;
544- var content = File . ReadAllText ( gitignorePath ) ;
546+ var excludePath = Path . Combine ( infoDir , "exclude " ) ;
547+ Assert . True ( File . Exists ( excludePath ) ) ;
548+ var content = File . ReadAllText ( excludePath ) ;
545549 Assert . Contains ( ".polypilot/" , content ) ;
546550 }
547551 finally { ForceDeleteDirectory ( tmpDir ) ; }
548552 }
549553
550554 [ Fact ]
551- public void EnsureGitIgnoreEntry_AppendsIfNotPresent ( )
555+ public void EnsureGitExcludeEntry_AppendsIfNotPresent ( )
552556 {
553557 var tmpDir = Directory . CreateTempSubdirectory ( "polypilot-test-" ) . FullName ;
554558 try
555559 {
556- var gitignorePath = Path . Combine ( tmpDir , ".gitignore" ) ;
557- File . WriteAllText ( gitignorePath , "*.user\n bin/\n " ) ;
560+ var infoDir = Path . Combine ( tmpDir , ".git" , "info" ) ;
561+ Directory . CreateDirectory ( infoDir ) ;
562+ var excludePath = Path . Combine ( infoDir , "exclude" ) ;
563+ File . WriteAllText ( excludePath , "*.user\n bin/\n " ) ;
558564
559- var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitIgnoreEntry " ,
565+ var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitExcludeEntry " ,
560566 System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Static ) ! ;
561567 method . Invoke ( null , [ tmpDir , ".polypilot/" ] ) ;
562568
563- var content = File . ReadAllText ( gitignorePath ) ;
569+ var content = File . ReadAllText ( excludePath ) ;
564570 Assert . Contains ( ".polypilot/" , content ) ;
565571 Assert . Contains ( "*.user" , content ) ; // existing content preserved
566572 }
567573 finally { ForceDeleteDirectory ( tmpDir ) ; }
568574 }
569575
570576 [ Fact ]
571- public void EnsureGitIgnoreEntry_IdempotentIfAlreadyPresent ( )
577+ public void EnsureGitExcludeEntry_IdempotentIfAlreadyPresent ( )
572578 {
573579 var tmpDir = Directory . CreateTempSubdirectory ( "polypilot-test-" ) . FullName ;
574580 try
575581 {
576- var gitignorePath = Path . Combine ( tmpDir , ".gitignore" ) ;
577- File . WriteAllText ( gitignorePath , ".polypilot/\n " ) ;
582+ var infoDir = Path . Combine ( tmpDir , ".git" , "info" ) ;
583+ Directory . CreateDirectory ( infoDir ) ;
584+ var excludePath = Path . Combine ( infoDir , "exclude" ) ;
585+ File . WriteAllText ( excludePath , ".polypilot/\n " ) ;
578586
579- var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitIgnoreEntry " ,
587+ var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitExcludeEntry " ,
580588 System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Static ) ! ;
581589 method . Invoke ( null , [ tmpDir , ".polypilot/" ] ) ;
582590 method . Invoke ( null , [ tmpDir , ".polypilot/" ] ) ; // call twice
583591
584- var lines = File . ReadAllLines ( gitignorePath ) ;
592+ var lines = File . ReadAllLines ( excludePath ) ;
585593 Assert . Equal ( 1 , lines . Count ( l => l . Trim ( ) == ".polypilot/" ) ) ; // only one entry
586594 }
587595 finally { ForceDeleteDirectory ( tmpDir ) ; }
588596 }
589597
590598 [ Fact ]
591- public void EnsureGitIgnoreEntry_MatchesWithoutTrailingSlash ( )
599+ public void EnsureGitExcludeEntry_MatchesWithoutTrailingSlash ( )
592600 {
593601 var tmpDir = Directory . CreateTempSubdirectory ( "polypilot-test-" ) . FullName ;
594602 try
595603 {
596- var gitignorePath = Path . Combine ( tmpDir , ".gitignore" ) ;
597- File . WriteAllText ( gitignorePath , ".polypilot\n " ) ; // no trailing slash variant
604+ var infoDir = Path . Combine ( tmpDir , ".git" , "info" ) ;
605+ Directory . CreateDirectory ( infoDir ) ;
606+ var excludePath = Path . Combine ( infoDir , "exclude" ) ;
607+ File . WriteAllText ( excludePath , ".polypilot\n " ) ; // no trailing slash variant
598608
599- var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitIgnoreEntry " ,
609+ var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitExcludeEntry " ,
600610 System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Static ) ! ;
601611 method . Invoke ( null , [ tmpDir , ".polypilot/" ] ) ;
602612
603- var content = File . ReadAllText ( gitignorePath ) ;
613+ var content = File . ReadAllText ( excludePath ) ;
604614 // Should NOT add a duplicate (already covered by ".polypilot" line)
605615 Assert . DoesNotContain ( ".polypilot/" , content ) ;
606616 }
607617 finally { ForceDeleteDirectory ( tmpDir ) ; }
608618 }
609619
620+ [ Fact ]
621+ public void EnsureGitExcludeEntry_HandlesWorktreeGitdirPointer ( )
622+ {
623+ var tmpDir = Directory . CreateTempSubdirectory ( "polypilot-test-" ) . FullName ;
624+ try
625+ {
626+ // Simulate a worktree where .git is a file pointing to the real gitdir
627+ var realGitDir = Path . Combine ( tmpDir , "real-gitdir" ) ;
628+ Directory . CreateDirectory ( Path . Combine ( realGitDir , "info" ) ) ;
629+ File . WriteAllText ( Path . Combine ( tmpDir , ".git" ) , $ "gitdir: { realGitDir } \n ") ;
630+
631+ var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitExcludeEntry" ,
632+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Static ) ! ;
633+ method . Invoke ( null , [ tmpDir , ".polypilot/" ] ) ;
634+
635+ var excludePath = Path . Combine ( realGitDir , "info" , "exclude" ) ;
636+ Assert . True ( File . Exists ( excludePath ) ) ;
637+ var content = File . ReadAllText ( excludePath ) ;
638+ Assert . Contains ( ".polypilot/" , content ) ;
639+ }
640+ finally { ForceDeleteDirectory ( tmpDir ) ; }
641+ }
642+
643+ [ Fact ]
644+ public void EnsureGitExcludeEntry_HandlesRelativeGitdirPointer ( )
645+ {
646+ var tmpDir = Directory . CreateTempSubdirectory ( "polypilot-test-" ) . FullName ;
647+ try
648+ {
649+ // Simulate a worktree with a relative gitdir pointer (e.g., ../.git/worktrees/name)
650+ var bareGitDir = Path . Combine ( tmpDir , "bare-repo.git" ) ;
651+ var worktreeGitDir = Path . Combine ( bareGitDir , "worktrees" , "my-branch" ) ;
652+ Directory . CreateDirectory ( Path . Combine ( worktreeGitDir , "info" ) ) ;
653+
654+ var worktreeDir = Path . Combine ( tmpDir , "my-worktree" ) ;
655+ Directory . CreateDirectory ( worktreeDir ) ;
656+ // Write a relative gitdir pointer
657+ var relativePath = Path . GetRelativePath ( worktreeDir , worktreeGitDir ) ;
658+ File . WriteAllText ( Path . Combine ( worktreeDir , ".git" ) , $ "gitdir: { relativePath } \n ") ;
659+
660+ var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitExcludeEntry" ,
661+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Static ) ! ;
662+ method . Invoke ( null , [ worktreeDir , ".polypilot/" ] ) ;
663+
664+ var excludePath = Path . Combine ( worktreeGitDir , "info" , "exclude" ) ;
665+ Assert . True ( File . Exists ( excludePath ) ) ;
666+ var content = File . ReadAllText ( excludePath ) ;
667+ Assert . Contains ( ".polypilot/" , content ) ;
668+ }
669+ finally { ForceDeleteDirectory ( tmpDir ) ; }
670+ }
671+
672+ [ Fact ]
673+ public void EnsureGitExcludeEntry_NoGitDirectory_NoOp ( )
674+ {
675+ var tmpDir = Directory . CreateTempSubdirectory ( "polypilot-test-" ) . FullName ;
676+ try
677+ {
678+ // No .git file or directory — should be a no-op, not create spurious directories
679+ var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitExcludeEntry" ,
680+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Static ) ! ;
681+ method . Invoke ( null , [ tmpDir , ".polypilot/" ] ) ;
682+
683+ Assert . False ( Directory . Exists ( Path . Combine ( tmpDir , ".git" ) ) ) ;
684+ Assert . False ( File . Exists ( Path . Combine ( tmpDir , ".git" , "info" , "exclude" ) ) ) ;
685+ }
686+ finally { ForceDeleteDirectory ( tmpDir ) ; }
687+ }
688+
689+ [ Fact ]
690+ public void EnsureGitExcludeEntry_MalformedGitFile_NoOp ( )
691+ {
692+ var tmpDir = Directory . CreateTempSubdirectory ( "polypilot-test-" ) . FullName ;
693+ try
694+ {
695+ // .git is a file but doesn't contain gitdir: prefix — should be a no-op
696+ File . WriteAllText ( Path . Combine ( tmpDir , ".git" ) , "this is not a valid gitdir pointer\n " ) ;
697+
698+ var method = typeof ( RepoManager ) . GetMethod ( "EnsureGitExcludeEntry" ,
699+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Static ) ! ;
700+ method . Invoke ( null , [ tmpDir , ".polypilot/" ] ) ;
701+
702+ // Should not have created any info/exclude anywhere
703+ Assert . False ( Directory . Exists ( Path . Combine ( tmpDir , ".git" , "info" ) ) ) ;
704+ }
705+ finally { ForceDeleteDirectory ( tmpDir ) ; }
706+ }
707+
610708 #endregion
611709
612710 #region Nested Worktree Path Traversal Tests
0 commit comments