Skip to content

Improve index.lock retry mechanism#5788

Open
stefanhaller wants to merge 5 commits into
v0.63.1from
improve-index.lock-retry
Open

Improve index.lock retry mechanism#5788
stefanhaller wants to merge 5 commits into
v0.63.1from
improve-index.lock-retry

Conversation

@stefanhaller

Copy link
Copy Markdown
Collaborator

In v0.63.0 we made a change to no longer use GIT_OPTIONAL_LOCKS=0 on git commands that are part of a "foreground" refresh, meaning the refresh after a lazygit command or the focus-in refresh. We do this on purpose to keep git's mod date cache from becoming stale, which could make lazygit become slower over time. However, this caused a problem for users who work very fast: staging a file and then immediately pressing shift-A to amend while the staging's refresh is still running would show the dreaded index.lock error.

We already had a retry-on-index-lock-error mechanism in place, but it wasn't used for commands like amend or commit; fix this so that the retry loop works for these too, and also make the retry window a little longer, and fix a problem where it wouldn't work in linked worktrees or submodules.

Closes #5778.

stefanhaller and others added 5 commits July 10, 2026 11:34
RunWithOutput and RunWithOutputs each carried their own near-identical
copy of the index.lock retry loop. Extract the loop into a single
retryOnLockError helper so the retry policy lives in one place, ahead of
changing that policy. Behavior is unchanged; the added tests characterize
it (success and non-lock errors run once, a lock error in the output is
retried).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gpg helper runs commands like amend with StreamOutput, so their
output isn't captured and a failed run returns an empty output string;
the index.lock message is carried by the error instead. isRetryableError
only inspects the output, so the retry loop never fires for these
commands. In practice this means a `shift-A` amend issued while a
foreground `git status` refresh briefly holds index.lock fails outright
instead of retrying.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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/<name>/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) <noreply@anthropic.com>
The retry budget was five fixed 50ms waits (250ms total). A foreground
`git status` refresh can hold index.lock for longer than that on a large
repo, so the retries could be exhausted before the lock clears. Wait 20ms
before the first retry and double each time, giving seven attempts over a
bit more than a second — enough to outlast a slow refresh while keeping
the common case (a lock that clears almost immediately) fast. The initial
delay is now a runner field so tests can zero it out instead of sleeping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant