Resolve the refresh's file reads against its repo root

The refresh workers read a few files at paths relative to the process
working directory: the submodule config read of .gitmodules, the files
refresh's check for conflict markers, and the submodule stash's
existence check. Git commands are pinned to the repo their instance was
created for, but these Go file reads still followed the cwd, so a
background refresh crossing a repo switch would read the new repo's
files while computing data for the old one. Join them with the worktree
root of the instance they belong to. (Most git-state file reads —
working tree state, rebase todos, bisect info — already resolve
against RepoPaths and need no change.)

This also fixes the submodule stash's existence check for nested
submodules: it stat'ed submodule.Path, which is relative to the parent
module, against the repo root — now it uses the submodule's full path,
matching the stash command right below it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-07-20 17:07:39 +02:00
co-authored by Claude Fable 5
parent ae095f276b
commit e0b8dbf48c
2 changed files with 14 additions and 4 deletions
+8 -3
View File
@@ -28,10 +28,15 @@ func NewSubmoduleCommands(gitCommon *GitCommon) *SubmoduleCommands {
}
func (self *SubmoduleCommands) GetConfigs(parentModule *models.SubmoduleConfig) ([]*models.SubmoduleConfig, error) {
gitModulesPath := ".gitmodules"
// Resolve the path against the repo this commands object was created for
// rather than the process working directory, so that a read from a
// still-running refresh keeps addressing that repo after the user
// switched to another one.
dir := self.repoPaths.WorktreePath()
if parentModule != nil {
gitModulesPath = filepath.Join(parentModule.FullPath(), gitModulesPath)
dir = filepath.Join(dir, parentModule.FullPath())
}
gitModulesPath := filepath.Join(dir, ".gitmodules")
file, err := os.Open(gitModulesPath)
if err != nil {
if os.IsNotExist(err) {
@@ -180,7 +185,7 @@ func (self *SubmoduleCommands) ConflictSideLog(path string, side string, otherSi
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
// because the intention here is to have no dirty worktree state
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreePath(), submodule.FullPath())); os.IsNotExist(err) {
self.Log.Infof("submodule path %s does not exist, returning", submodule.FullPath())
return nil
}
@@ -1,6 +1,7 @@
package helpers
import (
"path/filepath"
"strings"
"sync"
"sync/atomic"
@@ -1273,7 +1274,11 @@ func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, env re
prevConflictFileCount++
}
if file.HasInlineMergeConflicts {
hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Path)
// Join with the refresh's repo root rather than relying on the
// process working directory, which may already point at another
// repo if the user switched while this refresh was in flight.
hasConflicts, err := mergeconflicts.FileHasConflictMarkers(
filepath.Join(env.git.RepoPaths.WorktreePath(), file.Path))
if err != nil {
self.c.Log.Error(err)
} else if !hasConflicts {