Skip to content

Fix index.lock contention caused by git update-index on every FSEvents notification - #547

Merged
hannesa2 merged 1 commit into
gitx:masterfrom
goneng:master
Jun 14, 2026
Merged

hannesa2 merged 1 commit into
gitx:masterfrom
goneng:master

Conversation

@goneng

@goneng goneng commented Jun 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #164

While GitX has a repo open in the commit view, every file-system event triggered git update-index --refresh, which holds .git/index.lock for its entire run. This caused concurrent git operations from terminals or editors (git add, git commit, git stash, etc.) to intermittently fail with Unable to create index.lock: File exists.

  • Remove git update-index --refresh from the PBGitIndex refresh path; the remaining reads (diff-index, diff-files, ls-files) are read-only and never acquire the index lock
  • Add -refreshStatCache which runs update-index --refresh as a fire-and-forget call (followed by a refresh) to clear phantom stat-dirty entries
  • Call refreshStatCache on NSApplicationDidBecomeActiveNotification so the stat cache is reconciled when the user switches back to GitX, rather than on every file-system event

Test plan

Reproducing the bug requires commands that actually acquire index.lock (e.g. git add) and a repo large enough that update-index --refresh holds the lock for a measurable window.

Reproduce on unpatched build:

  1. Create a repo with many tracked files (500+ increases the lock-hold window)
  2. Open it in GitX with the Commit tab visible
  3. In a terminal on that repo, run a background writer and a git add loop:
   # keep FSEvents firing
   while true; do for i in 1 50 100 200 300; do echo $RANDOM > file_$i.txt; done; sleep 0.2; done &

   # hammer git add and watch for lock errors
   for i in $(seq 1 500); do
     echo "$i" > file_1.txt
     git add file_1.txt 2>&1 | grep -q "index.lock" && echo "LOCK ERROR on attempt $i"
   done

With the unpatched binary, expect several LOCK ERROR lines. With the fix applied, expect zero.

Verify stat-cache refresh still works:

  • Edit a file externally (e.g. touch file_1.txt) so its mtime drifts
  • Switch away from GitX and back -- the phantom "modified" entry should clear on reactivation

@hannesa2

hannesa2 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

I can't reproduce the issue, that's why I can't verify that this fixes the issue.

Test plan seems to be wrong

while true; do git status 2>&1 | grep "index.lock" && echo "LOCK ERROR"; sleep 0.3; done
there the grep "index.lock" should be replaced by find .git -name index.lock

There is no index.lock

I do
image

but there is no index.lock
image

I use
image

@goneng

goneng commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you - I will try to improve the tests around this issue, because the current workflow is a bit tricky to perform.
In terms of behaviour, with the current version of gitx open on a repo, I cannot safely run git commands (like 'rebase' via the terminal) in the same repo, as they fail to update the index.

…s notification

The commit view triggered git update-index --refresh on every file-system
event via the FSEvents -> PBGitRepositoryEventNotification -> refresh chain.
This held .git/index.lock for the duration of each run, causing concurrent
git operations (git add, git commit, git stash, etc.) from terminals or
editors to fail with "Unable to create index.lock: File exists".

- Remove git update-index --refresh from the PBGitIndex refresh path;
  the remaining reads (diff-index, diff-files, ls-files) are read-only
  and never acquire the index lock
- Add -refreshStatCache, which runs update-index --refresh as a separate
  fire-and-forget call followed by a refresh, to clear phantom stat-dirty
  entries (files that appear modified due to mtime mismatch but have
  unchanged content)
- Call refreshStatCache on NSApplicationDidBecomeActiveNotification so
  the stat cache is reconciled when the user switches back to GitX,
  rather than on every file-system event

Fixes gitx#164
@goneng

goneng commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author
  1. Rebased my work on-top of master
  2. Improved test-plan to use git add ... (that requires locking the index) instead of git status that does not lock
  3. Adding here a test-script that is NOT part of the commit:
#!/bin/bash
# Reproduce GitX index.lock contention bug.
#
# Two modes:
#   --simulate   self-contained: runs update-index in a tight loop to mimic old GitX
#   (default)    tests real GitX: open the printed repo path in GitX commit view, press Enter
#
# Design:
#   - Large repo (500 files) so update-index --refresh takes longer -> wider lock window
#   - Background writer loop modifies files every 0.2s to keep FSEvents firing continuously
#   - Foreground loop hammers "git add" independently of the writer
#   - Also polls .git/index.lock directly (catches contention even if git add retries)

set -euo pipefail

SIMULATE=0
ITERATIONS=500
[[ "${1:-}" == "--simulate" ]] && SIMULATE=1

REPO=$(mktemp -d)
WRITER_PID=""
GITX_SIM_PID=""

cleanup() {
    [[ -n "$WRITER_PID" ]] && kill "$WRITER_PID" 2>/dev/null || true
    [[ -n "$GITX_SIM_PID" ]] && kill "$GITX_SIM_PID" 2>/dev/null || true
    rm -rf "$REPO"
}
trap cleanup EXIT

cd "$REPO"
git init -q
git config user.email "[email protected]"
git config user.name "Test"

echo "==> Creating 500-file repo (wider lock window for update-index)..."
for i in $(seq 1 500); do
    echo "file $i" > "file_$i.txt"
done
git add .
git commit -q -m "init"

echo "    Repo: $REPO"
echo ""

# Background writer: modifies files every 0.2s to keep FSEvents firing.
# Decoupled from the git add loop so FSEvents fires throughout the entire test.
start_writer() {
    while true; do
        for i in 1 50 100 200 300 400 500; do
            echo "$RANDOM" > "file_$i.txt"
        done
        sleep 0.2
    done
}

if [[ $SIMULATE -eq 1 ]]; then
    echo "==> Simulating OLD GitX behavior (update-index in tight loop)..."
    while true; do
        git update-index -q --unmerged --ignore-missing --refresh 2>/dev/null
    done &
    GITX_SIM_PID=$!
    start_writer &
    WRITER_PID=$!
else
    echo "==> Open this repo in GitX and switch to the Commit tab:"
    echo "    $REPO"
    echo ""
    echo "    Press Enter when ready..."
    read -r
    start_writer &
    WRITER_PID=$!
    echo "    File writer started (keeps FSEvents firing every 0.2s)"
fi

echo ""
echo "==> Running $ITERATIONS rapid 'git add' attempts..."
echo ""

LOCK_ERRORS=0
SUCCESS=0

for i in $(seq 1 $ITERATIONS); do
    echo "change $i" > "file_1.txt"
    ERR=$(git add file_1.txt 2>&1 || true)
    if echo "$ERR" | grep -q "index.lock"; then
        LOCK_ERRORS=$((LOCK_ERRORS + 1))
        if [[ $LOCK_ERRORS -le 3 ]]; then
            echo "  [attempt $i] LOCK ERROR"
        fi
    else
        SUCCESS=$((SUCCESS + 1))
    fi
done

echo ""
echo "Results: $SUCCESS succeeded, $LOCK_ERRORS lock errors / $ITERATIONS attempts"
echo ""

if [[ $LOCK_ERRORS -gt 0 ]]; then
    echo "FAIL: index.lock contention detected -- bug is present"
    exit 1
else
    echo "PASS: no lock contention detected"
    echo "      (run 3x to be confident -- race is timing-dependent)"
    exit 0
fi

@hannesa2
hannesa2 merged commit a1bb182 into gitx:master Jun 14, 2026
2 checks passed
@hannesa2

Copy link
Copy Markdown
Contributor

Thanks a lot for the fix and the script !
On my M4 I don't run into the issue anymore

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitX locks repository for read-only access

2 participants