Skip to content

fix(box): worktrees with submodules can be removed when nothing would be lost - #197

Merged
pumfleet merged 10 commits into
mainfrom
most-of-my-worktree
Oct 27, 2026
Merged

pumfleet merged 10 commits into
mainfrom
most-of-my-worktree

Conversation

@pumfleet

@pumfleet pumfleet commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

git refuses to remove any worktree with a submodule checked out, however clean ("working trees containing submodules cannot be moved or removed"). Only --force gets past it, so in repos with submodules every removal or archive failed, and the app showed a dead-end "Couldn't remove" (its "Remove anyway" toast only matches git's dirty-files wording).

RemoveWorktree (internal/box/locations.go) now catches that refusal on an unforced removal, runs the check git skips, and forces only when nothing would be lost:

  • Changes: git status --porcelain --untracked-files=normal --ignore-submodules=none on the worktree, plus status inside each checked-out submodule, so neither status.showUntrackedFiles nor a submodule's own config can hide anything.
  • History: every submodule repository the removal would delete (the ones under the worktree's modules/, deinitialized ones included, nested ones too, and a checked-out submodule's own embedded .git) is checked with rev-list --all --not --remotes. That covers commits that only a branch, a tag or the stash holds.
  • If git can't answer either check, the worktree is kept.

When something would be lost, the box answers 409 in git's usual "…, use --force to delete it" form. The app keeps the worktree and offers Remove anyway, and its toast says "commits or stashes no remote has" rather than "uncommitted changes" when that's the reason.

Testing

  • New TestAWorktreeWithSubmodulesIsRemovedWhenNothingWouldBeLost covers each case that must keep the worktree, then checks that a clean one with a deinitialized submodule is removed. The cases: a file in a submodule, the same with untracked files hidden by config, a file in the worktree hidden by config, a submodule stash, an uncommitted and then a committed-but-unpushed submodule commit, a commit only a local tag holds, a branch in a deinitialized submodule, and a commit in a hand-cloned (embedded) submodule. With the embedded lookup turned off, the test fails at that case.
  • go vet ./..., go test -race ./..., cargo check, all the app checks, pnpm test, pnpm build, npx tsc -p e2e and the docs build pass.
  • TestReviewRoutesOverTheWire flakes on TempDir cleanup ("directory not empty"), 3 runs in 10 on both this branch and origin/main. It's unrelated to this change and left alone.

Review

Five Codex review rounds, each by a fresh reviewer:

  1. Fixed (P1): status.showUntrackedFiles=no hid untracked files from the check. Fixed with --untracked-files=normal and status in each submodule. Fixed (P1): submodule stashes weren't checked.
  2. Fixed (P1): deinitialized submodules keep their repository in the worktree's modules/, where foreach never looks. Every repository there is now walked. Fixed (P1): commits that only a local tag holds were missed. The check now uses --all instead of HEAD --branches.
  3. Fixed (P1): a submodule's embedded .git (made with git clone) wasn't checked. Each checked-out submodule's own git dir is now included.
  4. Partly rejected (P2): a local-only tag on an already-pushed commit is removed without asking. Telling a local tag from a fetched one would mean a git ls-remote (network, maybe auth) on every removal, and offline it would refuse every submodule worktree again. Such a tag holds no commits that aren't already on a remote, the same as a local branch on pushed history, whose name is also not kept. The fixed part: the message, toast and changelog had promised to keep "tags", and now say exactly what is kept.
  5. Clean.

After the PR opened:

  • cubic, P1 (rejected, maintainer agreed): ignored files aren't counted, so a submodule worktree whose only extras are ignored files is removed without asking. I left this as it is because it matches git exactly: a plain git worktree remove deletes such a worktree (reproduced), and Shipyard's ordinary path does too. Counting ignored files would refuse nearly every real worktree (node_modules, build output).
  • cubic, P2 (fixed): a "could not be checked" refusal got a plain error with no way forward. It now gets the Kept toast with Remove anyway (keptReason, unit-tested).
  • Codex round 6, P1 (fixed): a deinitialized submodule nested inside a hand-cloned one keeps its repository under that clone's own .git/modules, which nothing scanned. Those are now walked too. A new test fails without the fix.
  • Codex round 7, P2 (fixed): a nested submodule checked out at a different commit could be hidden by submodule.<name>.ignore=all in the submodule above it. Each submodule's own status now uses --ignore-submodules=none. A test covers it and fails without the fix.
  • Codex round 8: clean.
  • cubic, P2 (fixed): keptReason matched bare words (contains, modified), so an unrelated failure could have offered Remove anyway. It now matches the refusal's own words.
  • Merged origin/main. Codex round 9: clean.

🤖 Generated with Claude Code

View guided diff Turn on auto-fix

pumfleet and others added 5 commits October 27, 2026 11:03
… be lost

git refuses to remove any worktree with a submodule checked out, however
clean, so removing or archiving one always failed. On that refusal,
RemoveWorktree now checks for the work git won't (changes in the worktree
or its submodules, submodule commits no remote has) and forces the removal
only when there is none. Otherwise it answers 409 in git's words, so the
app keeps the worktree and offers Remove anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd stashes

Address Codex review (round 1):
- the safety check counts untracked files even when status.showUntrackedFiles
  hides them, in the worktree and in each submodule (each submodule is now
  asked itself, so its own config can't hide a change)
- a submodule stash counts as work to keep, since it lives only in the
  worktree's copy of the submodule's history

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d deinitialized submodules' history

Address Codex review (round 2):
- every submodule repository under the worktree's git folder is checked,
  not only checked-out ones: a deinit leaves the repository (and its
  branches) behind, and the removal would delete it
- each one is checked against all its local refs (branches, tags, stashes,
  HEAD), so a commit only a local tag holds counts as work to keep

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odule's own .git

Address Codex review (round 3):
- a checked-out submodule's own git folder is checked too, so one cloned by
  hand (its repository embedded in the worktree's folder, not under the
  worktree's modules) can't lose commits no remote has

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address Codex review (round 4):
- the refusal, the app's toast and the changelog promised to keep "tags";
  what is kept is commits no remote has (one only a tag holds included)
  and stashes. A local tag's name on history a remote has isn't checked:
  that would take asking the remote on every removal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
berth Ready Ready Preview Oct 27, 2026 6:09pm UTC
berth-docs Ready Ready Preview Oct 27, 2026 6:09pm UTC

Request Review

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread internal/box/locations.go
Comment thread app/src/lib/remove-worktree.ts Outdated
…fers remove anyway

Address cubic review on #197:
- a refusal because the box couldn't check a worktree with submodules got
  a plain error with no way forward; it now gets the Kept toast with
  Remove anyway, in its own words rather than "uncommitted changes"
- which refusal says what is now keptReason, with a unit test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread app/src/lib/kept-reason.ts Outdated
…hand-cloned ones

Address Codex review (round 6):
- a hand-cloned submodule keeps its own submodules' repositories under its
  own .git/modules; a deinitialized one there is no longer skipped, so its
  unpushed branches and stashes keep the worktree

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ignore setting

Address Codex review (round 7):
- each submodule's own status now overrides submodule.<name>.ignore, so a
  nested submodule checked out at another commit (an uncommitted change)
  keeps the worktree even when the submodule above it ignores it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pumfleet and others added 2 commits October 27, 2026 13:07
Address cubic review on #197:
- keptReason matched bare words (contains, modified, untracked), so an
  unrelated removal failure could be called uncommitted changes and offer
  a force; it now matches the refusal's own words, "modified or untracked
  files", which git and the box both use

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pumfleet
pumfleet merged commit 9278c1e into main Oct 27, 2026
9 checks passed
@pumfleet
pumfleet deleted the most-of-my-worktree branch October 27, 2026 18:41
@linear-code

linear-code Bot commented Oct 27, 2026

Copy link
Copy Markdown

ENG-6967

This branch was successfully deployed

2 active deployments
Preview – berth-docs — ed54f4cd Deployed Oct 27, 2026 by vercel[bot]
Preview – berth — ed54f4cd Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci Run CI on this pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant