2015年2月15日日曜日

rebase して FF マージすることによる一直線の履歴は本当にわかりやすいのか

Git でトピックブランチを統合ブランチにマージする直前、最新の統合ブランチの先端にrebaseしたうえで FF (Fast-forward)マージすることで、見かけ上の履歴は一直線になる。 このような一直線の履歴が「わかりやすい」と解説されていることが多いんだけど、これは本当だろうか?

実は、このような見かけだけ「一直線」の履歴はむしろトピックの範囲が不明確になるのでわかりにくいんだ。



rebase && FF マージする理由としてよく挙げられるのが「マージコミットがあると log で見た時に履歴が入り乱れてわかりにくい」というものなんだけど、 これは明確な誤りだ。

マージコミットを含んだ統合ブランチにおいてファーストペアレントが常に統合ブランチ側になるようにマージしていれば[※]
[※] これは「統合ブランチ側をcheckoutして git merge <topic> でトピックをマージしていればしていれば、」とも言い換えられる。これはマージコミットによって統合ブランチ側を進めようとすると、こうすることになるので、普通みんなが無意識にやっているはずだ。
git log --first-parent
でその統合ブランチで起こった主要な変更の流れ(ほとんどがマージコミットだけになるはずだ)だけをサマライズしたログを見ることが可能なんだ。 さらにそのログでマージコミットが見えている個別のトピックについて詳細を見たければマージコミットの親との差分を見れば良い。 例えばマージコミットMによってマージされたトピックのコミットだけを見るには
git log M^..M
とすればいい。

実際の例を見てみよう。

次の log はマージコミットのあるリポジトリの統合ブランチを --first-parent で見た時のログだ。
% git log --first-parent --oneline
b06284f Merge branch 'drop-echo-p2'
f4d2f6a Update Readme
31d1ab0 Merge branch 'skip-and-todo'
110b339 Merge branch 'drop-echo' into pu
39cef7d Merge branch 'fix-for-netbsd' into pu
9d4b098 Merge branch 'refactor-genvers'
fdcce7c Merge branch 'enhance-tests'
この統合ブランチにどういうトピックがマージされて来たのかが一目瞭然だ。 さらに、ここでマージコミット
31d1ab0 Merge branch 'skip-and-todo'
のトピックの詳細を知りたくなったとする。 その場合は以下のようにすればいい。
% git log --oneline 31d1ab0^..31d1ab0
31d1ab0 Merge branch 'skip-and-todo'
6bffd91 Add documentation about todo
c815364 Add a new feature todo
d19b204 Add documentation about skip
dc4bd8a Add new feature 'skip'
5069ace Add a wrapper function for cmp_ok
1b37fa8 Change places adding '-' to testname output. 
31d1ab0^..31d1ab0 という範囲指定にすこし困惑するかもしれない。これは 「31d1ab0^ (つまり 31d1ab0 の親)の祖先には含まれないが 31d1ab0 の祖先には含まれるコミット」を指定する書き方だ。ふつうのコミットでこのような指定をすると、単にそのコミットがひとつだけ含まれることになる。ところがマージコミットについてこのような指定をすれば、それは実質的に「そのマージコミットでマージされたトピックに含まれるコミット」を指定できることになるんだ。 つまりマージコミットMについて log M^..M を見れば、統合ブランチから分岐したあとマージされたときまでのコミットが一覧できる。 これでマージコミット
31d1ab0 Merge branch 'skip-and-todo'
でマージされたトピックがどのような内容だったのか一目瞭然だ。 もしさらに情報が欲しいのであれば、--oneline をはずす、 -p をつけるなどすればいい。


上でみたように git log --first-parent で見ることのできる統合ブランチとしてのマージの履歴は一直線だし、 git log M^..M で見ることのできる各トピックの履歴も一直線だ。 マージコミットによる区切りがあるぶん、 rebase と FF で見かけだけ一直線にした履歴よりもはるかに閲覧効率が良い履歴になる。

もちろんトピックを途中で何度も統合ブランチに(または逆方向に→逆方向にだけマージしていればOKでした)マージしていると、そのトピックの履歴だけを git log M^..M で一息に見ることができなくなるけど、

  • 「理由なくトピックを何度も統合ブランチに(あるいは逆方向に)マージしない」
  • 「トピックは最終的にマージする最も下流の(≒ 古い)統合ブランチから分岐させる」

という基本を守っていればそのようなシチュエーションに陥ることはまずないはずだ。

もしこの統合ブランチ全体がrebase && FFマージによって作られた見かけだけ一直線のログであれば、どこからどこまでが 'skip-and-todo' というブランチだったのか、後から調べるのは大変だろう。
 
これに対し「いやいや、 rebase と FF マージするときに squash merge するから大丈夫」という意見を見かけることもある。 でも、ちょっと待って欲しい。本当に大丈夫なんだろうか?

これを説明するためにもういちど上の 31d1ab0 でマージされたトピック個別のログに話を戻すね。 「この例では」という但し書き付きになるけど、"git log --oneline 31d1ab0^..31d1ab0" から --oneline オプションを取り除き、-p をつけて詳細を見ていくと skip-and-todo ブランチは 'skip' と 'todo' という似たコンセプトの機能を追加するトピックで、 前半のいくつかのコミットはそのための準備としてリファクタリングを行っていることがわかるようになっているんだ。 squash merge するとこのような情報は失われ、巨大な diff の中をコミットメッセージなどの助けになる情報も少ない中でいちいち見ていかなければならなくなる。 このトピックで行われた変更に何か問題があったときにも困ったことがおこる。 squash merge していると bisect が示してくれるのはその巨大なコミットになってしまうんだ。 もし squash せずに細かい粒度のコミットを残していれば、bisect は問題ある小さなコミットを示してくれる。


ここまで見てくれた人は rebase と FF で見かけだけ一直線にした履歴が実はわかりにくい、ということをわかってもらえたと思うけど、「見かけの履歴が一直線であること」「各トピックの範囲が簡単にわかること」「コミットを細かい粒度に保つこと」すべてを同時に成立させる方法はないだろうか?

squash merge していなければ、見かけだけが一直線の履歴で達成できないのは「各トピックの範囲が簡単にわかること」だけだ。 実際のところこれは Git が「あるコミットが、以前どのブランチに所属していたのか」という情報を持たないことが原因なんだ。 従ってコミットメッセージ一行目の先頭ににブランチ名を残す、などのルールで運用すればいい。上の例でいえば、
8c85947 [drop-echo-2] Drop 'echo -n' #2
f4d2f6a [master] Update Readme
6bffd91 [skip-and-todo] Add documentation about todo
c815364 [skip-and-todo] Add a new feature todo
d19b204 [skip-and-todo] Add documentation about skip
dc4bd8a [skip-and-todo] Add new feature 'skip'
5069ace [skip-and-todo] Add a wrapper function for cmp_ok
1b37fa8 [skip-and-todo] Change places adding '-' to testname output.
fd533b7 [drop-echo] t/load.t: Drop echo and modify to use printf instead
687285f [drop-echo] t/output.t: Drop echo and modify to use printf instead
のようなログにしておけば rebase と FF マージでもトピックの境目がわかるようになる。

でも、この場合にもいくつか問題はある。

  • 統合ブランチで log --first-parent したときのようなサマリを得るのは難しい。
  • あとからトピック全体を revert したいときに面倒だし、ミスを誘発しやすい。 マージコミットがあれば revert -m 1 でトピック全体を打ち消せる。
  • 本来は不要な rebase 作業を開発者に強いることになる
  • 統合ブランチを進められるトピックは常にひとつしかない、つまり開発者はみんな統合ブランチの先端への書込処理を競って競合することになる。これは並列な分散開発という Git の利点のひとつを捨てるということだ。

このような問題を対価にして履歴を見かけだけ一直線に保つ合理的な理由は、僕はないと思う。

一応補足すると、プロジェクトによっては履歴の質(閲覧性の良さやbisectしやすいことなど)よりもとにかくコードを書き換えていくことが求められることもある。 そういう場合にはマージコミットがどうのといったことは比較的どうでもいいことになる。 でもそういう場合は、同時に履歴が一直線かどうかもどうでもいいはずだ。 そういう状況が長期にわたって続くプロジェクトだと、もしかしたら、そもそもVCSを使ったバージョン管理も不要かもしれない。日時をサフィックスにしたフォルダなどで原本を切り替えていくような、よりシンプルな方法を検討してみるのもアリだと思う。

(念の為の補足) トピックを統合ブランチにマージしようとしたらコンフリクトした、という場合にトピックを統合ブランチの先端方向に rebase するのは全く合理的な行為だと思う。この場合はマージでも解決可能だけど、どちらを選ぶかは状況による。もしトピック側にマージコミットが含まれていたら正しくrebaseできない可能性があるのでマージするほうが無難だろう。 あと、巻き戻し不可能な(= 使い捨てでない)統合ブランチにまだマージされていないトピックの履歴を綺麗にするために rebase -i するのも同様に合理的な習慣だ。これはむしろ推奨される。

0 件のコメント:

コメントを投稿