Gitの衝突を回避！ git rebase --onto の活用法

# はじめに

こんにちは！
今日はチーム開発でしばしば直面するGitの衝突問題と、それを効果的に解決するための`git rebase --onto`について解説します。
特にSquash Mergeを使用する開発フローでの衝突回避に焦点を当てていきます。

## GitHub FlowとSquash Merge

私たちのチームでは、ブランチワークとして当初は [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) を使用していました。
しかし、リリース頻度の向上を目標に手順の簡素化を求め、現在は [GitHub Flow](https://docs.github.com/ja/get-started/using-github/github-flow) を採用しています。GitHub Flowのブランチワークは、レビュー後のブランチをmainにマージし即リリース可能な状態として扱います。GitHub Flowの利点としては、以下のことが挙げられます。

- **シンプルさ**：ブランチワークがシンプルなので、チームメンバーが容易に理解できる
- **スピード**：小さな変更を頻繁に統合できるため、開発サイクルが速い
- **継続的インテグレーション**：変更を早く統合することで問題の早期発見が可能

次にマージ戦略として**Squash Merge**を採用しています。機能ブランチの複数のコミットを通常のマージのようにすべて履歴として残すのではなく、まとめて1つのコミットにして`main`ブランチにマージする手法です。

特に以下の利点が挙げられます。

- **クリーンな履歴**：`main`ブランチに機能単位の整理されたコミット履歴が作成される
- **読みやすさ**：各機能実装やバグ修正が1つのコミットとして明確に表示される
- **簡単なロールバック**：問題が発生した場合、特定の機能のコミットを簡単にリバートできる
- **開発プロセスの自由**：開発者は機能ブランチで自由にコミットできる（試行錯誤の過程は`main`には残らない）

頻繁にコミットを作成し履歴が多くなるようなケースでは、この戦略の恩恵にあずかれます。

## Squash Mergeで発生する衝突問題

しかし、GitHub FlowとSquash Mergeを組み合わせると、特定のシナリオで衝突問題が発生します。
以下の状況を考えてみましょう：

```
1. branch1による作業を実施しPRを出しレビュー待ちの状態
2. レビュー待ちの間に次の作業を実施するためbranch2を作成。
3. ただし、branch1の作業がベースになるためbranch1からbranch2を作成する
4. branch2で作業を開始しコミットを作成
5. レビューが通過したため、branch1をmainにsquash merge
6. その後、branch2のPRをだしレビュー後にmainにsquash merge
7. branch2はbranch1の変更含んだ上でmainにマージするため衝突する
```

### なぜ衝突が発生するのか？

この状況で衝突が発生する根本的な理由は以下のとおりです：

1. `branch1`がSquash Mergeされると、**複数のコミットが1つにまとめられ**、その履歴情報は変更される
2. `branch2`は`branch1`から派生しており、**元の詳細なコミット履歴を継承している**
3. `branch2`をマージしようとすると、Git は同じファイルの変更が**異なる履歴パス**で行われていると認識する
4. 結果として、既に`main`に取り込まれた`branch1`の変更と`branch2`に含まれる`branch1`の変更の間で衝突が発生する

つまり、Squash Mergeにより履歴グラフが書き換えられることで、Git の変更追跡メカニズムに混乱が生じるのです。

![git_squash_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4031570/22aed396-79a4-4d5d-bb30-5c2bc3091c56.png)

### 一般的な解決方法

このような状況で一般的に行われる解決方法は次のとおりです：

1. `branch1`が`main`にマージされた後、`branch2`を`main`に対してリベースする：
   ```bash
   git checkout branch2
   git rebase main
   ```

2. 衝突が発生した場合は手動で解決する
3. 衝突を解決した後、リモートの`branch2`を強制更新する：
   ```bash
   git push --force-with-lease origin branch2
   ```

この方法は有効ですが、**衝突解決が複雑**になることがあります。特に、`branch1`と`branch2`で多くの変更が重なっている場合、同じ競合を何度も解決することになり、非効率的です。

## git rebase --ontoを使用した効率的な解決方法

より効率的な解決方法が`git rebase --onto`です。このコマンドを使うと、特定のコミット範囲だけを別のベースブランチに移動させることができます。

### git rebase --ontoの基本構文

```bash
git rebase --onto <新しいベース> <古いベース> <移動させたいブランチ>
```

先ほどの問題のケースでは、以下のように使用します：

```bash
git checkout branch2
git rebase --onto main branch1 branch2
```

これは「`branch1`と`branch2`の間のコミット（つまり`branch2`固有の変更）のみを取り出し、`main`ブランチの先端に適用する」という意味になります。

### git rebase --onto の利点

- **衝突の減少**：既に`main`に取り込まれた`branch1`の変更を除外できるため、衝突が大幅に減少する
- **変更の明確化**：`branch2`独自の変更だけを新しいベースに適用するので、PRがより明確になる
- **作業効率の向上**：衝突の減少により解決の手間を大幅に削減できる

### 実際の手順

1. `branch1`がマージされた後、`branch2`で以下のコマンドを実行：
   ```bash
   git checkout branch2
   git rebase --onto main branch1 branch2
   ```

2. 衝突があれば解決（通常は`branch1`との重複がなくなるので衝突は少なくなる）
3. リモートリポジトリの`branch2`を更新：
   ```bash
   git push --force-with-lease origin branch2
   ```

4. これで`branch2`のPRには`branch1`の変更が含まれなくなり、`branch2`固有の変更のみとなる

![git-squash-merge-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4031570/a1eb6fa2-a001-4371-b302-c4fb7d6460d8.png)


## まとめ

GitHub FlowとSquash Mergeの組み合わせは非常に効果的な開発戦略ですが、ブランチに依存関係がある場合には衝突問題を引き起こすことがあります。
`git rebase --onto`を使うことで、これらの衝突を効率的に解決し、クリーンな履歴を保ちながら開発を進めることができます。

ぜひチームの開発フローに`git rebase --onto`を取り入れて、より効率的なGit運用を実現してみてください！

---
Schooでは一緒に働く仲間を募集しています！

https://corp.schoo.jp/careers/graduates

https://corp.schoo.jp/careers