@@ -133,22 +133,22 @@ var (
133133)
134134
135135type Git struct {
136- configErr error
137- config * ini.File
136+ commonCfgErr error
138137 Working * GitStatus
139138 Staging * GitStatus
140139 commit * Commit
141140 Rebase * Rebase
142141 User * User
143- ShortHash string
142+ commonCfg * ini. File
144143 Hash string
145- BranchStatus string
146144 HEAD string
147145 UpstreamIcon string
148146 UpstreamURL string
149147 Ref string
150148 RawUpstreamURL string
151149 mainWorktree string
150+ BranchStatus string
151+ ShortHash string
152152 Scm
153153 FieldRefs
154154 stashCount int
@@ -157,8 +157,8 @@ type Git struct {
157157 PushBehind int
158158 Behind int
159159 worktreeCount int
160+ commonCfgOnce sync.Once
160161 mainWorktreeOnce sync.Once
161- configOnce sync.Once
162162 IsWorkTree bool
163163 Merge bool
164164 CherryPick bool
@@ -452,11 +452,11 @@ func (g *Git) isRepo(gitdir *runtime.FileInfo) bool {
452452
453453func (g * Git ) setUser () {
454454 // user.name/user.email are very commonly set only in the user's global
455- // gitconfig, which getGitConfig () never reads (repo-local config only).
455+ // gitconfig, which commonConfig () never reads (repo-local config only).
456456 // Trust the local read only when it has both keys; anything less falls
457457 // back to exec git, which merges every config scope the way `git
458458 // config` itself does.
459- if cfg , err := g .getGitConfig (); err == nil {
459+ if cfg , err := g .commonConfig (); err == nil {
460460 section := cfg .Section ("user" )
461461 name := section .Key ("name" ).String ()
462462 email := section .Key ("email" ).String ()
@@ -487,15 +487,16 @@ func (g *Git) setUser() {
487487func (g * Git ) isBareRepo (gitDir * runtime.FileInfo ) bool {
488488 defer log .Trace (time .Now ())
489489
490- if gitDir .IsDir {
491- g .mainSCMDir = gitDir .Path
492- } else {
490+ bareDir := gitDir .Path
491+ if ! gitDir .IsDir {
493492 content := g .fileContent (gitDir .ParentFolder , ".git" )
494493 dir := strings .TrimPrefix (content , "gitdir: " )
495- g . mainSCMDir = resolveGitPath (gitDir .ParentFolder , g .convertToLinuxPath (dir ))
494+ bareDir = resolveGitPath (gitDir .ParentFolder , g .convertToLinuxPath (dir ))
496495 }
497496
498- cfg , err := g .getGitConfig ()
497+ g .mainSCMDir = bareDir
498+
499+ cfg , err := loadGitConfig (g .env , bareDir )
499500 if err != nil {
500501 log .Error (err )
501502 return false
@@ -574,8 +575,8 @@ func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool {
574575 g .repoRootDir = g .convertToLinuxPath (g .repoRootDir )
575576 // resolve relative paths (worktree.useRelativePaths = true)
576577 g .repoRootDir = resolveGitPath (g .scmDir , g .repoRootDir )
577- g .scmDir = moduleDir [:worktreeIndex ]
578578 g .mainSCMDir = g .scmDir
579+ g .scmDir = moduleDir [:worktreeIndex ]
579580 g .IsWorkTree = true
580581 return true
581582 }
@@ -602,13 +603,9 @@ func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool {
602603 }
603604 }
604605
605- // check for separate git folder(--separate-git-dir)
606- // check if the folder contains a HEAD file
607606 if g .env .HasFilesInDir (g .mainSCMDir , "HEAD" ) {
608- gitFolder : = strings .TrimSuffix (g .scmDir , ".git" )
607+ g . repoRootDir = strings .TrimSuffix (g .scmDir , ".git" )
609608 g .scmDir = g .mainSCMDir
610- g .mainSCMDir = gitFolder
611- g .repoRootDir = gitFolder
612609 return true
613610 }
614611
@@ -651,7 +648,7 @@ func (g *Git) setPushStatus() {
651648 return
652649 }
653650
654- pushRemote := g .getPushRemote ()
651+ pushRemote := g .pushRef ()
655652 if pushRemote == "" {
656653 return
657654 }
@@ -704,64 +701,68 @@ func (g *Git) setPushStatusNative(pushRemote string) bool {
704701 return true
705702}
706703
707- func (g * Git ) getPushRemote () string {
708- upstream := g .Upstream
709- if idx := strings .Index (upstream , "/" ); idx != - 1 {
710- upstream = upstream [:idx ]
704+ // pushRef resolves the destination of a push once, so both counts below describe the
705+ // same comparison. An empty rev-list result is a genuine failure, never a retry signal.
706+ func (g * Git ) pushRef () string {
707+ if ref := g .getGitCommandOutput ("rev-parse" , "--abbrev-ref" , "@{push}" ); ref != "" {
708+ return ref
711709 }
712710
713- if upstream == "" {
714- upstream = origin
715- }
711+ return g .getPushRemote ()
712+ }
716713
714+ func (g * Git ) getPushRemote () string {
717715 branch := g .Ref
718716 if branch == "" {
719717 return ""
720718 }
721719
722- cfg , err := g .getGitConfig ()
723- if err != nil {
724- pushRemote := g .getGitCommandOutput ("config" , "--get" , "remote.pushDefault" )
725- if pushRemote == "" {
726- pushRemote = upstream
727- }
728-
729- return strings .TrimSpace (pushRemote ) + "/" + branch
720+ pushRemote := g .getGitCommandOutput ("config" , "--get" , fmt .Sprintf ("branch.%s.pushRemote" , branch ))
721+ if pushRemote == "" {
722+ pushRemote = g .getGitCommandOutput ("config" , "--get" , "remote.pushDefault" )
730723 }
731724
732- sectionName := fmt .Sprintf (`branch "%s"` , branch )
733- section := cfg .Section (sectionName )
734- pushRemote := section .Key ("pushRemote" ).String ()
735725 if pushRemote == "" {
736- pushRemote = cfg . Section ( "remote" ). Key ( "pushDefault" ). String ( )
726+ pushRemote = regex . ReplaceAllString ( "/.*" , g . Upstream , "" )
737727 }
738728
739729 if pushRemote == "" {
740- pushRemote = upstream
730+ pushRemote = origin
741731 }
742732
743- return pushRemote + "/" + branch
733+ return strings . TrimSpace ( pushRemote ) + "/" + branch
744734}
745735
746- func (g * Git ) getGitConfig () (* ini.File , error ) {
747- g .configOnce .Do (func () {
748- configData := g .fileContent (g .mainSCMDir , "config" )
749- if configData == "" {
750- log .Debug ("git config file not found" )
751- g .configErr = fmt .Errorf ("git config file not found" )
752- return
753- }
736+ func loadGitConfigFile (env runtime.Environment , dir , file string ) (* ini.File , error ) {
737+ if dir == "" {
738+ return nil , fmt .Errorf ("no git directory to read %s from" , file )
739+ }
754740
755- cfg , err := ini .Load (configData )
756- if err != nil {
757- g .configErr = err
758- return
759- }
741+ configData := strings .Trim (env .FileContent (dir + "/" + file ), " \r \n " )
742+ if configData == "" {
743+ return nil , fmt .Errorf ("%s not found" , file )
744+ }
760745
761- g .config = cfg
746+ return ini .Load (configData )
747+ }
748+
749+ func loadGitConfig (env runtime.Environment , dir string ) (* ini.File , error ) {
750+ return loadGitConfigFile (env , dir , "config" )
751+ }
752+
753+ // commonConfig reads the repository's shared config. It refuses to memoize a failure
754+ // against an unknown directory, which a cache-restored segment would otherwise poison.
755+ func (g * Git ) commonConfig () (* ini.File , error ) {
756+ commonDir := g .commonGitDir ()
757+ if commonDir == "" {
758+ return nil , fmt .Errorf ("common git directory is unknown" )
759+ }
760+
761+ g .commonCfgOnce .Do (func () {
762+ g .commonCfg , g .commonCfgErr = loadGitConfig (g .env , commonDir )
762763 })
763764
764- return g .config , g .configErr
765+ return g .commonCfg , g .commonCfgErr
765766}
766767
767768func (g * Git ) cleanUpstreamURL (url string ) string {
@@ -1261,15 +1262,19 @@ func (g *Git) WorktreeCount() int {
12611262 return g .worktreeCount
12621263 }
12631264
1264- worktreesFolder := filepath .Join (g .mainSCMDir , "worktrees" )
1265+ commonDir := g .commonGitDir ()
1266+ if commonDir == "" {
1267+ return 0
1268+ }
1269+
1270+ worktreesFolder := filepath .Join (commonDir , "worktrees" )
12651271
12661272 if ! g .env .HasFolder (worktreesFolder ) {
12671273 return 0
12681274 }
12691275
1270- worktreeFolders := g .env .LsDir (worktreesFolder )
12711276 var count int
1272- for _ , folder := range worktreeFolders {
1277+ for _ , folder := range g . env . LsDir ( worktreesFolder ) {
12731278 if folder .IsDir () {
12741279 count ++
12751280 }
@@ -1339,12 +1344,18 @@ func (g *Git) ensureMainWorktreeContext() bool {
13391344}
13401345
13411346func (g * Git ) commonGitDir () string {
1347+ // scmDir is the common git directory at every discovery exit. The worktrees cut
1348+ // below is only for partially initialized state, where scmDir is not yet set.
1349+ if g .scmDir != "" {
1350+ return filepath .ToSlash (g .scmDir )
1351+ }
1352+
13421353 mainSCMDir := filepath .ToSlash (g .mainSCMDir )
13431354 if commonDir , _ , found := strings .CutLast (mainSCMDir , "/worktrees/" ); found {
13441355 return commonDir
13451356 }
13461357
1347- return filepath . ToSlash ( g . scmDir )
1358+ return ""
13481359}
13491360
13501361// isModuleAdminDir reports whether target is a submodule administrative directory
@@ -1370,9 +1381,12 @@ func (g *Git) isModuleAdminDir(target, parent string) bool {
13701381 return true
13711382 }
13721383
1373- cfg , err := ini .Load (g .fileContent (target , "config" ))
1384+ // A missing or unreadable config is simply not a submodule git dir, not an error
1385+ // worth reporting: every --separate-git-dir target spelled with a modules component
1386+ // lands here.
1387+ cfg , err := loadGitConfig (g .env , target )
13741388 if err != nil {
1375- log .Error ( err )
1389+ log .Debug ( "no readable config in" , target , "- not a submodule git dir" )
13761390 return false
13771391 }
13781392
@@ -1432,24 +1446,23 @@ func (g *Git) getRemoteURL() string {
14321446 upstream = origin
14331447 }
14341448
1435- cfg , err := g . getGitConfig ()
1436- if err != nil {
1437- return g . getGitCommandOutput ( "remote" , "get- url" , upstream )
1449+ // Ask git first because it applies insteadOf rewriting and reads the merged configuration.
1450+ if url := g . getGitCommandOutput ( "remote" , "get-url" , upstream ); url != "" {
1451+ return url
14381452 }
14391453
1440- url := cfg .Section ("remote \" " + upstream + "\" " ).Key ("url" ).String ()
1441- if len (url ) != 0 {
1442- log .Debug ("remote url found in config:" , url )
1443- return url
1454+ cfg , err := g .commonConfig ()
1455+ if err != nil {
1456+ return ""
14441457 }
14451458
1446- return g . getGitCommandOutput ("remote" , "get-url" , upstream )
1459+ return cfg . Section ("remote \" " + upstream + " \" " ). Key ( "url" ). String ( )
14471460}
14481461
14491462func (g * Git ) Remotes () map [string ]string {
14501463 var remotes = make (map [string ]string )
14511464
1452- cfg , err := g .getGitConfig ()
1465+ cfg , err := g .commonConfig ()
14531466 if err != nil {
14541467 return remotes
14551468 }
@@ -1497,9 +1510,50 @@ func (g *Git) repoName() string {
14971510 return path .Base (g .convertToLinuxPath (g .repoRootDir ))
14981511 }
14991512
1500- if repoRoot , _ , found := strings .CutLast (g .mainSCMDir , ".git/worktrees" ); found {
1501- return path .Base (repoRoot )
1513+ commonDir := g .commonGitDir ()
1514+ if commonDir == "" {
1515+ return ""
1516+ }
1517+
1518+ if parent := filepath .Dir (commonDir ); g .gitEntryResolvesTo (parent , commonDir ) {
1519+ return path .Base (g .convertToLinuxPath (parent ))
1520+ }
1521+
1522+ for _ , file := range []string {"config.worktree" , "config" } {
1523+ cfg , err := loadGitConfigFile (g .env , commonDir , file )
1524+ if err != nil {
1525+ continue
1526+ }
1527+
1528+ worktree := cfg .Section ("core" ).Key ("worktree" ).String ()
1529+ if worktree == "" {
1530+ continue
1531+ }
1532+
1533+ return path .Base (g .convertToLinuxPath (resolveGitPath (commonDir , worktree )))
15021534 }
15031535
15041536 return ""
15051537}
1538+
1539+ func (g * Git ) gitEntryResolvesTo (parent , commonDir string ) bool {
1540+ gitEntry := parent + "/.git"
1541+ commonDir = filepath .ToSlash (filepath .Clean (commonDir ))
1542+
1543+ if g .env .HasFolder (gitEntry ) {
1544+ return filepath .ToSlash (filepath .Clean (gitEntry )) == commonDir
1545+ }
1546+
1547+ if ! g .env .HasFilesInDir (parent , ".git" ) {
1548+ return false
1549+ }
1550+
1551+ content := strings .Trim (g .env .FileContent (gitEntry ), " \r \n " )
1552+ target , found := strings .CutPrefix (content , "gitdir: " )
1553+ if ! found {
1554+ return false
1555+ }
1556+
1557+ target = g .convertToLinuxPath (target )
1558+ return filepath .ToSlash (filepath .Clean (resolveGitPath (parent , target ))) == commonDir
1559+ }
0 commit comments