Run a shell command on every revision in a Git repository

By on

Git’s rebase command takes an --exec option to run a shell command on every revision in the rebase. For example, to run a formatter like Prettier1 over each file in your repository for every past revision2:

git rebase -i --exec 'prettier --write {**/*,*}.js' ffcfe45

Being an interactive rebase, Git opens up your $EDITOR to show the actions that are about to be executed. Although this rebase spans nine commits, there are eighteen actions as the commands are run as separate steps:

pick 22b042f npm install --save-dev mocha
exec prettier --write {**/*}.js
pick 76995a2 echo node_modules >> .gitignore
exec prettier --write {**/*}.js
pick 85d9d77 Use mocha as test script
exec prettier --write {**/*}.js
pick cfa165c Add failing test for crush.crush()
exec prettier --write {**/*}.js
pick 93cfd2a Minify with cssnano
exec prettier --write {**/*}.js
pick c9dcfb4 Add failing test for purging
exec prettier --write {**/*}.js
pick 1703abd npm install --save-dev @fullhuman/postcss-purgecss postcss
exec prettier --write {**/*}.js
pick 5faa328 Purge css with purgecss
exec prettier --write {**/*}.js
pick bc787ae Add bin/crush.js
exec prettier --write {**/*}.js

# Rebase ffcfe45..bc787ae onto 5faa328 (18 commands)
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
# These lines can be re-ordered; they are executed from top to bottom.
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
# Note that empty commits are commented out

When running the rebase, Git halts whenever the command returns a non-zero exit code to allow you to check what happened. In this example, the first couple of commits don’t have any JavaScript files to format, which are skipped with git rebase --continue:

Executing: prettier --write {**/*,*}.js
[error] No files matching the pattern were found: "**/*.js".
[error] No files matching the pattern were found: "*.js".
warning: execution failed: prettier --write {**/*,*}.js
You can fix the problem, and then run

  git rebase --continue

Each commit gets picked before the execution happens, which can cause conflicts between formatted and still-unformatted code. In that case, remember that the next exec step reformats the code and amends it to the commit, so you can pick the unformatted version and have the formatter do the formatting.


: Examples with different formatters:

mix format (Elixir)
git rebase -i --exec 'mix format' main
Rubocop (Ruby)
git rebase -i --exec 'rubocop --auto-correct' main
Prettier (Node.js)
git rebase -i --exec 'prettier --write {**/*,*}.js' main

: ffcfe45 is the commit that adds prettier to the project. I injected that commit after the others were already written to be able to the formatter retroactively to clean up history.