From e3ecb77939e191281ef3a70509be1183acc2b41e Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 10 Jul 2026 08:57:26 +0200 Subject: [PATCH] Recognize index.lock contention in worktrees and submodules The retry check matched the literal ".git/index.lock", which only ever appears for the main worktree. A linked worktree's lock is at .git/worktrees//index.lock and a submodule's is under its own git dir, so contention there was never retried. Match the bare "index.lock" fragment instead, which covers all of them. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/commands/git_cmd_obj_runner.go | 7 ++++++- pkg/commands/git_cmd_obj_runner_test.go | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pkg/commands/git_cmd_obj_runner.go b/pkg/commands/git_cmd_obj_runner.go index f22a0f7aa..c4d2ec049 100644 --- a/pkg/commands/git_cmd_obj_runner.go +++ b/pkg/commands/git_cmd_obj_runner.go @@ -24,12 +24,17 @@ type gitCmdObjRunner struct { // lock-related condition that may succeed on retry. The lock message can reach // us either in the command's captured output or, for streamed commands whose // output we don't capture, only in the returned error, so we check both. +// +// We match the bare "index.lock" fragment rather than a fuller path or message +// so we catch the lock wherever git puts it: the main .git dir, a linked +// worktree's git dir (.git/worktrees//index.lock), or a submodule's git +// dir. func isRetryableError(output string, err error) bool { text := output if err != nil { text += "\n" + err.Error() } - return strings.Contains(text, ".git/index.lock") || + return strings.Contains(text, "index.lock") || strings.Contains(text, "cannot lock ref") } diff --git a/pkg/commands/git_cmd_obj_runner_test.go b/pkg/commands/git_cmd_obj_runner_test.go index 903c6a90b..2cde503f5 100644 --- a/pkg/commands/git_cmd_obj_runner_test.go +++ b/pkg/commands/git_cmd_obj_runner_test.go @@ -105,3 +105,18 @@ func TestRunWithOutputRetriesWhenLockErrorIsOnlyInError(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 2, inner.calls) } + +func TestRunWithOutputRetriesLockErrorInLinkedWorktree(t *testing.T) { + // In a linked worktree the lock lives at .git/worktrees//index.lock + // rather than .git/index.lock, so only matching the bare "index.lock" + // fragment lets the retry fire there too. + inner := &scriptedRunner{results: []runnerResult{ + {output: "", err: errors.New("fatal: Unable to create '/repo/.git/worktrees/feature/index.lock': File exists.")}, + {output: "", err: nil}, + }} + + _, err := newTestRunner(inner).RunWithOutput(dummyCmdObj()) + + assert.NoError(t, err) + assert.Equal(t, 2, inner.calls) +}