Skip to content

feat: Add opt-in linked worktree cleanup - #188

Closed
AlextheYounga wants to merge 1 commit into
gittower:mainfrom
AlextheYounga:feat/worktree-deletion
Closed

AlextheYounga wants to merge 1 commit into
gittower:mainfrom
AlextheYounga:feat/worktree-deletion

Conversation

@AlextheYounga

@AlextheYounga AlextheYounga commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

feat: Add opt-in linked worktree cleanup

Lets you finish or delete a branch that's checked out in a linked worktree, without making workspace deletion the default.

Git refuses to delete a branch that's checked out in a worktree, so finish and delete fail with a "used by worktree" error whenever a branch lives in a second worktree. This adds opt-in cleanup: pass --remove-worktree (or set gitflow.<type>.finish.remove-worktree / gitflow.<type>.delete.remove-worktree) and git-flow removes that worktree right before deleting the branch. A dirty worktree is left alone unless you also pass --force-remove-worktree.

Relates #175

I actually didn't know of this particular Issue #175 until I had already completed the branch and tested, then I saw the conversation. Sorry about that. But on looking into the proposed solution, I took a slightly different, inverse route. I think an opt-in approach might be more prudent to deleting worktrees by default.

I am currently running this change on my machine and it's working fine.

Why opt-in

#175 proposes automatic removal by default. I went the other way on purpose — a worktree is a workspace, and it often holds local state you still want (editor buffers, build artifacts, notes, scratch files). Removing it silently as part of a routine finish feels too destructive. So this keeps today's behavior as the default and makes cleanup an explicit choice, either per command or per branch type via config.

A few intentional differences from #175:

  • Default is off, not on. You opt in with a flag or a config key.
  • No --keep-worktree / detached-HEAD behavior. If you want the worktree gone, it's gone; if you don't, don't enable it.
  • No cd: navigation when you're sitting inside the worktree being removed. I haven't wired in the shell-integration protocol from Worktree navigation: checkout and shell-init #174.
  • finish checks an enabled, non-forced worktree removal before its merge flow begins, so a dirty worktree leaves the branch, remote branch, and target branch unchanged.

Testing

  • Git-layer tests for locating a linked worktree, excluding the current worktree, removing a clean one, and refusing a dirty one without --force-remove-worktree.
  • finish tests: opt-in cleanup, default unchanged, --no-remove-worktree overriding config, --remove-worktree enabling it, dirty worktree erroring with actionable guidance, and force removal completing.
  • delete tests: opt-in cleanup, default unchanged, and dirty-worktree protection.
  • Before the dirty-worktree preflight follow-up, the targeted worktree tests, go build ./..., and go vet ./... passed; full validation of this branch is pending the isolated Git test-environment work.
  • Built and installed the binary, then finished a real feature held by a linked worktree with --remove-worktree — the worktree and branch were removed cleanly after the merge.

Review focus

  • cmd/worktree_cleanup.go — shared helper: finds the worktree, removes it, prints progress, and turns the dirty-worktree case into an actionable error.
  • internal/git/repo.go — WorktreeForBranch (porcelain parsing) and RemoveWorktree.
  • internal/config/resolver.go, cmd/topicbranch.go, cmd/shorthand.go — flag and config precedence.
  • cmd/finish.go and cmd/delete.go — where removal sits in the flow, right before local branch deletion.

Allow finish and delete to remove a linked worktree before deleting
its branch when explicitly enabled by flag or branch-type config.

Preserve existing behavior by default. Refuse to remove dirty
worktrees unless force removal is explicitly requested.

Relates gittower#175

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds opt-in linked worktree cleanup to finish and delete, allowing a topic branch to be deleted even when it’s checked out in a linked worktree (by removing that worktree first), while keeping the current default behavior unchanged unless explicitly enabled via flags or gitflow.<type>.* config.

Changes:

  • Add Git-layer support to locate a linked worktree for a branch, detect dirty state, and remove the worktree.
  • Wire --remove-worktree / --force-remove-worktree (and corresponding config keys) into finish and delete, including a finish preflight to avoid merging when cleanup would later refuse.
  • Add integration tests for finish/delete behavior and update manpages + docs/gitflow-config.5.md for the new options.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/internal/git/worktree_test.go Adds Git-layer tests for worktree lookup, dirty detection, and removal.
test/cmd/finish_worktree_test.go Adds end-to-end tests covering finish behavior with linked worktrees (opt-in, override, dirty/force).
test/cmd/delete_worktree_test.go Adds end-to-end tests covering delete behavior with linked worktrees (opt-in, dirty protection).
internal/git/repo.go Introduces WorktreeForBranch, RemoveWorktree, and WorktreeHasChanges Git wrapper methods.
internal/config/resolver.go Extends finish option resolution to include remove/force-remove worktree booleans.
cmd/worktree_cleanup.go Adds shared helper logic for locating/removing linked worktrees and producing actionable dirty-worktree errors.
cmd/topicbranch.go Plumbs new flags through branch-type commands and updates retention option wiring.
cmd/shorthand.go Plumbs new flags through shorthand finish/delete commands.
cmd/finish.go Adds preflight for dirty worktrees and integrates worktree cleanup into the branch deletion step.
cmd/delete.go Adds optional worktree cleanup before local branch deletion.
docs/gitflow-config.5.md Documents new gitflow.<type>.{finish,delete}.{remove-worktree,force-remove-worktree} keys.
docs/git-flow-finish.1.md Documents new finish flags and adds an example for finishing a branch held by a worktree.
docs/git-flow-delete.1.md Documents new delete flags and adds a configuration example section.
docs/git-flow-config.1.md Adds the new keys to the config command’s documented key list.

Comment thread docs/gitflow-config.5.md
Comment on lines +448 to +456
**gitflow.*type*.finish.remove-worktree**
: Remove a linked worktree holding the branch before the branch is deleted on finish. Git refuses to delete a branch checked out in a linked worktree; enabling this removes that worktree first so the finish can complete. Corresponds to `--remove-worktree`/`--no-remove-worktree`.
: *Type*: boolean
: *Default*: false

**gitflow.*type*.finish.force-remove-worktree**
: Force-remove a linked worktree that has uncommitted or untracked changes (those changes are lost) instead of failing. Only meaningful when `gitflow.*type*.finish.remove-worktree` is enabled. Corresponds to `--force-remove-worktree`/`--no-force-remove-worktree`.
: *Type*: boolean
: *Default*: false
Comment thread internal/git/repo.go
Comment on lines +253 to +258
output, err := r.gitCmd(args...).CombinedOutput()
if err != nil {
return fmt.Errorf("failed to remove worktree at '%s': %s", path, strings.TrimSpace(string(output)))
}
return nil
}
@alexrinass

Copy link
Copy Markdown
Contributor

Thanks for this — the opt-in instinct is right, and the concern behind it is the same one raised in #175, so I've replied there with where I'd like the design to land: git-flow tracks which worktrees it created and only removes those, while worktrees you made yourself are detached rather than removed. That avoids both a destructive default and a finish that fails after the merge.

Let's keep the design discussion in #175 so it stays in one place, and use this PR for the code once the default is settled. Holding off on a full review until then, since the outcome changes the shape of the flag and config surface.

@alexrinass alexrinass 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.

The design discussion in #175 has settled and the worktree specs are updated, so the requirements this PR implements have changed. Requesting changes on that basis rather than on the code — the part that moved is the surface this PR exposes, so a detailed code review would be reviewing something we already know has to change.

What changed

git-flow removes only the worktrees it created itself, recorded by a marker written at creation time and never inferred from the path. A worktree you created yourself is never removed: finish and delete detach its HEAD off the branch, delete the branch, and leave the directory and everything in it exactly as it was. That replaces the opt-in flags and the four gitflow.<type>.<command>.* keys with --keep-worktree and --force-worktree, and it removes the reason to keep cleanup off by default — the destructive case is gone, while someone using a worktree for a single feature still gets a finish that completes instead of failing after the merge. Full text in #175, with the shared model in #171 and #172.

What carries over from your branch: the dirty-worktree preflight, the rule that a dirty worktree is never removed without an explicit force, and the git-layer helpers for locating a branch's worktree and detecting changes.

Where that leaves this PR

The marker has to be written where the worktree is created, so cleanup now sits on top of #172 (foundation, git layer, provenance) and #173 (start), and neither is merged. I'll be working on those next. Rather than reshaping this branch blindly against specs whose foundation isn't in place yet, I'd wait for them to land and then re-express the cleanup on top — less wasted effort, and the flag and config surface will be settled by then. Glad to have you pick it up again at that point if you want it.

@AlextheYounga

Copy link
Copy Markdown
Contributor Author

Sounds good. I’ll hold off until #172 and #173 land

@alexrinass

Copy link
Copy Markdown
Contributor

Thanks for this, and apologies it sat for a while — worktree support turned into a larger piece of work and this PR ended up overlapping the middle of it.

Since this was opened, epic #171 has landed four specs: #172 added the worktree foundation (path templating, a provenance marker, a Git worktree operations layer, and a git flow worktree command), #174 added checkout navigation and shell-init, #173 added worktree creation on start, and #176 added worktree status to list. That changed the ground under this PR in two ways worth explaining.

The first is mechanical. This PR adds WorktreeForBranch, RemoveWorktree, and WorktreeHasChanges as methods on *Repo in internal/git/repo.go, and all three now exist on main in internal/git/worktree.go. GitHub still reports the branch as mergeable because the declarations are in different files, but the merge would not compile — and WorktreeForBranch also has a different signature now, returning a *WorktreeEntry rather than a path string.

The second is a design difference. The epic settled on git-flow removing only the worktrees it created itself, recorded through a gitflow.worktree.<branch>.managed marker written at creation time. When a branch has a worktree git-flow did not create, the intended behavior is to detach that worktree's HEAD and delete the branch, leaving the directory and any uncommitted work untouched. This PR removes whichever worktree holds the branch, which is the behavior the marker was introduced to avoid.

The cleanup work itself is specified in #175, which is not currently being worked on, so this area is unowned rather than taken.

If you would like to carry it forward, the shape that would fit is to build on the existing helpers in internal/git/worktree.go and internal/worktree/ rather than adding parallel ones, and to gate removal on the provenance marker with the detach path for unmarked worktrees. Happy to answer questions on either. If you would rather not, that is completely fine — say so and we will close this with credit to your work, and reuse what fits when #175 is picked up.

@alexrinass

Copy link
Copy Markdown
Contributor

Closing this per the review response window in CONTRIBUTING.md — it's been over a week since my last comment with no response, and in the meantime the worktree epic (#172–#176) landed and made this branch's code no longer apply cleanly.

I've opened #245 as a successor, built on the design settled across this thread and #175 (free only the worktrees git-flow itself created, detach the rest), and credited you via Co-authored-by on the commits. Feel free to take a look if you'd like.

@alexrinass alexrinass closed this Sep 7, 2026
@AlextheYounga

Copy link
Copy Markdown
Contributor Author

Hey @alexrinass, terribly sorry for late response here. It's been a busy week. I very much appreciate the co-author commits, you are a man of high integrity, and I am glad to have my name on this great project!

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.

3 participants