Skip to content

History view shows duplicate commit rows after rapid branch-changing git operations (e.g. git switch -c) #563

Closed

Description

@goneng

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

  1. Open a repo in GitX with at least two branches/commits.
  2. 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).
  3. 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:

  • PBGitRevList now tracks a per-instance load generation. loadRevisionsWithCompletionBlock: bumps it on every call, and updateCommits:operation:generation: rejects any commit-batch flush whose generation no longer matches the current one — regardless of whether NSOperation.isCancelled was 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 the HEAD rewrite from a single git switch -c) into a single refresh: 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 loaded though the repo only has 2 commits
  • The UI also complains: "Multiple commits are selected."
Image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions