@@ -347,14 +347,14 @@ func (g *Git) shouldDisplay() bool {
347347 return false
348348 }
349349
350- if g .options .Bool (FetchBareInfo , false ) {
351- g .IsBare = g .isBareRepo (gitdir )
352- }
353-
354350 if ! g .hasCommand (GITCOMMAND ) {
355351 return false
356352 }
357353
354+ if g .options .Bool (FetchBareInfo , false ) {
355+ g .IsBare = g .isBareRepo (gitdir )
356+ }
357+
358358 return g .isRepo (gitdir )
359359}
360360
@@ -402,7 +402,7 @@ func (g *Git) isBareRepo(gitDir *runtime.FileInfo) bool {
402402 } else {
403403 content := g .fileContent (gitDir .ParentFolder , ".git" )
404404 dir := strings .TrimPrefix (content , "gitdir: " )
405- g .mainSCMDir = filepath . Join (gitDir .ParentFolder , dir )
405+ g .mainSCMDir = resolveGitPath (gitDir .ParentFolder , g . convertToLinuxPath ( dir ) )
406406 }
407407
408408 cfg , err := g .getGitConfig ()
@@ -458,31 +458,33 @@ func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool {
458458 return false
459459 }
460460
461- // if we open a worktree file in a WSL shared folder, we have to convert it back
462- // to the mounted path
463- g .mainSCMDir = g . convertToLinuxPath ( matches [ "dir" ] )
461+ // Convert before resolving because filepath.IsAbs("C:/repo/.git") is false on Linux.
462+ raw := g . convertToLinuxPath ( matches [ "dir" ])
463+ g .mainSCMDir = resolveGitPath ( gitdir . ParentFolder , raw )
464464
465- // in worktrees, the path looks like this: gitdir: path/.git/worktrees/branch
466- // scmDir needs to become path/.git
467- // repoRootDir needs to become path
468- worktreeIndex := strings .LastIndex (g .mainSCMDir , "/worktrees/" )
465+ // The returned index only applies to the normalized path.
466+ adminDir := filepath .ToSlash (filepath .Clean (g .mainSCMDir ))
467+ worktreeIndex := worktreeAdminIndex (adminDir )
469468
470469 // in submodules, the path looks like this: gitdir: ../.git/modules/test-submodule
471- // we need the parent folder to detect where the real .git folder is
472- if strings .Contains (g .mainSCMDir , "/modules/" ) {
473- g .scmDir = resolveGitPath (gitdir .ParentFolder , g .mainSCMDir )
470+ // we need the parent folder to detect where the real .git folder is. Test raw rather
471+ // than the resolved path: a checkout below a folder named modules would otherwise
472+ // drag every genuine worktree in it into this branch.
473+ if strings .Contains (raw , "/modules/" ) && g .isModuleAdminDir (g .mainSCMDir , gitdir .ParentFolder ) {
474+ g .scmDir = g .mainSCMDir
474475 // this might be both a worktree and a submodule, where the path would look like
475476 // this: path/.git/modules/module/path/worktrees/location. We cannot distinguish
476477 // between worktree and a module path containing the word 'worktree,' however.
477- worktreeIndex = strings .LastIndex (g .scmDir , "/worktrees/" )
478+ moduleDir := filepath .ToSlash (filepath .Clean (g .scmDir ))
479+ worktreeIndex = worktreeAdminIndex (moduleDir )
478480 if worktreeIndex > - 1 && g .env .HasFilesInDir (g .scmDir , "gitdir" ) {
479481 gitDir := filepath .Join (g .scmDir , "gitdir" )
480482 realGitFolder := g .env .FileContent (gitDir )
481483 g .repoRootDir = strings .TrimSuffix (strings .TrimRight (realGitFolder , "\n \r " ), ".git" )
482484 g .repoRootDir = g .convertToLinuxPath (g .repoRootDir )
483485 // resolve relative paths (worktree.useRelativePaths = true)
484486 g .repoRootDir = resolveGitPath (g .scmDir , g .repoRootDir )
485- g .scmDir = g . scmDir [:worktreeIndex ]
487+ g .scmDir = moduleDir [:worktreeIndex ]
486488 g .mainSCMDir = g .scmDir
487489 g .IsWorkTree = true
488490 return true
@@ -493,22 +495,21 @@ func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool {
493495 return true
494496 }
495497
496- // convert to absolute path for worktrees only
497- if strings .HasPrefix (g .mainSCMDir , ".." ) {
498- g .mainSCMDir = resolveGitPath (gitdir .ParentFolder , g .mainSCMDir )
499- worktreeIndex = strings .LastIndex (g .mainSCMDir , "/worktrees/" )
500- }
501-
502498 if worktreeIndex > - 1 {
503- gitDir := filepath .Join (g .mainSCMDir , "gitdir" )
504- g .scmDir = g .mainSCMDir [:worktreeIndex ]
505- gitDirContent := g .env .FileContent (gitDir )
506- g .repoRootDir = strings .TrimSuffix (strings .TrimRight (gitDirContent , "\n \r " ), ".git" )
507- g .repoRootDir = g .convertToLinuxPath (g .repoRootDir )
499+ gitDirContent := g .env .FileContent (filepath .Join (g .mainSCMDir , "gitdir" ))
500+ gitDirPath := strings .TrimRight (gitDirContent , "\n \r " )
501+ root := strings .TrimSuffix (gitDirPath , ".git" )
502+ root = g .convertToLinuxPath (root )
508503 // resolve relative paths (worktree.useRelativePaths = true)
509- g .repoRootDir = resolveGitPath (g .mainSCMDir , g .repoRootDir )
510- g .IsWorkTree = true
511- return true
504+ root = resolveGitPath (g .mainSCMDir , root )
505+
506+ // A genuine worktree's metadata points back at the .git file we just read.
507+ if gitDirPath != "" && filepath .Clean (root ) == filepath .Clean (gitdir .ParentFolder ) {
508+ g .scmDir = adminDir [:worktreeIndex ]
509+ g .repoRootDir = root
510+ g .IsWorkTree = true
511+ return true
512+ }
512513 }
513514
514515 // check for separate git folder(--separate-git-dir)
@@ -1196,6 +1197,65 @@ func (g *Git) commonGitDir() string {
11961197 return filepath .ToSlash (g .scmDir )
11971198}
11981199
1200+ // isModuleAdminDir reports whether target is a submodule administrative directory
1201+ // belonging to the checkout at parent, or a linked worktree inside one.
1202+ //
1203+ // A pointer whose spelling merely contains a modules component is not enough: a
1204+ // --separate-git-dir target such as /srv/modules/project.git spells the same substring
1205+ // without being a submodule. Nor does the shape help the way it does for worktrees. A
1206+ // submodule's name is its path, so it may hold separators (.git/modules/vendor/libfoo),
1207+ // it may nest (.../modules/vendor/libfoo/modules/inner), and the folder in front of
1208+ // modules is only called .git when the superproject has no --separate-git-dir of its own
1209+ // (../../sepgit/modules/sub is a real pointer).
1210+ //
1211+ // What does hold is that git records core.worktree in a submodule's git dir, pointing
1212+ // back at the checkout, and never records it in a --separate-git-dir target. That
1213+ // back-reference is the same kind of proof the worktree branch takes from its gitdir
1214+ // metadata.
1215+ func (g * Git ) isModuleAdminDir (target , parent string ) bool {
1216+ // A linked worktree inside a module dir keeps no config of its own, so it cannot
1217+ // carry core.worktree. Its gitdir metadata identifies it instead, which is what the
1218+ // worktree case in the caller goes on to validate.
1219+ if worktreeAdminIndex (target ) > - 1 && g .env .HasFilesInDir (target , "gitdir" ) {
1220+ return true
1221+ }
1222+
1223+ cfg , err := ini .Load (g .fileContent (target , "config" ))
1224+ if err != nil {
1225+ log .Error (err )
1226+ return false
1227+ }
1228+
1229+ worktree := cfg .Section ("core" ).Key ("worktree" ).String ()
1230+ if worktree == "" {
1231+ log .Debug ("no core.worktree in" , target , "- not a submodule git dir" )
1232+ return false
1233+ }
1234+
1235+ // core.worktree is relative to the git dir holding it, and may need the same WSL
1236+ // conversion as the pointer itself.
1237+ root := resolveGitPath (target , g .convertToLinuxPath (worktree ))
1238+
1239+ return filepath .Clean (root ) == filepath .Clean (parent )
1240+ }
1241+
1242+ func worktreeAdminIndex (dir string ) int {
1243+ const segment = "/worktrees/"
1244+
1245+ normalised := filepath .ToSlash (filepath .Clean (dir ))
1246+ index := strings .LastIndex (normalised , segment )
1247+ if index < 0 {
1248+ return - 1
1249+ }
1250+
1251+ name := normalised [index + len (segment ):]
1252+ if name == "" || strings .Contains (name , "/" ) {
1253+ return - 1
1254+ }
1255+
1256+ return index
1257+ }
1258+
11991259func parseMainWorktree (output string ) (string , bool ) {
12001260 // Git guarantees the main worktree is the first record.
12011261 record , _ , found := strings .Cut (output , "\x00 \x00 " )
0 commit comments