Skip to content

refactor(error): introduce CommandError typed leaf for buffered git failures#2580

Merged
max-sixty merged 4 commits intomainfrom
multiline-error-debug-assert
May 4, 2026
Merged

refactor(error): introduce CommandError typed leaf for buffered git failures#2580
max-sixty merged 4 commits intomainfrom
multiline-error-debug-assert

Conversation

@max-sixty
Copy link
Copy Markdown
Owner

Summary

Builds on #2567 by replacing the leaf bail at the buffered command boundary with a typed error.

Repository::run_command and WorkingTree::run_command now return Err(CommandError { program, args, stderr, stdout, exit_code }) instead of bail!(\"{stderr}\"). The structured form lets the renderer separate a single-line summary (Display) from the multi-line captured payload (combined_output()), and lets callers that embed raw stderr in higher-level errors (RebaseConflict, WorktreeRemovalFailed, PushFailed, list-task errors) read it directly instead of round-tripping through e.to_string().

The debug_assert! that triggered #2564 in debug builds is now a meaningful invariant: it flags only multi-line bails that aren't typed as CommandError, instead of acting as a proxy for "developer remembered .context(...)."

What's in the diff

  • worktrunk::git::CommandError (new) — typed leaf with combined_output() and a single-line Display.
  • worktrunk::git::find_command_error / display_message — chain walkers for renderers and embedders.
  • print_command_error refactored to format_command_error -> String (testable) with a downcast branch that preserves intermediate .context(...) entries above the captured body.
  • Repository::extract_failed_command extended to recognize CommandError alongside StreamCommandError.
  • 5 user-facing serialization sites migrated to display_message so wrappers like WorktreeRemovalFailed.error and PushFailed.error keep showing git's real stderr (rather than the new single-line summary).

Reviewer orientation

  • src/git/error.rsCommandError, find_command_error, display_message.
  • src/git/repository/mod.rs, src/git/repository/working_tree.rs — leaf cutover.
  • src/main.rs — renderer; the new branch and the trim-then-anstream print_command_error were both flagged in iteration.
  • src/commands/{step_commands,worktree/push,list/collect/{mod,tasks}}.rs, src/output/handlers.rs — call-site migrations.

Notes

Test plan

  • cargo run -- hook pre-merge --yes — 3480 tests pass, all lints, all docs
  • Codex iteration: P2 (preserve raw stderr in typed wrappers) and P3 (preserve context chain in renderer) addressed; tests added for both
  • CI: linux/macos/windows test jobs green

This was written by Claude Code on behalf of @max-sixty

…ailures

Building on #2567's `.context(...)` patch, the buffered command wrappers
(`Repository::run_command`, `WorkingTree::run_command`) now return
`Err(CommandError { program, args, stderr, stdout, exit_code })` instead
of `bail!("{stderr}")`. The structured leaf separates the single-line
summary (Display) from the captured payload (`combined_output()`), so
multi-line stderr renders cleanly regardless of whether `.context(...)`
was added at the propagation site, and callers that embed raw stderr in
higher-level errors (`RebaseConflict`, `WorktreeRemovalFailed`,
`PushFailed`, the list-task error path) read it directly instead of
round-tripping through `e.to_string()`.

The `debug_assert!` that triggered #2564 in debug builds is now a
meaningful invariant: it flags only multi-line bails that aren't typed
as `CommandError`, instead of acting as a proxy for "developer
remembered `.context(...)`."

Key changes:

- `worktrunk::git::CommandError` (new) — typed leaf with
  `combined_output()` and a single-line `Display`.
- `worktrunk::git::find_command_error` / `display_message` — chain
  walkers for renderers and embedders.
- `print_command_error` refactored to `format_command_error -> String`
  (testable) plus a downcast branch that preserves intermediate
  `.context(...)` entries above the captured body.
- `Repository::extract_failed_command` extended to recognize
  `CommandError` alongside `StreamCommandError`.
- 5 user-facing serialization sites migrated to `display_message` so
  wrappers like `WorktreeRemovalFailed.error` and `PushFailed.error`
  keep showing git's real stderr.

Notes:

- `.context(...)` calls added in #2567 are no longer load-bearing for
  correctness — they remain as semantic UX. Pruning the mechanical ones
  is a follow-up.
- Method named `combined_output` rather than `output` because the
  `no-direct-cmd-output` pre-commit hook regex flags `.output()` to
  keep callers off `std::process::Command::output`.

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@worktrunk-bot worktrunk-bot left a comment

Choose a reason for hiding this comment

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

One missed migration site: handle_detached_removed_worktree_output in src/output/handlers.rs:1268 still does error: err.to_string() for GitError::WorktreeRemovalFailed, while the named-removal sibling at line 1344 was updated to display_message(&err). Both call remove_worktree_with_cleanup, which now produces a CommandError-bearing error via Repository::remove_worktree / delete_branch_if_safe — so for detached-HEAD removals the user now sees "git worktree remove --force /path failed (exit 128)" instead of git's actual stderr. The docstring on display_message even names WorktreeRemovalFailed as the canonical case for this helper.

Happy to push a one-line fix if you'd like.

Reviewer caught one missed migration (`handle_detached_removed_worktree_output`
at handlers.rs:1268) — the prior `replace_all` matched only the
sibling at L1344 because their indentation differed. Audit also found
the branch-deletion error path at L452, which similarly stringifies a
`run_command` error for direct gutter rendering. Both now go through
`worktrunk::git::display_message` so the user sees git's actual stderr
rather than the `CommandError` single-line summary.

Co-Authored-By: Claude <noreply@anthropic.com>
max-sixty and others added 2 commits May 4, 2026 00:41
Pre-commit fmt complained the inlined `eprintln!` with `display_message`
exceeded the line limit. Reformat to multi-line.

Co-Authored-By: Claude <noreply@anthropic.com>
Two trivial unit tests targeting codecov-flagged lines that didn't have
direct coverage:

- `command_error_command_string_handles_empty_args` exercises the
  args-empty branch of `CommandError::command_string()` (returns just
  the program name).
- `renders_command_error_with_empty_body` exercises the gutter
  assembly's empty-body fallback: a CommandError wrapped in `.context()`
  with no captured stderr/stdout falls back to the single-line summary
  rather than rendering an empty gutter.

Co-Authored-By: Claude <noreply@anthropic.com>
@max-sixty max-sixty merged commit 32e165b into main May 4, 2026
25 of 26 checks passed
@max-sixty max-sixty deleted the multiline-error-debug-assert branch May 4, 2026 08:31
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