2014年2月8日土曜日

Gitのコミットグラフ(歴史)を綺麗に保つ効果(2)

前回に引き続き、綺麗な歴史を残す利点についてです。
今回は少し複雑な例を用います。
ストーリーとしては前回と同じく、あるトピックをマージしたら不具合があったのでリバート、その後修正が完了して再度マージした、というものです。ステップバイステップで詳細に説明すると、以下の1〜5のようになります:

1. 開発用ブランチnextにトピックブランチother1, other2, topic, other3とマージして開発をすすめてきました

2. ここでtopicに大きな不具合があることが判明したため、topicをまるごとrevertしました[*1]

3. その後、別のトピックブランチother4をマージしました

4. topicの修正が完了しました(修正コミット1, 修正コミット2)。わかりやすくするため、1でマージしたときのtopicの先端にtopic.oldというブランチを切ってあります

5. 修正したtopicをnextに再度マージしました
ちなみに今回の場合は5の再マージ作業の一環として2で作ったrevertコミットをrevertしておく必要があります(「★リバートコミット2」のコミットのこと[*2])。また、グラフではnextの名前はnext.bakに変更してあります。

この時点でのmaster..next.bakの概観を得るために、 git log --first-parent  を実行してみます。
% git log --first-parent --oneline master..next.bak
0582cfe Merge branch 'topic' into next
9dcbb30 ★リバートコミット2 リバートコミット1 をさらにリバート
f06b292 Merge branch 'other4' into next
fc5d727 ★リバートコミット1 "Merge branch 'topic' into next"
e66752c Merge branch 'other3' into next
535a937 Merge branch 'topic' into next
6fe33e5 Merge branch 'other2' into next
e6867ed Merge branch 'other1' into next
「topicマージしたわー、ってウソウソ間違ってたわー、ってそれも嘘、やっぱホントにマージしたー」みたいなログになってます。

巻き戻し再構成

そして、これを巻き戻し再構成、つまりnextをmasterに一旦reset --hardし、もともとマージされていたブランチを再度mergeしたあとのコミットグラフは次のような感じになります。
topicの途中のマージやリバートコミットが消えて、スッキリとしたグラフになっています。

git log --first-parent --oneline で見ると、さらにスッキリとした印象です。
% git log --first-parent --oneline master..next
d20b9f8 Merge branch 'topic' into next
2fd7d91 Merge branch 'other4' into next
ebb28fd Merge branch 'other3' into next
e0fc649 Merge branch 'other2' into next
43133e1 Merge branch 'other1' into next
もちろんnextとnext.bakの内容に差はありません
% git diff next next.bak
%

汚れた歴史をmasterに残さないよう、使い捨てブランチやnextのようなrebaseが許される(と合意できている)ブランチでは、適切なタイミングで巻き戻し再構成をしたいところです。

異なるbisectability

偶然なのですが、この2つのグラフには見た目以外にも、実用上の興味深い違いがあります。それはgit bisectする際の効率の良さです。

例えば前述のストーリー項目5のあと、あるレグレッションが判明したのでgit bisectするシチュエーションを考えます。
(前提知識)
本題を進める前に、前提知識として、今回の例に取り上げているソースツリーの特徴を説明します。
マージ、リバート以外の各コミットはコミットログが「Update <文字列>」となっていますが、これは<文字列>に該当するファイルを作成し、追加する、という変更になります。 例えば Update JxAfzw というコミットログのコミットでは、JxAfzwというファイルを追加するような変更だった、と考えてください。(マージでコンフリクトを出さないよう、mktempを使ってこんな仕様にしています)
話を簡単にするため、topicの最初のコミット「あやしいコミット1 Update JxAfzw」で追加されたファイル"JxAfzw"が問題だったとします[*3]、そのレグレッションを確認するテストは
test ! -f JxAfzw
になります。

具体的には
% git bisect start next master           # または start next.bak master
% git bisect run test \! -f JxAfzw
...
のように、next* をBAD, masterをGOODとしてbisectし、test ! -f JxAfzwが0以外を返す最初のコミット(1st BAD commit)をbisectに探させます。

"nextとmaster"をそれぞれBADとGOODとしてbisectした場合は以下のように正しいコミットに到達します(タグbisect/badがちゃんとJxAfzwを追加したコミットに付いている)が、

"next.bakとmaster"をそれぞれBADとGOODとしてbisectすると、途中で間違ったコミットに到達して終了してしまいます:
これは想像ですが、master..next.bak間でのリバートコミットにGOODをつけている(bisect/good-fc5d727...)ため、それ以前にマージされている「あやしいコミット1」を含むmaster..topic.old部分が調査対象から除外されてしまうのだと思われます。

もちろんもう一度master..bisect/bad間でbisectをかければ正しい1st bad commitに到達できますし、revertコミットを残していると必ずbisectに失敗するというわけではありません。例えばtopicの修正時、master..topic.old部分を含めて完全にrebaseしてあればfc5d727にGOODをつけたとしても生まれ変わったtopic全体がbisect対象として残るので、正しくbisectできると思います。

今回の例は意図的に作られたコミットグラフですし、このようなbisectabilityの違いは必ずしも発生するわけではありませんが、revertなどで汚れた歴史を残しておくと、こういうことも起こりうる、と意識しておいたほうが良いかもしれません。

なお、今回用いたリポジトリは例によって http://pf.sourceforge.jp/gitroot/k/kt/ktateish/rewind_complex.git においてあります。


以下は注釈です。
[*1] git revert -m 1 <マージコミット>でfirst-parentをメインラインとみなし、それ以外のブランチの変更、つまりtopicをまるごとrevertできます

[*2] マージコミットをリバートした場合、再度マージする際には注意が必要です。以前マージしたブランチの上にそのままコミットが追加されている今回のようなケースでは、「リバートのリバート」をしてからマージしないと、以前マージしたブランチのコミットがなかったことにされたままになってしまいます。ただし、以前マージしたブランチのコミットがすべてrebaseされている場合は「リバートのリバート」をしてはいけません。途中までrebaseされている場合はrebaseされてない分をcherry-pickする必要がありますが、そういうときは全部rebaseしてもらったほうがいいと思います。

[*3] ファイルの存在が原因の場合は普通git blameやgit logですぐ探しだせます。

0 件のコメント:

コメントを投稿