🤖

Gitのリベースの説明

3 min read

はじめに

Gitのrebaseは、(特にSubversionから入ってきた人にとって)理解が難しいものです。rebaseの説明はネットに多数落ちており(例えばPro Git)、わかってから読み返すと「なるほど」と思うのですが、理解があやふやな時にrebaseでトラブルが起きるとどうして良いかわからなくなりがちです。

優れた解説が多い中、さらなる記事を書くのは屋上屋を架す感がありますが、「僕はこうやって教えてほしかった」的な覚書を残しておきます。

コミットと差分

commit.png

Gitのコミットは親コミットを覚えており、それをたどることで歴史を逆にさかのぼって行くことができます。Gitでは歴史を玉と線で表現することが多いです。玉はコミットを表し、コミットはその時点でのスナップショットを表しています。玉と玉の間の線は差分(パッチ)を表しており、一つ前のコミットにそのパッチを適用することで次のコミットが得られる、と解釈できます。

マージとリベース

beforemerge.png

いま、歴史が分岐しているとしましょう。masterbranchの二つのブランチがあり、共通のコミットからそれぞれ歴史が進んでいます。branchで加わった修正をmasterに取り込みたいとき、Gitはmergerebaseの二つの手段を選ぶことができます。

マージの場合

マージする場合は両方の修正を一度に取り込んだコミットを作ります。masterブランチからbranchにたいしてgit mergeをかけた場合、こんな感じになります。

aftermerge.png

この時、二つの歴史が一つになります。したがって、現在masterが指しているコミットの親は二つになります。親とつながる線は「その親から自分になるための修正パッチ」を意味しているため、それぞれ図示するとこんな感じになります。

patch.png

リベースの場合

branchブランチの修正をマージでmasterに取り込みたいとき、masterからbranchに対してgit mergeをかけました。それに対して、リベースで取り込みたいときには、まずbranchmasterに対してgit rebaseをします。

するとGitは、masterbranchの共通祖先から、branchの現在のコミットまでを切り出し、masterの先につなげます。これによりbranchの指すコミットの直接の祖先がmasterになるため、Fast Forwardマージが可能になります。

rebase.png

この図だけ見ると、branchにぶら下がっていたコミットを「移動」したように見えますが、実際にはbranchのコミット間から「パッチ」を取り出し、それを順番に適用することで新たにコミットを作っています。先ほどまでの図の例で見てみましょう。

rebase2.png

この図を見ると、新しくできたc1'c2'コミットは、リベース前の対応するコミットc1c2とは異なるスナップショットを表していることがわかります。むしろ変わっていないのは、c1c1'から親コミットに向かって伸びる線が表すパッチです。つまり、「リベースとは、玉(コミット)ではなく、線(パッチ)を移動する操作である」と理解できます。

リベースのsquash

リベースが「共通祖先からリベース元にいたるまでのパッチを、リベース先に次々と適用することだ」と理解できると、rebase -iで出てくるsquashの意味もわかります。

先ほどの状態でbranchからrebase -i masterを実行すると、どのコミットをどうするかを聞かれます。今回のケースでは二つコミットがあるので、それぞれについて対応を聞かれます。rebase -iで選べる対応はpickrewordeditsquashfixupxがありますが、よく使うのはpick(コミットを使う)と、squash(コミットを使うが一つ前のコミットと融合してしまう)でしょう。

デフォルトはpickです。-iをつけずにrebaseした場合は、リベース対象となっている玉の数だけ、リベース先にくっつくことになります。

squash.png

squashは、コミットをまとめます。図を見ると「玉(コミット)をまとめる」というよりは「線(パッチ)をまとめる」といった方が実態に近い気がします。

まとめ

Gitのリベースが何をやっているかを説明してみました。「Gitの歴史の表示においてコミットがスナップショット、コミット間の線がパッチを表す」ということ、「リベースは、共通祖先からリベース元へのパッチを、リベース先に次々適用することで一本の歴史を作る作業である」ということを理解するのに時間がかかりました。これがわかってしまうと、例えばマージならコンフリクトが一度しかおきないのに、リベースでは何度もコンフリクトが起きることがある理由がわかったり、squashにより空のパッチができてしまってリベースに失敗して焦るようなこともなくなるでしょう。

本稿が「リベースわからん」同志の助けになれば幸いです。

参考文献

GitHubで編集を提案

Discussion

ログインするとコメントできます