【ケーススタディ】マージ戦略を変えてみたらgit中級者になれた気がした


gitでえらいブランチの内容を優先して取り込みたいとき、どうしますか？

---

gitのケーススタディです。

gitのとあるブランチ(featureブランチ)上で開発しはじめて、結構長い時間が経ってしまいました。
その間にgitのえらいブランチ(派生元、masterブランチ)がどんどん先に進んでしまったので、
そろそろfeatureブランチにmasterブランチの内容を取り込みたいと思います。

というシチュエーションがあったので、これの解決方法を考えてみました。

## シチュエーション整理

* masterブランチからfeatureブランチが派生している
* featureブランチでは、リポジトリのうち特定の部分だけいじってある(仮に`/src/`フォルダとする)
* masterとfeatureは別々に開発されていて、どちらも結構進んでいる
* masterの内容をfeatureに取り込みたい(マージしたい)のだが、このとき次のような条件がある
  * **featureブランチでいじった部分はすべてfeatureブランチの内容を優先したい**
  * **それ以外はすべてmasterブランチの内容を優先したい**
* masterブランチはえらいのでうっかりプッシュできない(保護もされてない)

## やったこと

* 大筋としては、masterブランチの内容をfeatureブランチにマージすれば良さそう
  * しかし今回は先にfeature → masterにマージ、その後master → featureにマージという2段階が必要だった。
* **マージ戦略** がポイントとなる。
  * `ours` 戦略を使うことで、どちらか一方(「こちら側」と呼ばれる。現在いるブランチの側)の変更のみを使用してマージすることができる。今回はmasterブランチの内容を優先させるために使用した。
  * `ours`戦略はマージ相手方の変更を一切無視するので、マージ後(正確にはマージ作業中)に`git checkout ブランチ ファイルorフォルダ名`を使って特定のファイルorフォルダのみをfeatureブランチから持ってくるようにした。
  * マージコミットの内容を上記のように修正したかったので、`git merge --no-commit --no-ff`を使って自動でコミットされないようにした。
* masterブランチをリモートに誤ってプッシュしないよう、detached HEAD機能を使用した。

### コミット&ref図解

![git_merge_s_ours-STEP1.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156941/79a7ba2a-c268-a72a-453a-96b5a033d34a.png)

![git_merge_s_ours-STEP2.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156941/637f4cbc-6499-3915-263f-84ad352c704f.png)

![git_merge_s_ours-STEP4.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156941/c56f5579-6896-33a7-8ec4-592cacfbe6d9.png)



### コマンド

一応書いてみたけど私はGUI派なので
マージ戦略指定のような要所要所でしかコマンド使ってません。下記は参考まで

```bash
git checkout feature
git pull
git checkout master
git pull

git checkout HEAD^0 # 1.コミットをmasterから切り離す(deatched HEAD状態をつくる)
git merge -s ours --no-commit --no-ff feature # 2.feature → masterマージ(コミットはまだ)
git checkout feature -- /src # 3.featureブランチの変更をとってくる
git commit -am 'featureをmasterにマージ' # 4.マージコミット作成
git rev-parse HEAD # コミットのSHA-1をメモる
git checkout feature
git merge 1234567890 # 5.featureに切り替えて、マージコミットを再度featureに取り込む
```

## ポイント解説

### 1. コミットをmasterから切り離す(deatched HEAD状態をつくる)

detached HEAD状態はブランチからHEAD(現在参照しているコミット)を切り離す機能のことです。
過去のコミットをチェックアウトする際なんかにたまに見ると思います。(そしてその状態でビルドしてみてバグの混入タイミングを調べたり、`git reset`でブランチを巻き戻したり)

![スクリーンショット 2022-12-17 22.19.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156941/3448ef16-e099-0121-b08a-a73571c3874d.png)

今回はdetached HEAD状態を意図的に作り、masterブランチからデタッチすることで
うっかりmasterブランチへコミット・プッシュしないようにしてみました。

今回のシチュエーションでは必須作業ではないですが、masterがえらいブランチなので誤コミットを防ぐことで安心して作業できるようになりました。[^1]

### 2. feature → masterマージ(コミットはまだ)

gitのマージには実は **マージ戦略** というものがあり、マージの際の挙動を変えることができます。
普段はあまり意識しないかもしれませんが、今回のケースのように **大量に競合が発生することが予想される場合** や、
とりあえず普通にマージしてみたけどなんか思った通りにならない・・・という場合にはマージ戦略を変えてみるとそんなに競合しないで済むかもしれません。

参考: https://www.atlassian.com/ja/git/tutorials/using-branches/merge-strategy

今回はfeatureブランチにmasterブランチを、変更があったものだけfeatureを優先し、それ以外はmasterを優先して取り込みたかったので
`git merge -s theirs` みたいな戦略があればうれしかったのですが・・・そんなものはないっぽいです。

ですので「こちら側の変更を優先する」という`ours`戦略を使うことにしました。masterブランチの上で`git merge -s ours`すれば、masterの変更を優先して取り込むことができるはずです。

### 3. featureブランチの変更をとってくる

`git merge -s ours`で無事、masterの内容を優先して取り込むことができた・・・と思ったのですが、
この戦略って「変更があるなしにかかわらずこちらがわのブランチの内容を問答無用で使う」というものなんですね・・・。
というわけで`git merge -s ours`を何の工夫もなく使うのはちょっとダメそうですね。[^2]



gitには、特定のファイルやフォルダだけ他のブランチから持ってくる機能があります。
これを使って、マージ後にfeatureブランチから変更箇所だけを持ってきましょう。

```bash
git checkout feature -- /src # 3.featureブランチの変更をとってくる
```

### 4. マージコミット作成

正確には、この修正はマージコミットの後ではなくマージコミットの中に混ぜ込みたいですね。
なぜかというと、この修正はマージ作業の一環として行っているからです。

なので、先ほどのマージコマンドを修正して[^3]、コミットせずマージする → featureブランチの変更をとってくる → マージコミットする[^4] という流れにします。

```bash
git merge -s ours --no-commit --no-ff feature # 2.feature → masterマージ(コミットはまだ)
git checkout feature -- /src # 3.featureブランチの変更をとってくる
git commit -am 'featureをmasterにマージ' # 4.マージコミット作成
```

### 5. featureに切り替えて、マージコミットを再度featureに取り込む

あとは、もともとやりたかったこと(featureにmasterの内容を反映する)をやるために
featureブランチに切り替えて[^5]、いまのコミットの内容をfeatureブランチにマージします。

このとき、いまの状態がdetached HEAD状態であることに注意します。detached HEAD状態でもコミットを積み重ねることは可能ですが、ブランチが紐付いていないため、別のブランチをチェックアウトするとさっきまでのコミットが行方不明になりがちです。
一つの解決策は、今の状態でブランチ作成してしまうことです。

```bash
git checkout -b merge-temp
```

もう一つの解決策は、別にブランチ作成していなくても今のコミットのIDさえわかれば、それを使ってfeatureブランチにマージすれば良いというものです。

```bash
git rev-parse HEAD # 1234567890
git checkout feature
git merge 1234567890 # 5.featureに切り替えて、マージコミットを再度featureに取り込む
```

## まとめ(今回の学び)

* detached HEAD状態のままgit操作してみることでコミットやHEADの仕組み理解が深まった！
* マージ戦略にはいくつか種類があり、適したものを選ぶことで競合を防いだり手間を軽減できるかもしれない！
* ブランチを切り替えなくても他ブランチから一部の変更だけを持ってくることができる！(`git checkout feature -- /src`)


[^1]: 直感的な理解だとブランチの上にコミットが乗っている(あるいはコミットをつないでいってブランチができる、とか)イメージを持ちがちですが、コミットを指し示すポインタがブランチだという正しい理解をしているとdetached HEAD状態もあまり怖くないです。便利に使っていきたいです
[^2]: というかこれは「マージ」って呼んでいいのか？併合というよりは併呑だよね？
[^3]: 修正コマンドがあるわけではないです。先ほどのマージはあの時点でやらずに、という意味ね。うっかりマージしてしまったら`git reset`とかで戻す
[^4]: わたし`git merge`コマンド使ったことないのであってなかったらごめんなさい
[^5]: `git merge`はブランチを切り替えずに行える気がするんですけど、やっぱり直感的な理解に合っているので一度切り替えてから行ってみました。このへんが中級者以上になりきれない点なんですよね。。