pre-commit: How to run hooks during a rebase

Let’s make your rebases soar!

Update (2023-10-16): An improved version of this post’s content appears in my new book Boost Your Git DX.

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.

Fin

May your rebases go smoothly,

—Adam


Learn more about pre-commit in my Git DX book.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: ,