Repository navigation
Description
Summary
When an external process performs a branch-changing git operation that touches multiple files under .git/ in quick succession (e.g. git switch -c <new-branch>, which both creates a ref file and rewrites HEAD), the commit-history list (top-right pane, "All" scope) can end up showing every commit twice, and the diff pane shows "Multiple commits are selected" even though only one row was ever clicked.
Reproduction
- Open a repo in GitX with at least two branches/commits.
- In a terminal, in the same repo:
git switch -c another-branch(or run several such commands back-to-back, e.g. in a loop). - Watch the commit-history list (top-right pane): commits appear twice, and the commit list reports multiple selected commits.
It is intermittent — not every git switch -c reproduces it every time, which points to a timing race rather than a deterministic bug.
Root Cause
GitX watches the .git directory via FSEvents (PBGitRepositoryWatcher) and reacts to git-directory-changed notifications in PBGitHistoryController._repositoryUpdatedNotification:, which calls refresh: -> PBGitRepository.forceUpdateRevisions -> PBGitHistoryList.forceUpdate/updateHistory -> updateProjectHistoryForRev: (Classes/git/PBGitHistoryList.m).
updateProjectHistoryForRev: calls [projectRevList loadRevisionsWithCompletionBlock:...] (Classes/git/PBGitRevList.m) whenever it detects the refs changed. loadRevisionsWithCompletionBlock: cancels any previous in-flight parse and re-arms a resetCommits flag before queuing a new parse operation on a serial NSOperationQueue (max concurrency 1).
The problem: a single external command like git switch -c performs more than one write under .git/ (new ref file, then HEAD rewrite). FSEvents' 1-second coalescing latency does not reliably guarantee these land in a single callback batch — confirmed via lldb breakpoints on -[PBGitHistoryList updateProjectHistoryForRev:] and -[PBGitRevList loadRevisionsWithCompletionBlock:] that multiple overlapping refresh cycles can be triggered from a single burst of git operations (5 rapid git switch -c calls produced 4 breakpoint hits across both methods instead of the expected 10, i.e. some but not all of the FSEvents were coalesced, and the remainder raced).
When two loadRevisionsWithCompletionBlock: calls overlap on a small/fast repo, the previous (cancelled) parse could still flush its already-in-flight commit batch to -[PBGitRevList updateCommits:operation:] before the cancellation was observed — NSOperation.isCancelled is checked cooperatively, not enforced — and the new parse reset/appended independently, resulting in the same commits being appended to commits more than once.
Fix
Fixed by #564:
PBGitRevListnow tracks a per-instance load generation.loadRevisionsWithCompletionBlock:bumps it on every call, andupdateCommits:operation:generation:rejects any commit-batch flush whose generation no longer matches the current one — regardless of whetherNSOperation.isCancelledwas observed in time. This closes the race unconditionally, independent of FSEvents/operation-queue timing.PBGitHistoryController._repositoryUpdatedNotification:now debounces bursts of git-directory FSEvents (e.g. the ref-file write and theHEADrewrite from a singlegit switch -c) into a singlerefresh:call, reducing how often overlapping refresh cycles get triggered in the first place.
A GitXTests unit-test target with a regression test for the generation guard was also stood up, on a separate branch (standup-gitxtests-unit-tests), not included in #564.
Affected Code
Classes/git/PBGitRevList.m(loadRevisionsWithCompletionBlock:,updateCommits:operation:)Classes/Controllers/PBGitHistoryController.m(_repositoryUpdatedNotification:)
This was found incidentally while testing #562 (sidebar checked-out branch marker refresh) and is unrelated to that fix - confirmed the code paths involved here are untouched by it.
More Info
See attached screenshot -
- The commits in the commit-history pane are listed twice
- The summary-message at the bottom says
4 commits loadedthough the repo only has 2 commits - The UI also complains: "Multiple commits are selected."

Activity
Guard against stale PBGitRevList parses overlapping (#563)
Debounce git-directory refresh notifications (#563)
Merge pull request #564 from goneng/fix-duplicate-commits-race
Dispatch preferencesChanged WebKit calls to the main thread
Dispatch preferencesChanged WebKit calls to the main thread