最近Gitを新卒に教えることがあった@oliver_diaryです。
その中で、mergeとrebaseの違いを教える機会があったので、記事にしました。
Gitを使っていると、はじめに立ちはだかる関門だと私が勝手に思っているrebaseとmergeの違いですが、しっかりとこの2つの違いを理解し、メリット、デメリットを抑えておくと、Gitを使いこなしてる感が出ると思っています。
また、実際の挙動について、GitHubなどのリモートリポジトリでの挙動をベースとした説明をしている記事があまりなかったので、そこについて触れることで、より実践的にイメージできればと思います。
mergeについて
まずはmergeですが、日本語では合流などと表現したりします。
例えば、masterブランチとtopicブランチが存在し、masterブランチにtopicブランチをmergeさせると、その名の通り、masterブランチに対しtopicブランチを合流させる形になります。
また、mergeにはfast-forward mergeとnon fast-forward mergeが存在します。
fast-forward mergeについて
fast-forward merge はmergeする際、mergeコミットを打たなくて済む場合に利用されます。
イメージとしては下記の通りです。
non fast-forward mergeについて
それに対し、non fast-forward mergeはmergeコミットが必要な場合に利用されます。
どういう場合にnon fast-forward mergeが起こるかというと、topicブランチを作成した後にmasterブランチに変更が入った場合などです。
下記の図の通り、コミットBからtopicブランチを作成した後に、masterでは新たにコミットCが打たれています。
こうなった場合は、fast-forward mergeは利用できず、non fast-forward mergeになります。
また、fast-forward mergeが利用できる場合でもnon fast-forward mergeにすることができます。
なぜそのようにするかというと、non fast-forward mergeの場合はブランチの情報が残るため、コミットがどのブランチで行われたのか特定することが容易になるためです。
この動作は、GitHubのプルリクエストをmergeする際のデフォルトの挙動となっています。
squashをせずとも、masterブランチをきれいに保ちつつ、topicブランチで行ったコミット情報は残るので、個人的には好きです。
rebaseについて
rebaseは挙動としては、rebaseされたブランチのベースブランチが付け変わるイメージです。
その副作用として、topicブランチでのコミットはパッと見た感じは変わらないですが、コミットハッシュやコミットされた時間などは変わります。
この挙動は、rebaseをした段階で一旦、topicブランチを作成した段階のmasterブランチから、rebaseをした段階のmasterブランチに付け替えるため、topicブランチで打たれたコミットが、もう一度打たれ直すからです。
図でもわかるように、XとYは同じように見えますが、実際はコミットとしては別物になっています。
mergeとrebaseの違いについて
上記で説明した通り、mergeとrebaseは根本的に挙動が違います。
大事なのは、違いを理解した上で、両者を使いこなすことです。
実際にGitを使っていて感じたそれぞれのメリットやデメリットを書いていこうと思います。
また、検証用に使用したGitHubのリポジトリを貼っておきます。
https://github.com/minakawa-daiki/git-merge-rebase
non fast-forward mergeの場合はマージコミットが打たれてしまう
これは、mergeする際、fast-forwardできないので、先頭にマージコミットが打たれてしまうケースです。
rebaseの場合は根本を付け替えるので、マージコミットのようなコミットが打たれることはありません。
なのでマージコミットがない方がコミットの数は少なくて済むので、mergeよりrebaseを使ってくださいと言われた場合は、上記のような背景があることを認識しておくといいでしょう。
実際、non fast-forward mergeでマージされた場合、GitHubでは下記のような見え方になります。
rebaseの場合はforce pushが必要になる場合がある
これは、すでにGitHubなどのリモートリポジトリにtopicブランチをPushしてしまった後に、ローカルリポジトリでrebaseをした場合、リモートリポジトリに再度Pushしようとするとforce pushが必要になります。
上記で説明したrebaseの挙動を見れば、納得できると思いますが、rebaseをした段階で全く異なるコミットになってしまうので、このような現象が起こります。
リモートリポジトリに対し、force pushしたくない場合は、注意しましょう。GitHubでもforce pushをした場合、下記のような表示になります。
また余談ですが、force pushの種類にはforce-with-leaseがあります。
これを使用することで、Pushする際、リモートのrefsと、ローカルのrebaseしたブランチのrefsを比較し、ズレていればforce pushが失敗するという挙動をします。
また、--force-with-lease前にfetchしていると効果が発揮されないので注意も必要です。
ですが、これによって大事件を回避できる場合があるので、要チェックです。
rebaseをした人がコミットを打った人と違う場合、複数人の記録が残る
rebaseは、新しいコミットを作成するため、rebaseを実行した人がコミットを打ったことになります。
ですが、GitHubなどのサービスでは、rebaseする前の人も同時に表示し、見やすくなっています。
なので、コミットに対して二人のアイコンがあった場合、驚かず、rebaseされたんだな。と思いましょう。
ちなみに、同じ人がrebaseした場合は1人しか表示されません。
rebaseの場合は時系列が分からなくなる場合がある
これは、rebaseをした場合、コミットハッシュとコミットを打たれた時間が変わってしまうため起こります。
特にこの影響を受けるのは、GitHubなどのリモートリポジトリでの表示です。
上記の画像では、コメント1の後にrebaseしforce pushをした状態です。
これによって、コミットの時系列が崩れ、コメントの前に元々あったコミットの表示が見づらくなってしまうという難点があります。
コードレビューが始まっていた場合など、このように時系列が変わってしまうとレビュワーが混乱してしまう可能性もあるので注意しましょう。
mergeの場合はコンフリクト解消が1回で済む
mergeの場合は、merge元のブランチと、mergeしようとしているブランチの最新を比較するため、コンフリクト解消が1回で済みます。
ですがrebaeseの場合は、ベースとなっているブランチを付け替えた後、元々あったコミットを1つ1つコミットし直していきますので、コミットのたびにコンフリクトが起こる場合があります。
rebaseする前にsquashをしておけば確かに1回でコンフリクト解消は済むかもしれませんが、とても巨大なプルリクエストになってしまい、squashが難しい場合は、rebaseで消耗するよりmergeコミットを打った方がいい場面もあると私は思います。
ここはチームと要相談かなと思っています。
まとめ
以上を踏まえ、チームの方針はあるものの、それぞれの使い分けを理解し認識できていれば良いと思います。
本記事を読んでいただいて、「なんとなくmergeやなんとなくrebase」を卒業できれば幸いです。
また余談ですが、mergeとrebaseの使い分けの一般論としては、下記のように言われています。
一般論として、両者のいいとこどりをしたければ、まだプッシュしていないローカルの変更だけをリベースするようにして、 歴史をきれいに保っておきましょう。プッシュ済みの変更は決してリベースしないようにすれば、問題はおきません。
参考: Git-のブランチ機能-リベース








