Skip to content

Commit 9f5f7dc

Browse files
authored
fix(segment): resolve a relative gitdir: pointer against its .git file's folder
1 parent 83d061e commit 9f5f7dc

3 files changed

Lines changed: 700 additions & 95 deletions

File tree

src/segments/git.go

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
11991259
func 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

Comments
 (0)