Git commit それぞれに対してスクリプトを実行する

created: 2024-04-19

まとめ

以下のシェルコマンドで、最新2コミットに対してスクリプトを実行できる。

for c in $(git log --pretty=format:"%h" | head -2); do
    git checkout $c && do-script
done
# ただし、実行後はもとのブランチではなく最新のコミットにcheckoutした状態になる

まとめ2

調べていたらgit rebaseにある --exec(-x) オプションで同じことができるとわかった。 --exec オプションを使う方が良いだろう。

最新2コミットにスクリプトを実行したければ、

git rebase HEAD~2 --exec 'do-script'

と実行する。

実際にはコミットをまとめるrebase処理もあるので、

git rebase --interactive HEAD~4 --exec 'do-script'

のようになりそうだ。

詳細

ソフトウェア開発でGitを使っていることは多い。

その中で、Git hookを利用して自動的にテスト等のスクリプトを実行することも多い。もし使ったことがなければ、便利なので使ってみてほしい。 gitリポジトリの.git/hooksディレクトリには各hookのexampleファイルがあるし、pre-commitHuskyを使うと楽に設定できるだろう。

さて他方で、毎回のコミットでのpre-commit処理をするには向かないGit運用もある。 ごくごく小さな変更を細かくコミットしておいて、一段落ついた段階やpushする直前にrebase(squash, fixup)する運用だ。 僕はこの運用に近いのでpre-commitを避けたいことが多い。

たとえば

以下のgit logのように、add_inc_fブランチで機能およびテストを実装していて、細かい修正が入る度にコミットしている。

$ git log --abbrev-commit

commit 006 (HEAD -> add_inc_f)
Date:   2024-04-19T00:32:47+0900

    fix test of increment function

commit 005
Date:   2024-04-19T00:32:31+0900

    fix implementation of increment function

commit 004
Date:   2024-04-19T00:31:13+0900

    add test for increment function

commit 003
Date:   2024-04-19T00:30:51+0900

    implement increment function for u8

commit 002 (master)
Date:   2024-04-19T00:30:32+0900

    fixup (I forget to add .gitignore)

commit 001
Date:   2024-04-19T00:29:51+0900

    init

そして、一段落ついた時に、最新のコミット(006)はテスト実装のコミット(004)に、修正のコミット(005)は実装のコミット(003)に、それぞれsquash(fixup)する。

しかし、この時、pre-commitでテストが実行されることになっていると、コミット005を作る際にテスト実行が失敗してコミットができないはずだ。 なので、あまり良くないけれど、コミット005を作る時に git commit --no-verify でpre-commitを無視している。

さて、コミットを整理したあとのgit logは以下になっている。

$ git log --abbrev-commit

commit 008 (HEAD -> add_inc_f)
Date:   2024-04-19T00:31:13+0900

    add test for increment function

commit 007
Date:   2024-04-19T00:30:51+0900

    implement increment function for u8

commit 002 (master)
Date:   2024-04-19T00:30:32+0900

    fixup (I forget to add .gitignore)

commit 001
Date:   2024-04-19T00:29:51+0900

    init

このgit logにおいて、コミット007とコミット008はpre-commitが実行されていない。 pre-commitの目的は、各コミット単位で単体テストだとかコードフォーマットだとかをチェックしておくことなので、そういったpre-commitが実行されていないコミットが存在することは良くない。
そこで、これらのコミットに対してまとめてpre-commitを実行しておきたい。

そういうわけで、以下のスクリプトで各コミットにcheckoutしてpre-commit-scriptを実行することにした。

for c in $(git log --pretty=format:"%h" | head -2); do
    git checkout $c && pre-commit-script
done

追記

ここまで書いたところで git rebase --exec の存在を知った。

git rebase --interactive HEAD~4 -x 'do-script' すると以下のようなinteractive rebaseの表示になる。

pick 003 implement increment function for u8
exec do-script
pick 004 add test for increment function
exec do-script
pick 005 fix implementation of increment function
exec do-script
pick 006 fix test of increment function
exec do-script

あとは通常のrebase時と同様にコミットを移動/squash/fixして、実行したい箇所にexecを置いておくだけのようだ。

普段Gitを使っているにもかかわらず、認識していない機能はあるものだなあ