最近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-のブランチ機能-リベース