Skip to content

Add basic support for worktree branches (issue #629) - #633

Merged
hannesa2 merged 3 commits into
gitx:masterfrom
goneng:mark-worktree-branches
Sep 15, 2026
Merged

hannesa2 merged 3 commits into
gitx:masterfrom
goneng:mark-worktree-branches

Conversation

@goneng

@goneng goneng commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

A branch checked out somewhere else cannot be checked out here.

Closes the basic half of #629, independent of #630.

Two commits. The first fixes #635 - the ref label colors that had drifted into
two hard-coded palettes, and the current branch never reaching its orange in
the commit details pane. Both predate this work and can be reviewed on their
own. The second is the worktree support on top.

Worth knowing while reading the second: the #635 fix is also what makes the
worktree labels work, since a ref reaches the page through WebScripting as a
function and the class comparison had to be called through.

GitX showed such a branch like any other and then offered actions on it that
git refuses - or, in one case, that git does not refuse and should.

  • Read git worktree list --porcelain and answer which worktree holds a given
    ref, our own excluded, re-read whenever the refs are reloaded
  • Give those branches their own sidebar icon, a branch stem joined to a window,
    drawn on the same 16x16 grid as the icons beside it and kept in
    Resources/Images/Images.svg with the others; the row's tooltip names the
    worktree, since the icon says "held elsewhere" but not where
  • Color their labels teal in both commit panes. colorForRef: switches on ref
    type, and a worktree branch is still type head, so those labels were plain
    green
  • Offer "Open Worktree" in place of Checkout, its tooltip carrying the full
    path, and grey out Remove with a tooltip saying which worktree holds the
    branch - both settled while the menu is built rather than after the action
    fails. Remove stays in place rather than disappearing, so the entry does not
    go missing from under a reader who knows it should be there; a disabled item
    is built without its action, which is what keeps it from firing wherever the
    menu is shown.
  • Refuse the removal in deleteRef: as well, so a caller that never asked
    cannot walk into it either

Why Remove matters more than Checkout

Checkout of such a branch is refused by git itself, so the cost of not knowing
was a confusing error (which #630 improves). Remove is different: GitX deletes
refs with update-ref -d, which has none of the safety git branch -d has.

$ git branch -D feature
error: cannot delete branch 'feature' used by worktree at '.../wt'

$ git update-ref -d refs/heads/feature     # what GitX runs
$ (exit 0, silent)

$ cd ../wt && git status
On branch feature
No commits yet

The other worktree is left on a branch that no longer exists. That is why the
check is in the repository as well as in the menu.

On the teal: the palette draws black text with no color set, so every existing
label color sits between 10.7:1 and 17.5:1 against it. #8fe0d5 is 13.8:1, and
at 36% saturation it sits between the green (42%) and the remote blue (32%)
rather than standing out from both.

Screenshots of Suggested Design

Local Branch - Show the Correct Colors (Bug-Fix 1)

GitX_Issue-#633_support-worktrees_local-branch

 

Current Branch - Show the Correct Colors (Bug-Fix 2)

GitX_Issue-#633_support-worktrees_current

 

Worktree Branch - Show the New "Teal" Color and the New Icon

GitX_Issue-#633_support-worktrees_worktree

 

Worktree Branch - Show the New Context-Menu

GitX_Issue-#633_support-worktrees_context-menu
  • Can open a new GitX instance at the worktree's path
  • Cannot delete the worktree's branch from here - we need to add support for that

 

Test plan

  • make unit-test: 113 tests, 0 failures, 12 of them new, in
    PBGitRepositoryWorktreeTests (the porcelain reader over a main worktree, a
    linked one, a detached one, a locked one and a bare repository; our own
    branch excluded; the refusal to remove) and PBGitHistoryWorktreeMenuTests
    (Open Worktree replacing Checkout, Remove present but disabled and saying
    why, an ordinary branch unchanged)
  • Confirmed the reader tests bite: with the reader returning nothing, 4 of them
    fail
  • Checked in a Debug build against this repository, which has five worktrees of
    its own. Read back through accessibility, every branch another worktree holds
    carries WorktreeBranchTemplate, while master - held by this checkout -
    carries BranchTemplate plus the checkmark, and pr625, held nowhere,
    carries BranchTemplate alone. That matches git branch exactly, and it
    picked up a worktree added after the first run without being told.
  • The commit list path was confirmed the same way: the teal branch runs for
    those refs and no others
  • The icon and the two label colors were rendered offscreen to compare them at
    16x16 and magnified, in both tints
  • The commit details pane was checked by reading its rendered DOM back out of
    the page: master [refs head currentBranch], a worktree branch
    [refs head worktree], a remote [refs remote]
  • Not verified by eye in the running app: the tooltips (accessibility
    exposes no AXHelp for those rows) and the context menu itself, which does
    not survive an accessibility query - hence the unit test over
    menuItemsForRef:

Two things worth a look

The reader caches per refs-reload, and an early draw that finds no git binary
must not poison that cache with an empty answer - an earlier build did exactly
that, and no icon appeared until something forced a reload. It now returns empty
without caching, so the next draw retries.

The first lookup after each reload shells out to git inside a draw pass on the
main thread. That is the shape the sidebar already had, one subprocess per
reload, but the commit list reaches for it too now.

Not in scope: creating or removing worktrees, and a WORKTREES section in the
sidebar next to SUBMODULES.

@goneng goneng changed the title Mark branches that another worktree holds (issue #629) Tell branches held by another worktree apart (issue #629) Sep 13, 2026
@goneng
goneng force-pushed the mark-worktree-branches branch 2 times, most recently from 09eac74 to f9583c5 Compare September 13, 2026 18:03
@goneng goneng changed the title Tell branches held by another worktree apart (issue #629) Add basic support for worktree branches (issue #629) Sep 13, 2026
@goneng
goneng force-pushed the mark-worktree-branches branch 4 times, most recently from 99c496a to b0d90ae Compare September 13, 2026 20:15
@goneng
goneng force-pushed the mark-worktree-branches branch 2 times, most recently from a229648 to 3a4a07e Compare September 13, 2026 20:33
@goneng
goneng marked this pull request as ready for review September 13, 2026 21:20
@goneng
goneng force-pushed the mark-worktree-branches branch from 3a4a07e to deba78e Compare September 13, 2026 21:29
@hannesa2
hannesa2 requested a lite review from Copilot September 14, 2026 05:25
@hannesa2

Copy link
Copy Markdown
Contributor

Cool and thank you for the image rich explanation.
As my objective-c knowledge is very limited, I try for a review here Copilot. No clue, it the output of that makes sense here. It's on you to ignore it or jump into the suggestion

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved branch-state, duplicate-worktree handling, and main-thread performance issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds support for branches checked out in linked Git worktrees, including safer actions, visual indicators, and worktree-aware ref labels.

Changes:

  • Detects and caches worktree branch ownership while preventing unsafe removal.
  • Adds worktree icons, tooltips, colors, and “Open Worktree” actions.
  • Centralizes ref colors and adds coverage for repository and menu behavior.
File summaries
File Summary
Resources/Images/Images.svg Adds worktree branch icon artwork.
Resources/html/views/history/history.js Applies worktree-aware ref styling.
Resources/html/views/history/history.css Adds shared ref color variables and teal styling.
GitXTests/PBGitRepositoryWorktreeTests.m Tests worktree parsing and deletion protection.
GitXTests/PBGitHistoryWorktreeMenuTests.m Tests worktree context-menu behavior.
GitX.xcodeproj/project.pbxproj Registers updated sources, tests, and resources.
Classes/Views/PBSourceViewItem.m Supports named icon loading.
Classes/Views/PBSourceViewItem.h Declares named-icon loading.
Classes/Views/PBSidebarTableViewCell.m Displays worktree tooltips.
Classes/Views/PBSidebarTableViewCell.h Adds worktree path state.
Classes/Views/PBGitRevisionCell.m Applies centralized and worktree label colors.
Classes/Views/PBGitRefLabelColors.m Defines shared ref colors.
Classes/Views/PBGitRefLabelColors.h Declares the color provider API.
Classes/git/PBGitRepository.m Reads worktrees and blocks unsafe deletion.
Classes/git/PBGitRepository.h Exposes worktree lookup APIs.
Classes/Controllers/PBWebHistoryController.m Passes worktree refs and colors to the web pane.
Classes/Controllers/PBGitWindowController.m Opens linked worktrees.
Classes/Controllers/PBGitWindowController.h Declares the open-worktree action.
Classes/Controllers/PBGitSidebarController.m Shows worktree icons and paths.
Classes/Controllers/PBGitHistoryController.m Updates checkout and remove menu actions.
Review details

Suppressed comments (2)

Classes/Controllers/PBWebHistoryController.m:124

  • When the selected branch changes to another branch with the same tip commit, this path only refreshes worktreeRefs; commit.currentRef remains the previous branch because reload never receives or updates it. The details pane will therefore keep the orange class on the old branch. Pass the current head ref into reload (or otherwise refresh currentRef) before showRefs.
		[[self script] callWebScriptMethod:@"reload" withArguments:@[ [repository refNamesHeldByOtherWorktrees] ]];

Classes/Views/PBGitRevisionCell.m:223

  • colorForRef: is called from drawLabelAtIndex:, so the first label drawn after every refs reload synchronously launches git worktree list and waits for PBTask on the main thread. A slow or unavailable git process can freeze the history view, and this new call makes the commit list pay that cost as well as the sidebar; populate or refresh the worktree map outside drawing (or asynchronously) before using it here.
	if ([[controller repository] isRefHeldByAnotherWorktree:ref])
		return [PBGitRefLabelColors worktreeBranchColor];
  • Files reviewed: 19/21 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Classes/git/PBGitRepository.m Resolved
if (![PBGitBinary path].length)
return @{};

NSString *output = [self outputOfTaskWithArguments:@[ @"worktree", @"list", @"--porcelain" ] error:NULL];

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fixed here, tracked as #647. Measured at 9.4ms median, once per refs reload; the drawing paths predate this PR, so moving the snapshot off the main thread is better done on its own.

@goneng

goneng commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator Author

@hannesa2, it is always good for someone else to review your work, even if it is a bot :)

Copilot found a design-flaw, a bug and a performance issue I should also fix... - On It!

…#635)

One palette written down twice had drifted apart, and one label never
reached its color at all.

The history list and the commit details pane each carry their own copy
of the ref label colors, which no longer agree, and in that pane the
current branch is drawn green rather than orange. Issue gitx#635 has both,
and how the second one hid for so long.

* Write the colors down once and hand them to the page as CSS variables
  when it loads, leaving the same values in the stylesheet as fallbacks
* Keep the history list's shades, the paler set, for both panes
* Call the ref through, so the current branch is told apart again
A branch checked out somewhere else cannot be checked out here.

GitX showed such a branch like any other, in the sidebar and in both
commit panes alike, and went on to offer actions that git refuses or,
worse, that `update-ref -d` carries out and leaves the other worktree
pointing at a branch that is gone.

* Read `git worktree list --porcelain` and answer which worktree holds a
  given ref, our own excluded, re-read whenever the refs are reloaded
* Give those branches their own sidebar icon and a teal label in both
  commit panes, and name the worktree holding them in the row's tooltip
* Offer that worktree in place of Checkout, and grey out Remove with a
  tooltip saying which worktree holds it, both settled while the menu is
  built
* Refuse the removal in the repository itself, so a caller that never
  asked cannot walk into it either

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Moderate issues remain in worktree activation, synchronous rendering lookups, and detached-ref state handling; cache lifecycle coverage is also incomplete.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

Classes/Views/PBGitRevisionCell.m:223

  • isRefHeldByAnotherWorktree: reaches worktreePathsByRefName, which synchronously launches git worktree list when the cache is empty. Because this call is made from drawLabelAtIndex:, the first commit-list draw after every reloadRefs now blocks the main thread on a subprocess; populate or refresh this data off the drawing path and invalidate the cells when it is ready.
	if ([[controller repository] isRefHeldByAnotherWorktree:ref])
		return [PBGitRefLabelColors worktreeBranchColor];

Classes/git/PBGitRepository.m:316

  • outputOfTaskWithArguments: uses PBTask's synchronous launchTask: (with a 30-second timeout), and the new callers reach this lazy getter from sidebar and commit-label drawing. A slow or unavailable git can freeze the main UI during the first lookup; when the command fails, the nil cache also causes another blocking process launch on each draw. Populate this map before rendering or expose an asynchronous refresh/invalidation path.
	NSString *output = [self outputOfTaskWithArguments:@[ @"worktree", @"list", @"--porcelain" ] error:NULL];

Classes/git/PBGitRepository.m:314

  • The added tests cover porcelain parsing and an injected dictionary, but not this getter’s cache lifecycle: the no-git/failed-command {} paths must remain uncached and reloadRefs must invalidate a previously populated cache. Without coverage here, either regression can silently make worktree branches look ordinary until an unrelated reload.
	if (_worktreePathsByRefName)
		return _worktreePathsByRefName;

	if (![PBGitBinary path].length)
		return @{};
  • Files reviewed: 19/21 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread Resources/html/views/history/history.js Resolved
@goneng

goneng commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator Author

Both are real: the double-click path bypasses the worktree handling entirely and still tries to check out, and the @"" I pass for a nil headRef is precisely the value the truthiness check in reload throws away, so the stale ref it was meant to fix survives a branch-to-detached refresh; fixing both now.

The sidebar marked these branches, but nothing else followed the
marking: one could still fall out of the map entirely, double-clicking
one still attempted the checkout git refuses, and the details pane
could go on naming a branch HEAD had already left.

* Leave out our own worktree by the path it sits at, so a branch shared
  with another worktree is still reported as held there
* Open the worktree when such a branch is double-clicked, instead of
  attempting the checkout and showing the refusal
* Name the current branch again when the details pane refreshes in
  place, and let an empty name clear it rather than be ignored
@goneng
goneng force-pushed the mark-worktree-branches branch from f9db304 to f747025 Compare September 15, 2026 06:16
@hannesa2
hannesa2 merged commit 9b93ee1 into gitx:master Sep 15, 2026
3 checks passed
@hannesa2 hannesa2 added breaking-change breaking-change and removed enhancement labels Sep 15, 2026
@goneng
goneng deleted the mark-worktree-branches branch September 15, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change breaking-change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ref labels in the commit details pane use the wrong colors

3 participants