Gitのrebaseとmergeの挙動の違いをGitHubを用いて検証してみた

最近Gitを新卒に教えることがあった[@oliver_diary](https://twitter.com/oliver_diary)です。
その中で、`merge`と`rebase`の違いを教える機会があったので、記事にしました。

Gitを使っていると、はじめに立ちはだかる関門だと私が勝手に思っている`rebase`と`merge`の違いですが、しっかりとこの２つの違いを理解し、メリット、デメリットを抑えておくと、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`コミットを打たなくて済む場合に利用されます。
イメージとしては下記の通りです。

![git.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/437de62b-ab07-44b9-be6a-3ac222844a7e.jpeg)

![git.003.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/dfc8acfc-a0d6-5eb5-73a2-c5d8b2279965.jpeg)


## `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`になります。

![git.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/437de62b-ab07-44b9-be6a-3ac222844a7e.jpeg)

![git.002.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/ec06be8a-53dc-0d3a-d0f1-1355baeb02fd.jpeg)

また、`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`ブランチで打たれたコミットが、もう一度打たれ直すからです。

![git.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/437de62b-ab07-44b9-be6a-3ac222844a7e.jpeg)

![git.004.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/b6eea425-eff9-2437-95ee-f0cd8755f309.jpeg)

図でもわかるように、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では下記のような見え方になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/ac2eec86-4306-b66c-5741-944435d826c6.png)

https://github.com/minakawa-daiki/git-merge-rebase/pull/3

## `rebase`の場合は`force push`が必要になる場合がある
これは、すでにGitHubなどのリモートリポジトリに`topic`ブランチをPushしてしまった後に、ローカルリポジトリで`rebase`をした場合、リモートリポジトリに再度Pushしようとすると`force push`が必要になります。

上記で説明した`rebase`の挙動を見れば、納得できると思いますが、`rebase`をした段階で全く異なるコミットになってしまうので、このような現象が起こります。

リモートリポジトリに対し、`force push`したくない場合は、注意しましょう。GitHubでも`force push`をした場合、下記のような表示になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/5df7f45d-93b7-733b-9a0c-f5116cd6c82c.png)

https://github.com/minakawa-daiki/git-merge-rebase/pull/1

また余談ですが、`force push`の種類には`force-with-lease`があります。
これを使用することで、**Pushする際、リモートのrefsと、ローカルの`rebase`したブランチのrefsを比較し、ズレていれば`force push`が失敗する**という挙動をします。
また、`--force-with-lease`前に`fetch`していると効果が発揮されないので注意も必要です。

ですが、これによって大事件を回避できる場合があるので、要チェックです。

## `rebase`をした人がコミットを打った人と違う場合、複数人の記録が残る
`rebase`は、新しいコミットを作成するため、`rebase`を実行した人がコミットを打ったことになります。
ですが、GitHubなどのサービスでは、`rebase`する前の人も同時に表示し、見やすくなっています。
なので、コミットに対して二人のアイコンがあった場合、驚かず、`rebase`されたんだな。と思いましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/f821e2e5-64e7-b802-49ae-27aedd9d0371.png)

https://github.com/minakawa-daiki/git-merge-rebase/pull/1

ちなみに、同じ人が`rebase`した場合は１人しか表示されません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/596801e8-c616-8456-f294-7700a5d87296.png)

https://github.com/minakawa-daiki/git-merge-rebase/pull/4

## `rebase`の場合は時系列が分からなくなる場合がある
これは、`rebase`をした場合、コミットハッシュとコミットを打たれた時間が変わってしまうため起こります。
特にこの影響を受けるのは、GitHubなどのリモートリポジトリでの表示です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213538/8f7d0982-b047-9642-a20b-65d1ad83cab4.png)

https://github.com/minakawa-daiki/git-merge-rebase/pull/4

上記の画像では、コメント１の後に`rebase`し`force push`をした状態です。
これによって、コミットの時系列が崩れ、コメントの前に元々あったコミットの表示が見づらくなってしまうという難点があります。

コードレビューが始まっていた場合など、このように時系列が変わってしまうとレビュワーが混乱してしまう可能性もあるので注意しましょう。

## `merge`の場合はコンフリクト解消が1回で済む
`merge`の場合は、`merge`元のブランチと、`merge`しようとしているブランチの最新を比較するため、コンフリクト解消が1回で済みます。

ですが`rebaese`の場合は、ベースとなっているブランチを付け替えた後、元々あったコミットを1つ１つコミットし直していきますので、コミットのたびにコンフリクトが起こる場合があります。

`rebase`する前に`squash`をしておけば確かに1回でコンフリクト解消は済むかもしれませんが、とても巨大なプルリクエストになってしまい、`squash`が難しい場合は、`rebase`で消耗するより`merge`コミットを打った方がいい場面もあると私は思います。

ここはチームと要相談かなと思っています。

# まとめ
以上を踏まえ、チームの方針はあるものの、それぞれの使い分けを理解し認識できていれば良いと思います。
本記事を読んでいただいて、「なんとなく`merge`やなんとなく`rebase`」を卒業できれば幸いです。

また余談ですが、`merge`と`rebase`の使い分けの一般論としては、下記のように言われています。

> 一般論として、両者のいいとこどりをしたければ、まだプッシュしていないローカルの変更だけをリベースするようにして、 歴史をきれいに保っておきましょう。プッシュ済みの変更は決してリベースしないようにすれば、問題はおきません。

参考: [Git-のブランチ機能-リベース](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81%E6%A9%9F%E8%83%BD-%E3%83%AA%E3%83%99%E3%83%BC%E3%82%B9)