stgit icon indicating copy to clipboard operation
stgit copied to clipboard

feat: add --exec option to run commands after each patch during rebase

Open notactuallytreyanastasio opened this issue 1 month ago • 0 comments

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.

Screenshot 2025-12-11 at 7 32 21 PM

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 --exec options (run in sequence)
  • Commands run in user's $SHELL (or sh as fallback)
  • Graceful handling of command failures with transaction rollback

Implementation Approach

Design Decision: Modular Architecture

We considered two approaches:

  1. Handle --exec in rebase.rs only - simpler, more localized
  2. 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_exec method could be reused by other commands in the future
  • It follows the existing pattern in the codebase where transaction operations are encapsulated

Code Changes

  1. src/stupid/context.rs: Added exec_cmd() method to run shell commands via the user's $SHELL
  2. src/stack/transaction/mod.rs: Added push_patches_with_exec() method that pushes patches and runs exec commands after each successful push
  3. src/stack/transaction/ui.rs: Added print_exec() method for user feedback
  4. src/cmd/rebase.rs: Added --exec / -x argument 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 push patches 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