なぜ git rebase をやめるべきか

Why you should stop using Git rebase

translated on

Git での開発を数年間経験した後、徐々に日々の仕事の一部として、より高度な Git コマンドを使うようになりました。私は Git rebase を見つけてすぐにそれを毎日の仕事に使いました。リベースに精通している人は、どれだけ強力で魅力的なツールであるのか知っているでしょう。しかし、リベースには、初めてリベースを触ったときにはわからなかったのですが、いくつかの課題があることに気が付きました。これを説明する前に、マージとリベースの違いをおさらいしておきましょう。

最初に、feature ブランチを master にマージする例を考えてみましょう。マージすることにより、新しいマージコミット g を作成します。下のコミットグラフではマージした際に何が起こるのかを説明しています。また、開発が盛んなリポジトリでよく見かける「線路」のようなグラフになっているのが見て取れるでしょう。

マージの例

あるいは、マージする前にリベースすることもできます。def のコミットは削除され、feature ブランチは master にリセットされた後、feature ブランチのコミットが再適用されます。これらの再適用されたコミットの差分は、元のコミットと普通は同じになります。しかし、親となるコミットが変わります。したがって、SHA-1 キーが変わります。

リベースの例

私たちは今、feature ブランチのベースコミットを b から c に変更しました。feature ブランチ上のすべてのコミットが master の子孫であるため、feature ブランチから master へのマージが fast-forward マージになります。

fast-forward マージの例

マージと比較して、リベースでは、履歴が直線的になり分岐するブランチがありません。このことによる読みやすさは、私がリベースが好きだった理由でした。おそらく、同じ理由でリベースを好む開発者もいるのではないでしょうか。

しかし、このアプローチにはいくつかの課題があります。

feature ブランチで使用している依存関係(例えばライブラリ等)が master 上の c で削除された場合を考えてみましょう。feature ブランチが master にリベースされたとき、アプリケーションのビルドは壊れますが、コンフリクトがない限りリベースは中断されません。最初のコミットでエラーが発生しているので、その後のすべてのコミットにおいてもエラーが発生し続けます。

このエラーはリベースが完了した後にのみ検出され、通常は新しいバグフィックスコミット g を適用して修正されます。

失敗したリベースの例

しかし、リベース中にコンフリクトが発生した場合、Git はコンフリクトしているコミットを一時停止し、処理を続行する前にコンフリクトを修正します。コミットが多く連なっている場合、リベース中にコンフリクトを解決することは、しばしば混乱を招き、正しく修正するのが難しく、潜在的なエラーの原因ともなります。

エラーの混入は、リベース中に起こった場合、特に問題となります。履歴を書き換えるときに新しいエラーが発生し、履歴が最初に書き込まれたときに混入した真のバグを隠す可能性があります。特に、Git Toolbox の中で最も強力なデバッグツールである Git bisect を使うのがより難しくなります。例として、次の feature ブランチを考えてみましょう。ブランチの最後の方でバグが混入したとしましょう。

最後の方にバグが混入したブランチ

ブランチが master にマージされてから数週間後まで、このバグに気づかないかもしれません。バグが混入したコミットを見つけるには、数十回または数百回のコミットを検索する必要があります。このプロセスは、バグが混入していないかどうかを確認するスクリプトを書くことにより自動化することができます。また、git bisect run <yourtest.sh> コマンドを使うことによりその実行も自動化できます。

bisect は履歴を二分探索することによりバグが混入したコミットを特定します。以下に示す例では、バグが混入した最初のコミットを見つけることに成功しています。これは、壊れている全てのコミットに、実際に探しているバグが含まれているからです。

Git bisect が成功した例

一方、リベース中に追加の壊れたコミットが混入した場合(ここでは de)、bisect は問題に遭遇します。この場合、Git には f が悪いと判断して欲しいのですが、誤って d と識別してしまいます。

Git bisect が失敗した例

この問題は思っている以上に大きいかもしれません。

私たちはなぜ Git を使うのでしょう?コード内のバグの原因を突き止めるための最も重要なツールだからです。Git は私たちのセーフティネットです。直線的な歴史にしたいというリベースは、Git の利点を損ねてしまいます。

少し前に、私はシステム内のバグを追跡するために数百のコミットを bisect しなければなりませんでした。欠陥のあるコミットは、同僚が失敗したリベースによって、コンパイルしなかったコミットの長いチェーンの途中にあります。この不必要かつ回避可能なエラーの結果、私はコミットを追跡するのに1日近く費やしてしまいました。

では、どのすればリベース中に壊れたコミットのチェーンを避けることができるでしょう? 1つのアプローチは、リベースプロセスを終了させ、コードをテストしてバグを特定し、バグが混入したした箇所に履歴を戻すことです。これは対話型のリベースを使うことで実現できます。

もう一つのアプローチは、リベースプロセスのすべてのステップで一時停止し、バグをテスト・修正していく方法です。これは厄介でエラーを起こしやすいプロセスであり、それを行う唯一の理由は直線的な履歴を達成することです。もっとシンプルで良い方法があるでしょうか?

あります。マージです。シンプルな1ステッププロセスです。すべてのコンフリクトが1回のコミットで解決されます。マージした結果として得られるマージコミットは、ブランチ間の統合点を明確に示しており、実際に何が起こったか、そしていつ起きたかを履歴でわかります。

あなたのリポジトリの履歴を正確に保つことの重要性は過小評価されるべきではありません。リベースすることは自分自身とチームに対して嘘をつきます。実際は昨日の別のコミットを元にしているが、そのコミットが今日書かれたふりをします。元のコミットの文脈が失われ、実際に何が起こったかわからなくなります。コードがビルドできることを確認していますか?そのコミットメッセージがまだ意味をなさないことを確認していますか?あなたはリポジトリの履歴を見やすくさせていると信じているかもしれませんが、その結果は逆になるかもしれません。

将来、あなたのコードベースにどのようなエラーや課題が起こるのかを言うことは不可能です。しかし、正しいリポジトリの履歴は、書き直された(または偽の)履歴よりも有用であると確信することができます。

開発者たちがブランチをリベースさせる動機は何ですか?

私はそれが虚栄心であるという結論に至りました。リベースは純粋に審美的な操作です。きれいな履歴は、開発者としての私たちにとって魅力的ですが、技術的または機能的な観点からは正当化できません。

非直線的な履歴。Figure from Paul Hammant

非直線的な履歴のグラフ(線路のようなグラフ)は脅威になりえます。私もはじめは間違いなくそう感じると思いますが、恐れなくても大丈夫です。複雑な Git の履歴を分析して視覚化できるツール(GUI と CLI の両方)が数多くあります。非直線的なグラフには、いつ、何が起こったのか、という貴重な情報が含まれており、それを直線的にしたところで何も得られません。

Git はブランチが分岐するような非直線的な履歴のために作られ、またそれを推奨しています。もし、それをあなたが忘れてしまっていたら、直線的な履歴のみをサポートするもっとシンプルな VCS を使うほうが良いかもしれません。

私はあなたのリポジトリの履歴を正確に保つべきだと思います。正しい履歴のリポジトリを分析するツールで快適になり、リベースする誘惑にも落ちません。リベースのメリットは少ないですが、リスクは大きいです。次にバグを追跡するために bisect する際、あなたは私に感謝するでしょう。

Paul HammantAslak Hellesøy により、この記事に対する貴重なフィードバックを頂きました。図を提供してくれた Paul Hammant に感謝します。彼の素晴らしいサイトは非常におすすめです。最初にこの投稿を書くことをおすすめしてくれた Aslak に感謝します。

この投稿は、私が JavaZone 2016 でノルウェーで発表した話に基づいています。
https://vimeo.com/182068915