pre-commit: How to run hooks during a rebase

Let’s make your rebases soar!

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 how to make your tests run quickly in my book Speed Up Your Django Tests.


Subscribe via RSS, Twitter, Mastodon, or email:

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

Related posts:

Tags: ,