Skip to content

fix(worktree): offer Force Delete with a plain warning when a checkout has submodules - #28878

Merged
brennanb2025 merged 13 commits into
mainfrom
brennanb2025/sta-10391-submodule-delete
Oct 16, 2026
Merged

brennanb2025 merged 13 commits into
mainfrom
brennanb2025/sta-10391-submodule-delete

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​76 $\color{#cf222e}{\Huge{\mathbf{−}}}$​12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​64
Prod 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​147 $\color{#cf222e}{\Huge{\mathbf{−}}}$​17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​130

ELI5

Some workspace deletes failed with a raw Git error and no way forward when the workspace had git submodules set up. Git won't delete a checkout with submodules unless it's forced. Orca now explains that in plain words and offers the existing Force Delete button, with a warning that forcing also throws away anything in the submodules that was never pushed.

What Changed

Before: deleting a workspace whose checkout had an initialized submodule, through a delete that doesn't force, failed with Git's own text: "fatal: working trees containing submodules cannot be moved or removed". No Force Delete button appeared, so a user who had turned off the delete confirmation couldn't delete the workspace from the app. The deletes that don't force are: deleting with the confirmation turned off ("don't ask again"), the Space manager's delete, and orca worktree rm without --force. The ordinary delete confirmation dialog already forces, so it was not affected and is unchanged.

After: in the app, the failure toast and the Space manager row say: "This workspace contains submodules, so Git deletes it only with Force Delete. Force Delete also permanently removes anything in those submodules that was never pushed: commits, local branches and stashes." The Force Delete button is offered (the View button is hidden, since there are no changes to view), and clicking it deletes the workspace. Nothing is deleted without that click. The orca worktree rm output is unchanged.

The same warning also shows when the delete stops because the workspace has changed files and it has set-up submodules. That includes a commit made inside a submodule, which the parent sees as a changed vendor. Before, that case said only "It has changed files. Use Force Delete to delete it anyway.", and Force Delete then removed the never-pushed submodule commit with no warning. The View button stays available there, since there are changes to view. QA found this on the first build of this pull request.

After the Space manager deletes a workspace (its row's Force button, or a later delete), the failure toast for that workspace now closes. Before, it stayed on screen and still offered Force Delete for a workspace that no longer existed.

Also: Orca's dirty-file check before a delete now passes --ignore-submodules=none, so a repository or user setting that hides submodule changes can't make a workspace with edited submodule files look clean. Those deletes now stop before any terminal is closed, with Force Delete and the existing "It has changed files" message (or the submodule warning above when a submodule is what changed).

Why

Git refuses a non-forced git worktree remove for any checkout with an initialized submodule, even a clean one. The only way through is --force, and for a linked checkout --force also deletes the submodule's own repository (it lives inside that workspace's Git admin folder). Commits, local branches and stashes made inside the submodule and never pushed exist nowhere else, and git status in the parent can't see them. A delete that doesn't ask can't know whether it is about to destroy that work.

The first version of this pull request retried automatically with --force when git status was clean. Review reproduced, with real Git 2.50.1 and 2.25.1, that the retry permanently destroyed a submodule commit the parent branch still pointed at. #24602 had already removed the same automatic retry for that reason, and its real-Git guard test was later lost in #27215. Orca's rule is not to clear user data automatically to recover from an error, but to offer a button. So this version keeps Git's refusal and makes it actionable through the existing Force Delete path. It adds one Force Delete reason (submodules) beside the others in src/shared/worktree/removal.ts. It adds no new force mechanism and no extra Git command on every delete. Trying a delete first and offering Force Delete when Git refuses is the common pattern.

The refusal is recognized in the app's window from the error text (Git output is pinned to English on every host), so this also works when the workspace lives on an SSH host or paired server running an older version. Nothing on the wire changed.

For a workspace with changed files, Orca's own changed-files check refuses before Git does, so Git's submodule refusal never appears, and the parent's git status can't show commits that exist only inside a submodule. So only when that check is already refusing, it checks whether the workspace has a set-up submodule, the way Git's own refusal does: a path listed in .gitmodules (git config --file .gitmodules, works on Git 2.25) that holds a .git. If so, it reports a submodule refusal, with the changed paths still listed after it. Clean deletes run no extra command. Mixed versions (intended): that refusal still contains the "uncommitted or untracked changes" sentence, so an older app reads it as changed files and keeps offering Force Delete; an older host paired with this app shows the generic changed-files copy, as before.

#25979 is an open pull request with the same approach.

Linked Issue

Tracked in #28164 (Linear STA-10391).

Visual Proof

Remote Electron QA on the M4Air (macOS), built app, isolated home, every delete a real UI click, on head 00f2ba1 against the base on main.

Delete with the confirmation turned off, clean workspace with an initialized submodule. Before (main): red toast with Git's raw text, no Force Delete. After: plain warning with Force Delete.

Before (main) After (this PR)
main-f2-toast pr-f2-toast

After Force Delete, the workspace is gone:

pr-f2-after-force

Space manager delete. Before (main): row shows Git's raw text with no Force button. After: the plain warning and a Force button, which removed the workspace.

Before (main) After (this PR) After Force
main-f5-result pr-f5-result pr-f5-after-force

A real signed-in Claude CLI running in the workspace: same warning, and Force Delete removed it.

Before (main) After (this PR)
main-f7-toast pr-f7-toast

Unchanged and still working: delete through the confirmation dialog, an untracked file inside the submodule (refused with Force Delete; the file survived), and a plain workspace with a terminal open.

Dialog delete (this PR) Untracked file in submodule (this PR) Plain workspace (this PR)
pr-f1-result pr-f4-toast pr-f6-result

The first QA run found two things, both fixed and re-checked on the M4Air at d626edd (every flow passed):

A commit inside the submodule (the parent shows a changed vendor). Before the fix (00f2ba1) it said only "It has changed files"; now it shows the submodule warning, keeps View, and the commit survives the refusal.

Before the fix After
pr-f3-toast-before-fix F3-1-refusal-toast

A submodule commit already recorded in the workspace plus an unrelated new file: same warning. A repo without submodules and an edited file: the unchanged "It has changed files" copy.

Recorded submodule commit + new file No submodules (unchanged)
F3b-1-refusal-toast F3c-1-refusal-toast

Space manager: after the row's Force removes the workspace, its failure toast closes; only "Workspace deleted" remains.

Row failed, toast up After the row's Force
F5-1-row-failed F5-2-after-row-force

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • Real Git: a clean checkout whose submodule holds a commit that exists only there is refused without force; the checkout and the commit survive, the refusal is classified as the submodule Force Delete reason, and a forced delete then succeeds. This guards the Git output the app depends on.

  • Real Git: a workspace with a never-pushed submodule commit and an unrelated new file is refused by Orca's changed-files check as the submodule Force Delete reason, with the changed path still listed and the sentence older apps rely on still present.

  • The earlier unit test "preserves Git refusal even when parent status cannot reveal unpublished submodule commits" is back as it is on main.

  • Not run: a live WSL distro or a live SSH host. Git 2.25.1 was checked by hand during review: same refusal text, and the status flag works.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Author: @BrennanKB5

Known, unchanged from main, and out of scope here: the ordinary delete confirmation already forces, so it deletes never-pushed submodule work without this warning; on the deletes that don't force, Orca closes the workspace's terminals and agents before Git refuses; the workspace cleanup list still shows Git's raw text for any refused delete; orca worktree rm can report success for a delete that is refused later in the background; a delete failure toast still stays up when the workspace is deleted some other way than the Space manager (from the terminal, the cleanup list or another window); in the Space manager the failure toast can cover a failed row's Force button until it is closed (toasts share one screen position); and a workspace whose submodule history remains without a .gitmodules entry pointing at it (a deinitialized submodule, or .gitmodules removed) gets the generic changed-files message instead of the submodule warning, still with Force Delete.

Cross-platform: the Git arguments are the same on macOS, Linux and Windows. Folder workspaces are not Git checkouts and are unaffected.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Git refuses to remove any worktree with an initialized submodule unless --force is set, even when nothing is uncommitted. After that specific refusal, re-check the checkout including files inside the submodule and pass --force once. A dirty submodule still blocks the delete. A second --force is not added, because that would also clear a Git lock.
…modules

Drop the automatic --force retry. A forced removal deletes the submodule's
repository under .git/worktrees/<id>/modules, including commits, local
branches and stashes that were never pushed, and the parent's dirty check
cannot see them (the retry #24602 removed for the same reason).

Instead classify Git's "working trees containing submodules cannot be moved
or removed" refusal as a new Force Delete reason, 'submodules', after the
lock check and the force guard. The renderer classifies the error string,
so older SSH and paired hosts get it too with no wire change. The toast,
the delete dialog and the Space manager row show one plain warning naming
what Force Delete would lose instead of Git's text.

Keep --ignore-submodules=none in the pre-teardown dirty check so a user's
ignore setting cannot hide submodule edits from it.

Replace the retry test with one real-Git test: a clean checkout whose
submodule holds a local-only commit is refused without --force, stays
registered, keeps the commit, and its refusal classifies as 'submodules'.
…ew for it

The warning now says Git deletes the workspace only with Force Delete, the submodule
refusal no longer offers View (there are no changes to show), the real-Git test pins
that Force Delete then succeeds, and a comment no longer describes the removed retry.
…lete, and clear a stale delete toast

With a commit inside a submodule that the parent has not recorded, the parent sees `M vendor`, so
Orca's dirty check refused first with the generic "It has changed files". Force Delete then destroyed
the never-pushed submodule commit without warning. On a dirty refusal the check now asks
`git status --porcelain=v2` whether a changed entry is a submodule, and if so reports a refusal the
app shows with the submodule Force Delete warning. The detail keeps the dirty sentence, so older
apps still offer Force Delete.

After the Space manager deleted a workspace (its row Force, or a later delete), the failure toast for
that workspace stayed up and still offered Force Delete. A Space delete now dismisses it.
…out that has them

Review found the warning still vanished when a submodule commit was already recorded and some other
file was dirty: the parent's status shows no submodule change, yet Force Delete destroys the commit.
The changed-files check now asks whether the checkout has a populated submodule (a .gitmodules path
holding a .git, as Git's own refusal checks) instead of whether one changed. The refusal keeps the
changed paths after the detail, and View stays available since there are changes to view.
@brennanb2025

brennanb2025 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Review loop status: done

What this PR changes for users: deleting a workspace whose checkout has set-up Git submodules, through a delete that doesn't force (confirmation turned off, the Space manager), used to fail with Git's raw "working trees containing submodules cannot be moved or removed" and no way forward. Now the failure says in plain words that Force Delete is needed and that it also permanently removes anything in those submodules that was never pushed (commits, local branches, stashes), and offers Force Delete. Nothing is deleted without that click.

Review: four rounds, plus an architecture challenge and a design ruling.

  • The first version retried automatically with --force. Review reproduced, on real Git 2.50.1 and 2.25.1, that this destroyed a never-pushed submodule commit, so the automatic retry was dropped in favor of the Force Delete button.
  • QA then found two problems. A workspace with changed files and a set-up submodule got only the generic "It has changed files" message. And the Space manager left a stale failure toast behind after its row's Force succeeded. Both are fixed.
  • A round-4 reviewer found that the first fix missed a submodule commit that was already recorded plus an unrelated changed file. The check now asks whether the checkout has a set-up submodule, not whether one changed.
  • No P0, P1 or P2 findings remain open.

Deferred, listed in the PR body Notes (unchanged from main or rare):

  • The normal confirmation dialog already forces, with no submodule warning. That product question is with Brennan.
  • A refused delete has already closed the workspace's terminals and agent.
  • The cleanup list shows Git's raw text.
  • orca worktree rm can report success for a delete that is refused later.
  • A stale toast stays up after a delete from outside the Space manager.
  • A deinitialized submodule with kept history, plus a changed file, gets the generic copy.

QA: real Electron on the M4Air (macOS), built app, isolated home, every delete a real UI click.

  • First run (00f2ba1, against main): every main flow passed.
  • Re-run (d626edd, the same code as this head apart from a type-only refactor and the main merge): the submodule commit case, the recorded-commit-plus-new-file case, the no-submodule control, the clean submodule case, and the Space manager stale toast all passed.
Submodule commit: warning, View, Force Delete Space manager: toast closes after the row's Force
F3-1-refusal-toast F5-2-after-row-force

CI: the full PR workflow is green on this head after the final main merge.

QA ran on d626edd; later commits are main merges (latest a64df01) plus a reviewed conflict resolution, and two reviewed fixes for a CodeRabbit finding: the failure toast is now keyed by host and workspace, so a Space delete on one host can't close another host's toast for a workspace with the same id (66d6300, 2407678).

@brennanb2025
brennanb2025 marked this pull request as ready for review October 16, 2026 06:08
@brennanb2025 brennanb2025 changed the title fix(worktree): remove a clean checkout that contains submodules fix(worktree): offer Force Delete with a plain warning when a checkout has submodules Oct 16, 2026
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6594e31a-ece9-4dc4-a72b-d0a3b43fce30
📥 Commits

Reviewing files that changed from the base of the PR and between 2407678 and a64df01.

📒 Files selected for processing: 1
  • src/renderer/src/i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes: 1
  • src/renderer/src/i18n/locales/en.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.

📝 Walkthrough

The worktree-removal preflight now includes submodule changes in Git status checks and identifies populated submodules when constructing dirty-worktree errors. Shared classification recognizes submodule removal refusals. Renderer components display translated guidance, adjust failure-toast actions, and dismiss failure toasts for handled deletions. Tests cover submodule-only commits, dirty worktrees, and status command arguments.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to a64df

A forced deletion may lack the submodule-data warning, and deleting one workspace may dismiss another host’s failure notification. Resolve or explicitly accept these risks before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 13 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: offering Force Delete with a warning for worktrees that contain submodules.
Description check Passed The description is complete and follows the repository template. It explains the user impact, implementation, rationale, linked issue, visual proof, testing, known limitations, cross-platform consider…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is complete and follows the repository template. It explains the user impact, implementation, rationale, linked issue, visual proof, testing, known limitations, cross-platform considerations, and checklist status.

Full details: Docstring Coverage

Explanation

Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 13 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7993bd66-2f3d-4c52-8162-5526d6fecddf
📥 Commits

Reviewing files that changed from the base of the PR and between da099c3 and 41e03ce.

📒 Files selected for processing: 11
  • src/main/git/remove-worktree-clean-preflight.test.ts
  • src/main/git/worktree-removal-preflight.ts
  • src/main/git/worktree-removal-real-git.test.ts
  • src/renderer/src/components/sidebar/delete-worktree-failure-toast.tsx
  • src/renderer/src/components/sidebar/delete-worktree-toast.ts
  • src/renderer/src/components/sidebar/worktree-delete-error-display.ts
  • src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts
  • src/renderer/src/components/status-bar/workspace-space-worktree-row.tsx
  • src/renderer/src/i18n/locales/en.json
  • src/shared/git-command-failure-text.ts
  • src/shared/worktree/removal.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

…1-submodule-delete

# Conflicts:
#	src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8a2d3e4a-70da-4be0-8f71-536746f24475
📥 Commits

Reviewing files that changed from the base of the PR and between 41e03ce and a8002f1.

📒 Files selected for processing: 3
  • src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts
  • src/renderer/src/components/status-bar/workspace-space-worktree-row.tsx
  • src/renderer/src/i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes: 1
  • src/renderer/src/i18n/locales/en.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@brennanb2025
brennanb2025 merged commit 23b60a5 into main Oct 16, 2026
33 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/sta-10391-submodule-delete branch October 16, 2026 22:23
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.

1 participant