Skip to content

Fix phantom-files stuck in the staging-area - #571

Merged
hannesa2 merged 3 commits into
gitx:masterfrom
goneng:fix-phantom-files-in-stage-area
Aug 18, 2026
Merged

hannesa2 merged 3 commits into
gitx:masterfrom
goneng:fix-phantom-files-in-stage-area

Conversation

@goneng

@goneng goneng commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

The stage view could list unchanged files as new, and staging one showed it
as staged in GitX while git status reported that path as deleted.
Nothing cleared the rows short of relaunching.
(The full analysis is in the individual commit messages.)

  • Derive the file list from all three git outputs of a refresh at once, so
    an entry cannot outlive the state it came from and the result no longer
    depends on which of the three concurrent commands finishes first
  • Re-run a refresh that arrived while one was in flight instead of dropping
    it, so the settling event of a rebase burst is not lost
  • Read the index back after staging, so the view cannot claim an outcome
    git never recorded

Related

On #428 / #530: an index-only write by GitX does not trigger the FSEvents
refresh, so the refresh added here is the only one for those operations
rather than a duplicate, and it runs the same three read-only commands as
CMD-R (no update-index, so no index.lock). Coalescing keeps at most one
refresh in flight, so 0903ca4's process-storm fix still holds.

Test plan

  • xcodebuild test -scheme GitX -only-testing:GitXTests - 11 tests pass,
    including 9 new ones covering both routes into the stuck state, order
    independence, and the status precedence staging depends on

  • Scratch repo driven from a terminal with git mv on files and folders
    plus repeated rebases: the stage view settles to exactly git status,
    with no leftover rows

  • 60+ rapid index and working-tree mutations: panes matched git status
    exactly (20 staged / 22 unstaged), including a path both staged-added and
    deleted in the working tree

  • Staging and unstaging through the UI now agree with git status within a
    second; verified against the pre-change build, where an index-only
    unstage left the view stale until a manual refresh

  • Toggling Amend while a refresh was in flight, with git slowed so the
    window is deterministic: the staged pane and the Commit button both
    settle on the amended commit's contents. Against the pre-change build
    the same toggle left the pane empty and the button disabled for good,
    with only a stage/unstage able to recover it

  • Depends on Stand up GitXTests unit-test target with a race-condition regression test #566,
    which adds the GitXTests target and is included in this branch until it lands.
    Will rebase once it is merged.

@goneng goneng changed the title Fix phantom files stuck in the stage area Fix phantom-files stuck in the staging-area Aug 14, 2026
@goneng
goneng force-pushed the fix-phantom-files-in-stage-area branch 2 times, most recently from a9bbdbf to 48c58b6 Compare August 17, 2026 21:28
@goneng
goneng marked this pull request as ready for review August 17, 2026 21:28
@hannesa2

Copy link
Copy Markdown
Contributor

Please rebase to recent master

goneng added 3 commits August 18, 2026 12:07
Derive the whole file list from the three git outputs of a single index
refresh, so that an entry cannot outlive the change it stood for.

The Stage view could list files that were not changed at all, showing them as new.
Staging one moved it to Staged Changes in GitX while git status reported
that path as deleted, and nothing cleared the row short of relaunching GitX.

Entries were only deleted once both 'hasStagedChanges' and 'hasUnstagedChanges'
were NO, and those flags were cleared by inferring from a file's absence
from one of the three dictionaries.
An entry with status NEW and a non-nil 'commitBlobSHA' failed the guard on both
branches that could clear 'hasUnstagedChanges', so it was never cleared
and never deleted.
The files array starts empty at launch, hence the relaunch being the only cure.

Two ways in, both reachable from renaming and rebasing outside GitX.
git 'diff-index --cached' reports an added path with an all-zero source SHA,
which was recorded as a real blob while status stayed NEW.
Separately, a tracked path appears in 'ls-files --others' whenever the index
momentarily lacks it, which set status to NEW while keeping the blob SHA from
an earlier pass.
Which of the two fired depended on the order the three concurrent commands
finished in, so the symptom was intermittent and hit arbitrary files.

- Collect the three command outputs and reconcile once, in the completion block,
  instead of folding each into the shared file list as it arrives
- Assign every property from the collected data, so the result depends on
  git's output alone and no entry can survive a clean working tree
- Record no blob when the source mode is ':000000', rather than storing the
  "all-zero SHA" that git reports as though it were a real blob
- Keep the previous list when a command failed, rather than reading a
  failure as an empty working tree
- Reuse the existing entry for a path so table-selection survives a refresh
- Announce the index-update once the reconcile has run, so that observers
  reading the file list are not served the previous refresh's contents
- Add 'PBGitIndexReconcileTests' covering both routes into the stuck state,
  independence from prior state, and the status-precedence that staging
  and unstaging depend on
Re-run a refresh that arrived while another was in flight, rather than
discarding the request and leaving the view showing stale state.

A refresh requested during an in-flight one was skipped outright and never re-run.
Rebasing or renaming from a terminal produces a burst of 'FSEvents',
so the final settling event - the one carrying the state the user ends up
looking at - was the most likely of the burst to be dropped.
A skipped refresh also never posted 'PBGitIndexFinishedIndexRefresh', leaving the
commit view's busy flag set and its status stuck on "Refreshing index".

- Record the request instead of dropping it, and run it once the in-flight
  refresh finishes, so no overlapping refreshes still holds
- Read and clear the pending flag together with the in-progress flag, so a
  request arriving as the refresh ends cannot be lost between the two
Read the index back after staging or unstaging, instead of assuming the
outcome git was asked for is the outcome git recorded.

Staging flipped each file's staged and unstaged flags optimistically and
nothing re-read the index afterwards, so the view reported what had been
requested rather than what happened.
Staging is done with 'update-index --add --remove', which stages a deletion
for any path that no longer exists on disk.
A row left over from a path a rename had moved away, therefore, staged the
deletion of that path while GitX showed it as an ordinary staged change.

- Refresh once the update-index call has succeeded, keeping the optimistic
  flag update so the tables still respond immediately
- Safe to issue per operation, now that an overlapping refresh is coalesced
  rather than dropped or run concurrently
@goneng
goneng force-pushed the fix-phantom-files-in-stage-area branch from 48c58b6 to 24849b9 Compare August 18, 2026 09:47
@goneng

goneng commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Please rebase to recent master

Sure - done.
(thank you for managing this project - I enjoy using GitX every day, and now - more than ever 🙂)

@hannesa2

Copy link
Copy Markdown
Contributor

Btw, I would love to see some tests concerning your changes

@hannesa2
hannesa2 merged commit 51f4112 into gitx:master Aug 18, 2026
3 checks passed
@goneng

goneng commented Aug 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Btw, I would love to see some tests concerning your changes

@hannesa2, Most of my bug-fixes come from my workflow, where I rebase/force-push from the terminal while GitX is open in the background.
This tripped some race-conditions in GitX's workflow that led to strange cases like the Phantom "Detached HEAD", the incoherent file-statuses in "Stage" view (especially when amending commit-content during rebase) and the 'index.lock' issue that drove me nuts...
As it had been a white since I wrote in Objective-C and the code here is well-developed, I usually give Claude the scenario and let it figure-out what could cause it to fail in a specific way.
GitXTests Unit-Tests were "revived" to help confirm each scenario before and after the suggested fix.

@goneng
goneng deleted the fix-phantom-files-in-stage-area branch August 18, 2026 11:42
hannesa2 pushed a commit that referenced this pull request Aug 20, 2026
Add tests for the two commits of #571 that landed without any, and for
the notification ordering that the third one changed.

PBGitIndexReconcileTests covers how the file list is derived, but
nothing exercised what surrounds it: that staging re-reads the index
rather than trusting the flags it has just set, that a refresh announces
the file list only once it has been rebuilt from all three commands, and
that a request arriving during a refresh is deferred rather than
discarded.

None of that needs a repository on disk. -[PBGitRepository init] touches
nothing outside itself, and the two collaborators that would otherwise
reach the filesystem are ordinary overridable methods, so test-local
subclasses record the calls instead of making them.

- Extract the refresh guard into -beginRefreshOrDeferIt and
  -endRefreshTakingDeferred so the coalescing contract can be asserted
  directly, leaving what they do unchanged
- Add PBGitIndexRefreshTests: staging and unstaging ask git and then
  read the index back, a rejected update-index reads nothing back,
  neither arm of a per-command result announces an index update, and a
  burst of requests collapses into exactly one re-run
- Give the GitXTests target a header search path for Classes/Util, which
  PBGitRepository_PBGitBinarySupport.h needs
@goneng

goneng commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Btw, I would love to see some tests concerning your changes

@hannesa2, I've improved the tests around this issue in PR#576

@hannesa2

Copy link
Copy Markdown
Contributor

Cool, thank you !

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.

Unstaging chunks occasionally gets stuck

2 participants