Skip to content

Guard pr merge --delete-branch against worktree conflicts - #13955

Open
tidy-dev wants to merge 1 commit into
tidy-dev-worktree-client-helperfrom
tidy-dev-pr-merge-worktree-guards
Open

Guard pr merge --delete-branch against worktree conflicts#13955
tidy-dev wants to merge 1 commit into
tidy-dev-worktree-client-helperfrom
tidy-dev-pr-merge-worktree-guards

Conversation

@tidy-dev

@tidy-dev tidy-dev commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Closes https://github.com/github/gh-cli-and-desktop/issues/271
Closes #3442

Makes gh pr merge --delete-branch behave safely under git worktrees. Today the local-cleanup half of --delete-branch unconditionally runs git checkout <base-branch> then deletes the branch. In a worktree setup this either fails (fatal: '<base>' is already used by worktree at ...) or silently repurposes the worktree, leaving a confusing partial state.

Changes

Branches the local-cleanup logic on whether the PR head branch is checked out in a linked worktree:

  1. cwd IS the linked worktree for the PR head branch - skip local cleanup, print a warning with manual instructions. The merge and remote branch deletion still succeed.
  2. PR head branch is in a sibling linked worktree - git worktree remove it, then delete the local branch ref. No git checkout <base> needed.
  3. No worktree conflict - existing behavior unchanged.

In all cases gh pr merge never exits non-zero solely because local worktree cleanup could not complete.

Demo

demo-pr-merge-worktree
Text transcript of the demo

Three worktrees are set up: main-checkout (main), feature-wt (linked, on wt-demo-scenario1), and sibling-wt (linked, on wt-demo-scenario2).

  1. Show starting state
    git worktree list shows all three worktrees.

  2. Scenario 1: merge from inside the PR's linked worktree

    cd /tmp/worktree-demo/feature-wt
    git branch --show-current
    wt-demo-scenario1
    
    gh pr merge --squash --delete-branch
    ✓ Squashed and merged pull request tidy-dev/playground#27 (Demo scenario 1)
    ! Branch wt-demo-scenario1 is checked out in the current worktree (/private/tmp/worktree-demo/feature-wt); skipping local delete
      To finish cleanup, first navigate out of the worktree, then run:
      git worktree remove /private/tmp/worktree-demo/feature-wt && git branch -D wt-demo-scenario1
    ✓ Deleted remote branch wt-demo-scenario1
    
  3. Scenario 2: merge from main worktree (PR branch lives in sibling)

    cd /tmp/worktree-demo/main-checkout
    gh pr merge wt-demo-scenario2 --squash --delete-branch
    ✓ Squashed and merged pull request tidy-dev/playground#28 (Demo scenario 2)
    ✓ Removed worktree /private/tmp/worktree-demo/sibling-wt
    ✓ Deleted local branch wt-demo-scenario2
    ✓ Deleted remote branch wt-demo-scenario2
    
  4. Final state
    git worktree list shows only main-checkout and feature-wt remain - sibling-wt was removed automatically, while feature-wt was preserved with cleanup instructions.

Design decisions

  • linkedWorktreeForBranch() skips worktrees[0] - git always lists the main worktree first. By iterating [1:] we only consider linked worktrees, eliminating false positives in normal (non-worktree) repos.
  • len(worktrees) > 1 guard - if only the main worktree exists, no linked worktrees are possible so we skip worktree logic entirely.
  • No --force on git worktree remove - a dirty worktree is reported as a warning rather than forced, preserving uncommitted work.
  • Conventional single-working-directory path unchanged - the git checkout <base> + pull + delete path only runs when there's no worktree involvement.

Test scenarios (4 new + 9 updated)

Scenario Behavior
cwd is linked worktree on PR branch Warn + skip local delete
PR branch in sibling linked worktree Remove worktree + delete branch
Dirty sibling worktree Warn, skip branch delete
No worktree conflict (normal repo) Existing behavior unchanged

All 9 existing deleteLocalBranch tests updated with worktree stubs to prevent panics from the new git worktree list call.

Rework the local-cleanup half of --delete-branch to handle git worktrees:

Scenario 1 - cwd is the PR head worktree: skip local cleanup entirely
and print a warning with manual cleanup instructions, since we cannot
safely check out another branch or remove the worktree we are standing
inside.

Scenario 2 - cwd is not the PR head worktree but a sibling worktree has
the branch: remove that worktree via git worktree remove, then delete
the branch ref. If removal fails (e.g. dirty worktree), warn and skip
rather than exiting non-zero after a successful merge.

The conventional single-working-directory path (no worktrees) is
unchanged. Remote branch deletion proceeds normally in all cases.

Also adds git.Client.WorktreeRemove() helper.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@tidy-dev
tidy-dev marked this pull request as ready for review July 23, 2026 20:28
@tidy-dev
tidy-dev requested a review from a team as a code owner July 23, 2026 20:28

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.

Pull request overview

Updates gh pr merge --delete-branch local cleanup to be safe in repositories using git worktrees, avoiding disruptive git checkout <base> behavior when the PR head branch is checked out in a linked worktree.

Changes:

  • Detect whether the PR head branch is checked out in the current linked worktree vs a sibling linked worktree vs no worktree involvement, and adjust local cleanup accordingly.
  • Add a git.Client.WorktreeRemove helper and wire it into PR merge local cleanup.
  • Expand/update PR merge tests to stub git worktree list / --show-toplevel and add new worktree-specific scenarios.
Show a summary per file
File Description
pkg/cmd/pr/merge/merge.go Adds worktree-aware branching to local branch deletion behavior and introduces linkedWorktreeForBranch.
pkg/cmd/pr/merge/merge_test.go Adds new worktree scenarios and updates existing stubs for new git calls.
git/client.go Adds a WorktreeRemove helper on the git client.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (2)

pkg/cmd/pr/merge/merge_test.go:2149

  • If git worktree remove is invoked with a -- delimiter (to prevent option injection via paths starting with -), this stub needs to include -- so the test continues to match the executed command.
	cs.Register(`git worktree remove /path/to/feature-wt`, 0, "")

pkg/cmd/pr/merge/merge_test.go:2202

  • If git worktree remove is invoked with a -- delimiter, this failing stub should also include -- so the test matches the executed command.
	cs.Register(`git worktree remove /path/to/feature-wt`, 128, "fatal: '/path/to/feature-wt' contains modified or untracked files, use --force to delete it")
  • Files reviewed: 3/3 changed files
  • Comments generated: 4
  • Review effort level: Low

Comment thread pkg/cmd/pr/merge/merge.go
Comment on lines +420 to +421
_ = m.warnf(" git worktree remove %s && git branch -D %s\n",
currentWorkdir, m.pr.HeadRefName)
Comment thread pkg/cmd/pr/merge/merge.go
Comment on lines +412 to +415
// worktrees[0] is always the main worktree (the initially cloned repo).
// If our workdir differs we are inside a linked worktree.
isInLinkedWorktree := len(worktrees) > 1 && worktrees[0].Path != currentWorkdir

Comment thread git/client.go
// WorktreeRemove removes the worktree at the given path via
// `git worktree remove <path>`.
func (c *Client) WorktreeRemove(ctx context.Context, path string) error {
cmd, err := c.Command(ctx, "worktree", "remove", path)
assert.Equal(t, "", output.String())
assert.Contains(t, output.Stderr(), "skipping local delete")
assert.Contains(t, output.Stderr(), "navigate out of the worktree")
assert.Contains(t, output.Stderr(), "git worktree remove /path/to/feature-wt && git branch -D feature")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants