Files
orca/src/shared/git-command-failure-text.ts
T
Brennan Benson 23b60a5d07 fix(worktree): offer Force Delete with a plain warning when a checkout has submodules (#28878)
* fix(worktree): remove a clean checkout that contains submodules

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.

* fix(worktree): offer Force Delete for a checkout Git refuses over submodules

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'.

* fix(worktree): clarify the submodule Force Delete warning and hide View 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.

* fix(worktree): name submodules when a changed submodule blocks the delete, 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.

* fix(worktree): warn about submodules on any refused delete of a checkout 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.

* test(worktree): mock the .gitmodules probe as missing in the preflight unit tests

* refactor(worktree): attach the dirty-check status without a type assertion

* fix(worktree): key the delete failure toast by host and worktree id

A Space delete on one host must not dismiss another host's failure toast for a worktree with the same id.

* fix(worktree): dismiss the failure toast by the store row's host after a Space row Force
2026-10-16 15:23:13 -07:00

40 lines
1.5 KiB
TypeScript

/**
* The text a failed Git invocation left behind, for the predicates that classify a
* failure by what Git said.
*
* Why all three streams and not just `message` + `stderr`: the errors Orca classifies
* do not all come straight out of `execFile`. Node puts Git's stderr in both `message`
* and `stderr`, but Orca also throws its own failures with the Git output on `stdout`
* (the removal dirty check attaches `git status --porcelain` output that way). Reading
* all three is what keeps the classifiers from disagreeing about the same error object.
*
* Against a real binary this reads no differently: Git emits every refusal this module
* classifies through `error()`/`die()`, i.e. stderr only, on 2.25 through 2.55.
*/
export function readGitCommandFailureText(error: unknown): string {
if (typeof error !== 'object' || error === null) {
return String(error)
}
const parts: string[] = []
for (const field of ['message', 'stderr', 'stdout'] as const) {
const value = (error as Record<string, unknown>)[field]
if (typeof value === 'string' && value) {
parts.push(value)
}
}
return parts.join('\n')
}
export function readGitCommandFailureStderr(error: unknown): string | null {
if (typeof error !== 'object' || error === null || !('stderr' in error)) {
return null
}
if (typeof error.stderr === 'string') {
return error.stderr
}
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(error.stderr)) {
return error.stderr.toString('utf8')
}
return null
}