feat: add --exec option to run commands after each patch during rebase
Add --exec option to run commands after each patch during rebases
Disclaimer
I wrote this pull request with the assistance of Claude Opus 4.5. I can note this in the commit log if that is a desirable flag.
I am an experienced programmer and do not think this is slop, I have tried to actually implement this in a reasonable/sane manner.
I used a tool I made called deciduous while writing this and it provides a neat flow diagram of how it made the choices that are implemented, so I am including that just for fun.
The PR
This PR implements the --exec option for stg rebase, as requested in #469. It allows running a shell command after each patch is successfully applied during a rebase operation, similar to git rebase --exec.
Key features:
- Execute shell commands after each patch push during rebase
- Support multiple
--execoptions (run in sequence) - Commands run in user's
$SHELL(orshas fallback) - Graceful handling of command failures with transaction rollback
Implementation Approach
Design Decision: Modular Architecture
We considered two approaches:
-
Handle
--execin rebase.rs only - simpler, more localized -
Add exec callback to
transaction/push_patches- more modular, reusable
We chose Option 2 (modular approach) because:
- It keeps the transaction logic cohesive
- The
push_patches_with_execmethod could be reused by other commands in the future - It follows the existing pattern in the codebase where transaction operations are encapsulated
Code Changes
-
src/stupid/context.rs: Addedexec_cmd()method to run shell commands via the user's$SHELL -
src/stack/transaction/mod.rs: Addedpush_patches_with_exec()method that pushes patches and runs exec commands after each successful push -
src/stack/transaction/ui.rs: Addedprint_exec()method for user feedback -
src/cmd/rebase.rs: Added--exec/-xargument with appropriate conflicts
Design Discussion Point: Failure Behavior
When an exec command fails, the entire transaction is rolled back (no patches remain applied). This differs from git rebase --exec which leaves you at the failing commit to fix things.
Current behavior (rollback):
$ stg rebase --exec "make test" master
+ patch1
Executing: make test
error: `make test` exited with code 1
$ stg series
- patch1 # Rolled back to unapplied
- patch2
Git's behavior (partial state):
$ git rebase --exec "make test" master
Executing: make test
error: ...
# You're left at patch1 with a dirty worktree
Rationale for rollback:
- Consistent with how stgit transactions work (atomic operations)
- Safer - no partial/dirty state left for the user to clean up
- User can always
stg pushpatches one at a time with manual checks
However, you / the community may prefer git's behavior for consistency. I'm happy to discuss and modify this if desired.
Personally, I have never wanted to keep the mid-run state that comes with an issue when running this command, so I just went with my own personal preference here. It would not be hard to change, let me know what you think @jpgrayson @fbenkstein
Usage Examples
# Run tests after each patch
stg rebase --exec "cargo test" master
# Multiple commands
stg rebase --exec "cargo fmt --check" --exec "cargo test" master
# Complex shell commands work
stg rebase --exec "make && make test" master
Testing
- Added comprehensive test suite in
t/t2206-rebase-exec.sh - Tests cover: basic exec, multiple exec, failure rollback, conflict with --nopush/--interactive
- All 7 tests pass
- Manual testing performed using the feature itself during development
Manual Test Output
$ stg rebase --exec "echo First" --exec "echo Second" HEAD
- patch1..patch2
info: Rebasing to ...
+ patch1
Executing: echo First
First
Executing: echo Second
Second
+ patch2
Executing: echo First
First
Executing: echo Second
Second
> patch2
Implements: #469