Repository navigation
Fix phantom "detached HEAD" entry stuck in the sidebar - #569
Merged
Merged
Conversation
Rebasing a series of local branches from a terminal while GitX watched the repository left a "detached HEAD" row permanently at the top of the BRANCHES list, long after every rebase had finished and HEAD was attached again. Selecting other branches did not clear it, because by then the row was no longer backed by the model at all. The full chain, confirmed by instrumenting reloadRefs and the sidebar: 1. reloadRefs injects a synthetic "HEAD" ref while isHEADDetached is true, which git rebase makes true for its whole run. An FSEvent refresh landing mid-rebase therefore adds a legitimate, transient "detached HEAD" row. Its title starts with a curly quote, so it sorts first in BRANCHES. 2. The sidebar's currentBranch and refs observers captured selectedRow as an index, called reloadData, then re-selected that same index. reloadData drops the selection entirely (observed: selectedRow becomes -1), so the forced re-select always fires a selection change - and because the new row was inserted at the top of BRANCHES, the stale index now denoted a different item. With the first branch row selected, that item was the phantom itself, so outlineViewSelectionDidChange: assigned currentBranch = "HEAD" with no user interaction. 3. When the rebase finished, reloadRefs correctly pruned "HEAD" and the branches KVO removal deleted the row, but the prune loop only guarded headRef, never currentBranch. Nothing else reconciles currentBranch after an external HEAD change: the FSEvent refresh path calls reloadRefs and only reloadRefs, while readCurrentBranch is invoked solely by GitX's own HEAD-moving operations. currentBranch was left dangling at a spec absent from repository.branches. 4. selectCurrentBranch fed that dangling spec into addRevSpec:, rebuilding the row as an orphan. The sidebar only deletes rows in response to a branches KVO removal, and the model had already removed this one, so no removal could ever fire for it again. Changes: - Re-select by item instead of by row index in both sidebar observers, via a new reloadPreservingSelection helper. This keeps 6e92c1a's intent of recomputing the checked-out badge on every refresh, without reassigning the user's selected branch behind their back. - Reset a pruned currentBranch through readCurrentBranch at the end of reloadRefs, so an externally moved or deleted HEAD heals itself. Guarded on the branch actually having been pruned, since setCurrentBranch: reaches reloadRefs again through updateHistory. - Look the row up with itemForRev: in selectCurrentBranch, so a sidebar row can never outlive its model entry. Row creation stays in the branches KVO insertion path. - Call readCurrentBranch after reloadRefs in pullBranch:fromRemote:rebase:, matching checkout, merge, cherry-pick, reset and rebase. GitX's own Pull and Rebase runs on a background queue, so it has the same mid-rebase window. Verified against a scratch repo: an external detach/re-attach, deleting the selected branch externally, a five branch external pull --rebase loop, and a conflicted rebase followed by --abort. The row still appears while HEAD is genuinely detached, always disappears afterwards, currentBranch never dangles, and the sidebar selection stays put throughout.
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rebasing a series of local branches from a terminal while GitX watched the
repository leaves a
"detached HEAD"row permanently at the top of the BRANCHESlist, long after every rebase has finished and HEAD is attached again. Selecting
other branches does not clear it, because by then the row is no longer backed by
the model at all. Confirmed by instrumenting
reloadRefsand the sidebar, thechain is:
reloadRefslegitimately injects a syntheticHEADref while a rebasehas HEAD detached, and its title sorts first in BRANCHES; the sidebar's observers
then re-select a captured row index across a
reloadDatathat has alreadydropped the selection, so the stale index lands on the newly inserted row and
silently assigns
currentBranch = HEAD; when the rebase ends, the prune loopremoves the ref but only guards
headRef, leavingcurrentBranchdangling, andnothing reconciles it because the FSEvent refresh path calls
reloadRefsand onlyreloadRefs; finallyselectCurrentBranchfeeds that dangling spec intoaddRevSpec:, rebuilding the row as an orphan that nobranchesKVO removal canever delete. The commit message carries the step-by-step detail.
currentBranchandrefsobservers, via a new
reloadPreservingSelectionhelper. This keeps 6e92c1a'sintent of recomputing the checked-out badge on every refresh, without silently
reassigning the user's selected branch whenever a row is inserted or removed
currentBranchthroughreadCurrentBranchat the end ofreloadRefs, so an externally moved or deleted HEAD heals itself. Guarded on thebranch actually having been pruned, since
setCurrentBranch:reachesreloadRefsagain throughupdateHistoryitemForRev:inselectCurrentBranch, so a sidebar row cannever outlive its model entry; row creation stays in the
branchesKVO insertionpath
readCurrentBranchafterreloadRefsinpullBranch:fromRemote:rebase:,matching checkout, merge, cherry-pick, reset and rebase. GitX's own Pull and
Rebase runs on a background queue, so it has the same mid-rebase window
Test plan
Reproduced first on an instrumented build against a scratch repo to confirm each
link in the chain, then re-run after the fix:
git checkout --detachfollowed by re-attach: the row appears while HEADis genuinely detached and disappears afterwards. Before the fix it survived, with
currentBranchstuck at theHEADspec and the scope bar reading"detached HEAD".currentBranchself-heals to the checked-out branch instead of dangling.git pull --rebase origin mainloop, and a conflictedrebase followed by
git rebase --abort: HEAD rows created and removed in balance,no dangling
currentBranchat any point, and the sidebar selection never moves onits own.
More Info
See attached screenshot