From c1cd500fa776c54f61b4cf6dddf6de0d90c7aa1a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 10 Jul 2026 08:56:56 +0200 Subject: [PATCH] Retry lock errors reported only through the command's error Have isRetryableError also inspect the returned error, not just the captured output. Streamed commands (amend, commit, and other operations run through the gpg helper) don't capture output, so their index.lock failures were slipping past the retry loop and surfacing to the user as a hard "Git command failed". Now they retry like every other command. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/commands/git_cmd_obj_runner.go | 18 ++++++++++++------ pkg/commands/git_cmd_obj_runner_test.go | 4 ---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/commands/git_cmd_obj_runner.go b/pkg/commands/git_cmd_obj_runner.go index a72565b5c..f22a0f7aa 100644 --- a/pkg/commands/git_cmd_obj_runner.go +++ b/pkg/commands/git_cmd_obj_runner.go @@ -20,11 +20,17 @@ type gitCmdObjRunner struct { innerRunner oscommands.ICmdObjRunner } -// isRetryableError returns true if the error output indicates a transient -// lock-related error that may succeed on retry -func isRetryableError(output string) bool { - return strings.Contains(output, ".git/index.lock") || - strings.Contains(output, "cannot lock ref") +// isRetryableError returns true if a failed command hit a transient +// 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. +func isRetryableError(output string, err error) bool { + text := output + if err != nil { + text += "\n" + err.Error() + } + return strings.Contains(text, ".git/index.lock") || + strings.Contains(text, "cannot lock ref") } func (self *gitCmdObjRunner) Run(cmdObj *oscommands.CmdObj) error { @@ -58,7 +64,7 @@ func (self *gitCmdObjRunner) retryOnLockError(run func() (string, error)) (strin for range RetryCount { output, err = run() - if err == nil || !isRetryableError(output) { + if err == nil || !isRetryableError(output, err) { return output, err } diff --git a/pkg/commands/git_cmd_obj_runner_test.go b/pkg/commands/git_cmd_obj_runner_test.go index 2ff2795dc..903c6a90b 100644 --- a/pkg/commands/git_cmd_obj_runner_test.go +++ b/pkg/commands/git_cmd_obj_runner_test.go @@ -102,10 +102,6 @@ func TestRunWithOutputRetriesWhenLockErrorIsOnlyInError(t *testing.T) { _, err := newTestRunner(inner).RunWithOutput(dummyCmdObj()) - /* EXPECTED: assert.NoError(t, err) assert.Equal(t, 2, inner.calls) - ACTUAL: */ - assert.Error(t, err) - assert.Equal(t, 1, inner.calls) }