どこかでゴミcommitが混ざってしまったときはgit bisectを使おう

開発中に適当にcommitしていたら気がついたらビルドできなくなっていて、どのcommitからビルドできなくなったのか分からなかったので、地道にマージしてはいけなかったcommitを探してたんですけど、どうもこういうのを上手に探す方法があることを知ったのでその話。

 

ゴミcommitが混ざって困った

開発途中で意図せず、ゴミcommit(ビルドとかテストとかが通らないコミット)が混ざってしまったものの、それに気づかずしばらく開発を続けていると、一体どこでゴミcommitが混じったのか分からず、どのcommitから修正すればいいのか分からなくなりました。

 

対処法 git bisect

こういうのはbisectオプションを使うといけます。

Git - git-bisect Documentation

git bisectはテストを設定して、テストが通るcommitをgood、通らないcommitをbadとし、最初のbadなcommitをにぶたんして、そのcommitにcheckoutしてくれます。

$ git log --oneline
commit d913e8f (HEAD -> master) echo Peach >> hoge.txtを実行した
commit ee9f27d echo Orange >> hoge.txt を実行した
commit c09ecf1 先頭行を削除した
commit bb040a6 echo Grape >> hoge.txt を実行した
commit 3c304a1 echo Banana >> hoge.txt を実行した
commit aad5e68 echo Apple > hoge.txt を実行した
$ cat hoge.txt
Banana
Grape
Orange
Peach

まずこういう状態だったとします。

もしこのプロジェクトではhoge.txtの先頭行にはAppleと書き込まれている必要があった場合、HEADのhoge.txtの先頭行はBananaとなっているためどこかでゴミcommitが混ざってしまったことになります。

このゴミcommit(先頭行をAppleから変更したcommit)をgit bisectを使って探していきます。

まずはgit bisect start [bad commit] [good commit]を実行して探索を開始します。

$ git bisect start HEAD aad5e68

aad5e68は一番最初のcommitです。

ここからテストスクリプトを使って判定していきます。

$ cat test.sh
#!/bin/bash
test $(cat hoge.txt | head -1) = Apple

先頭行がAppleかどうかを確かめるスクリプトを用意しました。このスクリプトの終了ステータスが0のときgood(=先頭行がApple)、1のときbad(=先頭行がAppleでない)となります。

テストスクリプトを使うにはgit bisect run [コマンド]を実行します。

$ git bisect run sh test.sh
$ git bisect run sh test.sh
running sh test.sh
Bisecting: 0 revisions left to test after this (roughly 1 step)
[ee9f27d8c006db80336ce5bce21091c112f50754] echo Orange >> hoge.txt を実行した
running sh test.sh
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[c09ecf138d06d7717994626bf360b7610219a9ee] 先頭行を削除した
running sh test.sh
c09ecf138d06d7717994626bf360b7610219a9ee is the first bad commit
commit c09ecf138d06d7717994626bf360b7610219a9ee
Author: hoge <hoge@hoge.com>
Date:   Wed Nov 7 15:24:58 2018 +0900

    先頭行を削除した

:100644 100644 4f03f2db8832cd64eacdc979adecc3269f10076f cca52e1f4f14c291c99550527871405657a68d27 M	hoge.txt
bisect run success

自動的に最初にtest.shのテストが失敗したcommitにcheckoutしてくれます(ここでは先頭行を削除したcommit)

bisect runで指定するコマンドは終了ステータスとして

goodのとき0

badのとき1~128(125を除く)

skip(テスト不能)のとき125

を返すようにする必要があります。

これで無事ゴミcommitを特定することができたので、あとは煮るなり焼くなり自由にしてください。

多くの場合はテストが用意されていたり、僕のようにビルドが通るか確認するだけなのでbisect runが使えますが、手動でgoodなのかbadなのかを判定することもできます。

現在のcommitがgoodのときgit bisect good、badのときgit bisect badを実行します。

$ git bisect start HEAD aad5e68
Bisecting: 2 revisions left to test after this (roughly 1 step)
[bb040a6b42f79bbc8d380e29d11cec3e02cf7e71] echo Grape >> hoge.txt を実行した
[0]shumon((no branch, bisect started on master)):tmp$ sh test.sh; echo $?
0
[0]shumon((no branch, bisect started on master)):tmp$ git bisect good
Bisecting: 0 revisions left to test after this (roughly 1 step)
[ee9f27d8c006db80336ce5bce21091c112f50754] echo Orange >> hoge.txt を実行した
[0]shumon((no branch, bisect started on master)):tmp$ sh test.sh; echo $?
1
[0]shumon((no branch, bisect started on master)):tmp$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[c09ecf138d06d7717994626bf360b7610219a9ee] 先頭行を削除した

特定できました。

 

git bisectのその他の使い方

別にテストに通るか通らないかではなく、単に特定の関数が追加されたタイミングとか、ファイル名が変わったタイミングとかを知りたいときにもgit bisectは便利です。

そういうときは専用のテストスクリプトを書いても良いですし、わざわざテストスクリプトを書かなくても手動で十分確認できます(git bisectはcommitを二分探索するので、極端に探索する範囲が広くない限り、ほんの数回のチェックで特定のcommitを見つけられます)

ただ、特定の関数が追加されたタイミングを知りたいときに、その関数が追加される前なのか後を"good"と"bad"で管理するのは混乱の元です。

Gitはこういうbisectの使い方を想定してくれているので、goodの代わりにnew、badの代わりにoldを使うことができます。

あとは同様にして特定のcommitを見つければいいです。

 

まとめ

git bisectはなかなか使う機会がないかもしれないけど、存在だけでも知っておくと、もしものときに便利かもしれません。

こんな感じで使う機会は少ないけど絶妙な便利機能がたくさんあるGit、奥が深いね。

あと多少めんどくさくてもちゃんとCI回しておけばこういうことにはならないと思うので、CI使いましょう。