pre-commit: How to run hooks during a rebase

pre-commit uses Git’s hook system to run tools when you commit. Unfortunately, Git doesn’t run any hooks when making a commit during a rebase. This can lead to you rebasing a branch and not realizing some code needs fixing, at least not until your CI system runs pre-commit (say, with pre-commit.ci).
Thankfully there’s a workaround, using git rebase
’s -x
option:
$ git rebase -x 'pre-commit run --from-ref HEAD~ --to-ref HEAD' main
-x
takes a command to run after each commit during the rebase. Here we tell it to invoke pre-commit run
on the files changed in the previous commit.
If you use interactive mode with -i
, you’ll see exec
lines in the rebase file:
pick 645d2a1 Honk
exec pre-commit run --from-ref HEAD~ --to-ref HEAD
# Rebase 01a5cd0..645d2a1 onto 01a5cd0 (2 commands)
#
# Commands:
...
There’s one after each command, as Git will execute the command between commits. If you know some commits will fail pre-commit hooks, you can remove the corresponding exec
lines.
When all goes well
Here’s what this looks like for a successful rebase:
$ git rebase -x 'pre-commit run --from-ref HEAD~ --to-ref HEAD' main
Executing: pre-commit run --from-ref HEAD~ --to-ref HEAD
black....................................................................Passed
Successfully rebased and updated refs/heads/honk.
Brilliant.
With multiple commits, you’ll see pre-commit running aftr each:
$ git rebase -x 'pre-commit run --from-ref HEAD~ --to-ref HEAD' main
Executing: pre-commit run --from-ref HEAD~ --to-ref HEAD
black....................................................................Passed
Executing: pre-commit run --from-ref HEAD~ --to-ref HEAD
black................................................(no files to check)Skipped
Successfully rebased and updated refs/heads/honk.
This can end up being quite a lot of output if you have many hooks and many commits. But at least explicit is better than implicit!
When a hook fails
If any hook fails, pre-commit run
fails. Git will then stop the rebase. This looks like:
$ git rebase -x 'pre-commit run --from-ref HEAD~ --to-ref HEAD' main
Executing: pre-commit run --from-ref HEAD~ --to-ref HEAD
black....................................................................Failed
- hook id: black
- files were modified by this hook
reformatted example.py
All done! ✨ 🍰 ✨
1 file reformatted.
error: cannot rebase: You have unstaged changes.
warning: execution failed: pre-commit run --from-ref HEAD~ --to-ref HEAD
and made changes to the index and/or the working tree
You can fix the problem, and then run
git rebase --continue
hint: Could not execute the todo command
hint:
hint: exec pre-commit run --from-ref HEAD~ --to-ref HEAD
hint:
hint: It has been rescheduled; To edit the command before continuing, please
hint: edit the todo list first:
hint:
hint: git rebase --edit-todo
hint: git rebase --continue
It’s a long message, but all the info you need is there. First, you can see pre-commit’s output, and spot which hook failed - in this case, Black. Then Git says error: cannot rebase
and explains what you can do next.
For a code formatter like Black, you probably want to amend the changes to the commit that was just rebased:
$ git add --patch
...
$ git commit --amend
black....................................................................Passed
[detached HEAD 372c69e] Honk
Date: Mon Nov 7 08:35:44 2022 +0000
1 file changed, 1 insertion(+)
create mode 100644 example.py
For other hooks like linters, you’ll need to edit the appropriate files before amending.
With the files fixed, you’re able to carry on with the rebase:
$ git rebase --continue
Executing: pre-commit run --from-ref HEAD~ --to-ref HEAD
black....................................................................Passed
Successfully rebased and updated refs/heads/honk.
And it’s done!
If you get lost at any stage, git status
will show you a bunch of hints. You can always abort the rebase with git rebase --abort
.
Alias it up
You can add a Git alias so you don’t need to type the full invocation each time. For example, to add it as prebase
(“pre-commit rebase”):
$ git config --global alias.prebase "rebase -x 'pre-commit run --from-ref HEAD~ --to-ref HEAD'"
This will add to your ~/.gitconfig
:
[alias]
prebase = rebase -x 'pre-commit run --from-ref HEAD~ --to-ref HEAD'
To use it:
$ git prebase -i main
Neato.
Learn more about pre-commit, particularly for Python projects, in my DX book.
One summary email a week, no spam, I pinky promise.
Related posts:
- pre-commit: Various Ways to Run Hooks
- Git: How to automatically stash while rebasing or merging
- Git: How to clean up squash-merged branches
Tags: pre-commit, git