Skip to content

Commit 384774c

Browse files
committed
fix(git): repair the mainSCMDir/scmDir directory-role invariant
Keep mainSCMDir checkout-specific and scmDir repository-wide across worktrees, submodules, bare repositories, and separate git directories. Read config, remotes, worktree counts, repository names, and native status from the correct directory, with regression coverage for the supported layouts. Fixes #7798
1 parent b093656 commit 384774c

3 files changed

Lines changed: 744 additions & 215 deletions

File tree

src/segments/git.go

Lines changed: 126 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -116,30 +116,30 @@ type Rebase struct {
116116
}
117117

118118
type Git struct {
119-
configErr error
120-
config *ini.File
119+
commonCfgErr error
121120
Working *GitStatus
122121
Staging *GitStatus
123122
commit *Commit
124123
Rebase *Rebase
125124
User *User
126-
ShortHash string
125+
commonCfg *ini.File
127126
Hash string
128-
BranchStatus string
129127
HEAD string
130128
UpstreamIcon string
131129
UpstreamURL string
132130
Ref string
133131
RawUpstreamURL string
134132
mainWorktree string
133+
BranchStatus string
134+
ShortHash string
135135
Scm
136-
stashCount int
137136
Ahead int
138-
PushAhead int
139137
PushBehind int
140138
Behind int
141139
worktreeCount int
142-
configOnce sync.Once
140+
PushAhead int
141+
stashCount int
142+
commonCfgOnce sync.Once
143143
mainWorktreeOnce sync.Once
144144
IsWorkTree bool
145145
Merge bool
@@ -397,15 +397,16 @@ func (g *Git) setUser() {
397397
func (g *Git) isBareRepo(gitDir *runtime.FileInfo) bool {
398398
defer log.Trace(time.Now())
399399

400-
if gitDir.IsDir {
401-
g.mainSCMDir = gitDir.Path
402-
} else {
400+
bareDir := gitDir.Path
401+
if !gitDir.IsDir {
403402
content := g.fileContent(gitDir.ParentFolder, ".git")
404403
dir := strings.TrimPrefix(content, "gitdir: ")
405-
g.mainSCMDir = resolveGitPath(gitDir.ParentFolder, g.convertToLinuxPath(dir))
404+
bareDir = resolveGitPath(gitDir.ParentFolder, g.convertToLinuxPath(dir))
406405
}
407406

408-
cfg, err := g.getGitConfig()
407+
g.mainSCMDir = bareDir
408+
409+
cfg, err := loadGitConfig(g.env, bareDir)
409410
if err != nil {
410411
log.Error(err)
411412
return false
@@ -484,8 +485,8 @@ func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool {
484485
g.repoRootDir = g.convertToLinuxPath(g.repoRootDir)
485486
// resolve relative paths (worktree.useRelativePaths = true)
486487
g.repoRootDir = resolveGitPath(g.scmDir, g.repoRootDir)
487-
g.scmDir = moduleDir[:worktreeIndex]
488488
g.mainSCMDir = g.scmDir
489+
g.scmDir = moduleDir[:worktreeIndex]
489490
g.IsWorkTree = true
490491
return true
491492
}
@@ -512,13 +513,9 @@ func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool {
512513
}
513514
}
514515

515-
// check for separate git folder(--separate-git-dir)
516-
// check if the folder contains a HEAD file
517516
if g.env.HasFilesInDir(g.mainSCMDir, "HEAD") {
518-
gitFolder := strings.TrimSuffix(g.scmDir, ".git")
517+
g.repoRootDir = strings.TrimSuffix(g.scmDir, ".git")
519518
g.scmDir = g.mainSCMDir
520-
g.mainSCMDir = gitFolder
521-
g.repoRootDir = gitFolder
522519
return true
523520
}
524521

@@ -561,7 +558,7 @@ func (g *Git) setPushStatus() {
561558
return
562559
}
563560

564-
pushRemote := g.getPushRemote()
561+
pushRemote := g.pushRef()
565562
if pushRemote == "" {
566563
return
567564
}
@@ -583,64 +580,68 @@ func (g *Git) setPushStatus() {
583580
wg.Wait()
584581
}
585582

586-
func (g *Git) getPushRemote() string {
587-
upstream := g.Upstream
588-
if idx := strings.Index(upstream, "/"); idx != -1 {
589-
upstream = upstream[:idx]
583+
// pushRef resolves the destination of a push once, so both counts below describe the
584+
// same comparison. An empty rev-list result is a genuine failure, never a retry signal.
585+
func (g *Git) pushRef() string {
586+
if ref := g.getGitCommandOutput("rev-parse", "--abbrev-ref", "@{push}"); ref != "" {
587+
return ref
590588
}
591589

592-
if upstream == "" {
593-
upstream = origin
594-
}
590+
return g.getPushRemote()
591+
}
595592

593+
func (g *Git) getPushRemote() string {
596594
branch := g.Ref
597595
if branch == "" {
598596
return ""
599597
}
600598

601-
cfg, err := g.getGitConfig()
602-
if err != nil {
603-
pushRemote := g.getGitCommandOutput("config", "--get", "remote.pushDefault")
604-
if pushRemote == "" {
605-
pushRemote = upstream
606-
}
607-
608-
return strings.TrimSpace(pushRemote) + "/" + branch
599+
pushRemote := g.getGitCommandOutput("config", "--get", fmt.Sprintf("branch.%s.pushRemote", branch))
600+
if pushRemote == "" {
601+
pushRemote = g.getGitCommandOutput("config", "--get", "remote.pushDefault")
609602
}
610603

611-
sectionName := fmt.Sprintf(`branch "%s"`, branch)
612-
section := cfg.Section(sectionName)
613-
pushRemote := section.Key("pushRemote").String()
614604
if pushRemote == "" {
615-
pushRemote = cfg.Section("remote").Key("pushDefault").String()
605+
pushRemote = regex.ReplaceAllString("/.*", g.Upstream, "")
616606
}
617607

618608
if pushRemote == "" {
619-
pushRemote = upstream
609+
pushRemote = origin
620610
}
621611

622-
return pushRemote + "/" + branch
612+
return strings.TrimSpace(pushRemote) + "/" + branch
623613
}
624614

625-
func (g *Git) getGitConfig() (*ini.File, error) {
626-
g.configOnce.Do(func() {
627-
configData := g.fileContent(g.mainSCMDir, "config")
628-
if configData == "" {
629-
log.Debug("git config file not found")
630-
g.configErr = fmt.Errorf("git config file not found")
631-
return
632-
}
615+
func loadGitConfigFile(env runtime.Environment, dir, file string) (*ini.File, error) {
616+
if dir == "" {
617+
return nil, fmt.Errorf("no git directory to read %s from", file)
618+
}
633619

634-
cfg, err := ini.Load(configData)
635-
if err != nil {
636-
g.configErr = err
637-
return
638-
}
620+
configData := strings.Trim(env.FileContent(dir+"/"+file), " \r\n")
621+
if configData == "" {
622+
return nil, fmt.Errorf("%s not found", file)
623+
}
639624

640-
g.config = cfg
625+
return ini.Load(configData)
626+
}
627+
628+
func loadGitConfig(env runtime.Environment, dir string) (*ini.File, error) {
629+
return loadGitConfigFile(env, dir, "config")
630+
}
631+
632+
// commonConfig reads the repository's shared config. It refuses to memoize a failure
633+
// against an unknown directory, which a cache-restored segment would otherwise poison.
634+
func (g *Git) commonConfig() (*ini.File, error) {
635+
commonDir := g.commonGitDir()
636+
if commonDir == "" {
637+
return nil, fmt.Errorf("common git directory is unknown")
638+
}
639+
640+
g.commonCfgOnce.Do(func() {
641+
g.commonCfg, g.commonCfgErr = loadGitConfig(g.env, commonDir)
641642
})
642643

643-
return g.config, g.configErr
644+
return g.commonCfg, g.commonCfgErr
644645
}
645646

646647
func (g *Git) cleanUpstreamURL(url string) string {
@@ -1111,15 +1112,19 @@ func (g *Git) WorktreeCount() int {
11111112
return g.worktreeCount
11121113
}
11131114

1114-
worktreesFolder := filepath.Join(g.mainSCMDir, "worktrees")
1115+
commonDir := g.commonGitDir()
1116+
if commonDir == "" {
1117+
return 0
1118+
}
1119+
1120+
worktreesFolder := filepath.Join(commonDir, "worktrees")
11151121

11161122
if !g.env.HasFolder(worktreesFolder) {
11171123
return 0
11181124
}
11191125

1120-
worktreeFolders := g.env.LsDir(worktreesFolder)
11211126
var count int
1122-
for _, folder := range worktreeFolders {
1127+
for _, folder := range g.env.LsDir(worktreesFolder) {
11231128
if folder.IsDir() {
11241129
count++
11251130
}
@@ -1189,12 +1194,18 @@ func (g *Git) ensureMainWorktreeContext() bool {
11891194
}
11901195

11911196
func (g *Git) commonGitDir() string {
1197+
// scmDir is the common git directory at every discovery exit. The worktrees cut
1198+
// below is only for partially initialized state, where scmDir is not yet set.
1199+
if g.scmDir != "" {
1200+
return filepath.ToSlash(g.scmDir)
1201+
}
1202+
11921203
mainSCMDir := filepath.ToSlash(g.mainSCMDir)
11931204
if worktreeIndex := strings.LastIndex(mainSCMDir, "/worktrees/"); worktreeIndex > -1 {
11941205
return mainSCMDir[:worktreeIndex]
11951206
}
11961207

1197-
return filepath.ToSlash(g.scmDir)
1208+
return ""
11981209
}
11991210

12001211
// isModuleAdminDir reports whether target is a submodule administrative directory
@@ -1220,9 +1231,12 @@ func (g *Git) isModuleAdminDir(target, parent string) bool {
12201231
return true
12211232
}
12221233

1223-
cfg, err := ini.Load(g.fileContent(target, "config"))
1234+
// A missing or unreadable config is simply not a submodule git dir, not an error
1235+
// worth reporting: every --separate-git-dir target spelled with a modules component
1236+
// lands here.
1237+
cfg, err := loadGitConfig(g.env, target)
12241238
if err != nil {
1225-
log.Error(err)
1239+
log.Debug("no readable config in", target, "- not a submodule git dir")
12261240
return false
12271241
}
12281242

@@ -1282,24 +1296,23 @@ func (g *Git) getRemoteURL() string {
12821296
upstream = origin
12831297
}
12841298

1285-
cfg, err := g.getGitConfig()
1286-
if err != nil {
1287-
return g.getGitCommandOutput("remote", "get-url", upstream)
1299+
// Ask git first because it applies insteadOf rewriting and reads the merged configuration.
1300+
if url := g.getGitCommandOutput("remote", "get-url", upstream); url != "" {
1301+
return url
12881302
}
12891303

1290-
url := cfg.Section("remote \"" + upstream + "\"").Key("url").String()
1291-
if len(url) != 0 {
1292-
log.Debug("remote url found in config:", url)
1293-
return url
1304+
cfg, err := g.commonConfig()
1305+
if err != nil {
1306+
return ""
12941307
}
12951308

1296-
return g.getGitCommandOutput("remote", "get-url", upstream)
1309+
return cfg.Section("remote \"" + upstream + "\"").Key("url").String()
12971310
}
12981311

12991312
func (g *Git) Remotes() map[string]string {
13001313
var remotes = make(map[string]string)
13011314

1302-
cfg, err := g.getGitConfig()
1315+
cfg, err := g.commonConfig()
13031316
if err != nil {
13041317
return remotes
13051318
}
@@ -1347,10 +1360,50 @@ func (g *Git) repoName() string {
13471360
return path.Base(g.convertToLinuxPath(g.repoRootDir))
13481361
}
13491362

1350-
ind := strings.LastIndex(g.mainSCMDir, ".git/worktrees")
1351-
if ind > -1 {
1352-
return path.Base(g.mainSCMDir[:ind])
1363+
commonDir := g.commonGitDir()
1364+
if commonDir == "" {
1365+
return ""
1366+
}
1367+
1368+
if parent := filepath.Dir(commonDir); g.gitEntryResolvesTo(parent, commonDir) {
1369+
return path.Base(g.convertToLinuxPath(parent))
1370+
}
1371+
1372+
for _, file := range []string{"config.worktree", "config"} {
1373+
cfg, err := loadGitConfigFile(g.env, commonDir, file)
1374+
if err != nil {
1375+
continue
1376+
}
1377+
1378+
worktree := cfg.Section("core").Key("worktree").String()
1379+
if worktree == "" {
1380+
continue
1381+
}
1382+
1383+
return path.Base(g.convertToLinuxPath(resolveGitPath(commonDir, worktree)))
13531384
}
13541385

13551386
return ""
13561387
}
1388+
1389+
func (g *Git) gitEntryResolvesTo(parent, commonDir string) bool {
1390+
gitEntry := parent + "/.git"
1391+
commonDir = filepath.ToSlash(filepath.Clean(commonDir))
1392+
1393+
if g.env.HasFolder(gitEntry) {
1394+
return filepath.ToSlash(filepath.Clean(gitEntry)) == commonDir
1395+
}
1396+
1397+
if !g.env.HasFilesInDir(parent, ".git") {
1398+
return false
1399+
}
1400+
1401+
content := strings.Trim(g.env.FileContent(gitEntry), " \r\n")
1402+
target, found := strings.CutPrefix(content, "gitdir: ")
1403+
if !found {
1404+
return false
1405+
}
1406+
1407+
target = g.convertToLinuxPath(target)
1408+
return filepath.ToSlash(filepath.Clean(resolveGitPath(parent, target))) == commonDir
1409+
}

0 commit comments

Comments
 (0)