【Git】同じコンフリクト解消を繰り返している人に教えたい「git rerere」

# はじめに
こんにちは、kenです。みなさんコンフリクト解消してますか！

チーム開発をしているとコンフリクトとは嫌でも向き合うことになりますが、コンフリクト解消って緊張感のある作業なのでやりたくないですよね。

そんなコンフリクト解消をちょっぴり楽にする(かもしれない)コマンドを最近知ったので今回はそれを紹介します、その名も`git rerere`です。

# git rerereとは
[Gitの公式ドキュメント（日本語版）](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-Rerere)には次のように記載されています。
>git rerere コマンドはベールに包まれた機能といってもいいでしょう。これは “reuse recorded resolution” の略です。その名が示すとおり、このコマンドは、コンフリクトがどのように解消されたかを記録してくれます。
そして、同じコンフリクトに次に出くわしたときに、自動で解消してくれるのです。

ここに書かれているように、`git rerere`は**以前に解決したコンフリクトを再利用することで、同じコンフリクトに再度遭遇した際、自動で解決してくれる機能**です。

一般的にコンフリクトは、どの変更を採用するかを決めるために意図を理解している人が手動で解決する必要がありますが、同じコンフリクトが再発する場合は、前回と同じ解決策を適用できることがほとんどです。そんなときに`git rerere`は非常に便利です。

しかし、このコマンドを使ったことがない人の中には「本当にコンフリクトを自動で解消してくれるの…？」と疑われる方もいらっしゃると思います（現に私がそうでした）。
そのため、次の章では実際に使っている様子をお見せしようと思います。

# 実際に使ってみる
本当に同じコンフリクト解消を自動で行ってくれるのか実際に試してみます。
まずはコンフリクトをわざと起こしますが、具体的なコンフリクト内容は知らなくてもいいので興味のある人だけ読んでください。
<details open><summary>起こしたコンフリクトの詳細</summary>

このデモのために作ったリポジトリ上の`main`と`feature`ブランチ間でコンフリクトを起こします。
最初`main`ブランチの`hello.txt`は次のような状態でした。
```:hello.txt(mainブランチ)
こんにちは
こんばんは
```
そこから`feature`ブランチを切り、この2つの挨拶に伸ばし棒を付け加えました。
```:hello.txt(featureブランチ)
こんにちはー
こんばんはー
```
それに並行して`main`ブランチで2つの挨拶にビックリマークを付け加える変更をしました。
```:hello.txt(変更を加えた後のmainブランチ)
こんにちは！
こんばんは！
```

この状態で`main`ブランチを`feature`ブランチにマージしようとするとコンフリクトが発生しますね。これで準備が完了です。
![スクリーンショット 2024-06-23 19.52.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1649218/240da8b9-57b7-15c0-757d-1f99adcd658d.png)

</details>

コンフリクトが起こせる状態になったので`git rerere`の出番です。
ここまで書いていませんでしたが、`git rerere`は`git config`で編集できるgitの設定のうちのひとつです。そのため、有効化したい場合は次のコマンドを打ちます。[^1]
```
git config --global rerere.enabled true
```

[^1]:もしくは`rerere`を有効にしたいリポジトリの`.git`配下に`rr-cache`を作成することでも`rerere`を有効化することはできます。この有効化の仕方は特定のリポジトリでのみ`rerere`を使いたい場合に便利です。`git config`で`rerere`を有効化すると全リポジトリで`rerere`が有効化されます。

まずは比較のため`rerere`が無効になっている状態（デフォルト）を見てみます。

`feature`ブランチ上で`git merge origin/main`を実行し`origin/main`とのコンフリクトを解消。その後`git reset --hard HEAD^`でマージコミットを打ち消し、再度`origin/main`とのマージを実行すると、二回目のマージ実行時も同じコンフリクトを解消しないといけなくなりました。これが普通ですよね。
![false.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1649218/15be9476-9b13-7dde-7668-f9e268245b64.gif)

次は`rerere`を有効にした状態で同じ操作をしてみます。

すると1回目のマージ実行時には先ほどと同様にコンフリクトの解消が求められましたが、2回目のマージ実行時にはすでにコンフリクトは解消されており、手動でのコンフリクト解消は必要なく`git add`して`commit`するだけになりました！

![true.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1649218/9047c7a0-8146-d724-fc5b-3a8cab0996b2.gif)


よくみると2回目のマージ実行時には`Resolved 'hello.txt' using previous resolution.`と出ており、以前のコンフリクト解消の結果を使っていることがわかります。
![スクリーンショット 2024-06-23 19.12.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1649218/a5e107b5-d7dc-5a42-4e96-d9a6eea5a3a5.png)

# 以前のコンフリクト解消の記録をどこに保存しているのか
この`git rerere`、一体どんな仕組みで動いているのか気になると思うので、その中身についても少し触れようと思います。

`git rerere`を有効にしてコンフリクト解消を行うと、作業を行ったリポジトリの`.git`配下に`rr-cache`というディレクトリが生成されていると思います。ここにコンフリクト解消の履歴は記録されていきます。
![スクリーンショット_2024-06-23_20_02_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1649218/4fb07ff3-6d0c-2a0b-366c-27dde7d2e0bc.png)

たとえば先程のデモで行ったコンフリクト解消の後にこの`rr-cache`ディレクトリを覗くと、ハッシュ値の名前でフォルダが作られており、その中には`postimage`と`preimage`という名前のファイルが入っています。

![スクリーンショット 2024-06-23 20.06.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1649218/ddb4f0f6-3566-9bc4-fabb-d01b23358891.png)

`preimage`は **コンフリクト発生時のファイルの状態** を記録します。先程の例で生成された`preimage`は以下の通りです。
```
<<<<<<<
こんにちはー
こんばんはー
=======
こんにちは！
こんばんは！
>>>>>>>
```
一方`postimage`は、**コンフリクト解消後のファイルの状態**を記録します。次に同じコンフリクトが発生したとき、このファイルの内容が再適用されます。先程の例で生成された`postimage`は以下の通りです。
```
こんにちはー！
こんばんはー！
```
まとめると、コンフリクト解消の履歴は`.git/rr-cache`ディレクトリに保存され、`preimage`と`postimage`がそれぞれコンフリクトの解消前・後を記録しているというわけです。[^2]

やっていることはかなり原始的でしたね。

[^2]:ちなみに`preimage`や`postimage`が入っていたディレクトリのフォルダ名がハッシュ値になっていましたが、これは`preimage`をこねこねした結果をSHA1でハッシュ化した結果のようです。（詳しくは[こちら](https://git-scm.com/docs/rerere)）

# 具体的な使用場面

ここまでで、同じコンフリクトの解消において`git rerere`が非常に便利であることをお伝えできたかと思います。最後に、このコマンドが有効に使える場面、すなわち具体的に同じコンフリクト解消が発生する場面についても少し触れておきたいと思います。これらの例は、冒頭でリンクを貼ったGitのドキュメントに書かれているものです。

<br>

- **長期にわたって開発が続いているトピックブランチをいつでもmainブランチに問題なくマージしたいが、そのためのマージコミットが複数生まれるのを避けたいとき。**
  （こまめにmainをマージしてはマージコミットを取り消すことで実現できますが、その際にrerereを有効にしておくと、同じコンフリクト解消を繰り返さなくて済みます。）

<br>

- **リベースする度に同じコンフリクトを処理することなく、ブランチをリベースされた状態に保っておきたいとき。**
  （頻繁にリベースを行う場合でも、rerereを使うことで毎回同じコンフリクトを解消する手間が省けます。）

<br>

- **多くのコンフリクト解消を乗り越えてようやくマージをしたあとに、リベースを使うことに方針を変更することになったとき。**
  （最初にマージを行った際にrerereが有効になっていれば、リベースに方針を変更しても同じコンフリクトを再度解消する必要がなくなります。）

<br>

- **開発中のトピックブランチをいくつもまとめてマージして、テスト可能なHEADを生成するとき。**
  （テストが失敗した場合、マージを取り消して失敗の原因となったブランチを除外して再度テストを実行しますが、rerereを使うことでその際のコンフリクト解消が不要になります。）

<br>

これらの場面では、`git rerere`が効果的に機能し、開発効率を大幅に向上させることができると思います。

# さいごに
今回は`git rerere`について書いてみました。
コンフリクト解消の履歴を記録して自動的に解消する以上、間違ったコンフリクト解消が記録されるとそれも勝手に適用されてしまうため注意が必要ですが、便利な機能であることは間違いないので使い所を見極めてうまくつかっていきたいですね。

間違いなどありましたらコメントにてご指摘ください、ここまで読んでいただきありがとうございました。