どこかでゴミ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使いましょう。