派生元のブランチでの部分的な履歴改変をgit rebase -iで簡単に取り込む

この記事は[NTTテクノクロスアドベントカレンダー 2021](https://qiita.com/advent-calendar/2021/ntt-tx) 13日目の記事です。


## はじめに

こんにちは。NTTテクノクロスの中野です。

https://twitter.com/nobeans/status/1468761082764627968

https://twitter.com/nobeans/status/1468761346640871427

とツイートした内容なのですが、ちょうどいいのでここでまとめておきます。
もしかするとよく知られた方法かもしれませんが、気にしません。


## 状況設定

- トピックブランチAで一通り実装した後、マージリクエスト[^1]を出してレビュー待ちしている間に、次のタスクの実装を進めたい。
- でも、さっきのトピックブランチAの実装内容を利用する必要がある。

みたいな状況ってありますよね。で、そういうときに、

- (1) ローカルのトピックブランチAをもとにトピックブランチBを生成する。

    ```
    > git checkout -b topic-b topic-a
    ```

- (2) トピックブランチB上で実装を進める。
- (3) トピックブランチAが無事に`main`にマージされたら、最新化した`main`でトピックブランチBを`rebase`すればいいよね。[^2]

    ```
    > git checkout main
    > git pull origin main
    > git checkout topic-b
    > git rebase main
    ```

という感じで作業を進めたりすることがあります。

しかし、多くの場合、トピックブランチAに関するマージリクエストのレビューでコメントがついて、修正する必要がでてきます。

で、その修正について、単純に修正コミットを1つ追加するだけであれば、上記の(3)での`rebase`には影響はないのですが、コミット履歴はできればきれいにしておきたい厨なので、当然履歴改変を使って、元々その修正内容が反映されているべきだったコミットに`squash`していく訳です。

そうなると、(3)での`rebase`で100％コンフリクトが発生してしまうんですよね。

個人的には、レビュー待ち中に先を急いで次の実装に取り掛かることが多いので、割とそういう状況になります。


## これまでの対処方法

これまでは、面倒ですが、普通にエディタでコンフリクトを解消してました。

トピックブランチB側で、トピックブランチAでの修正箇所を使っていない状態であれば、雑に`rebase -i`で、トピックブランチA由来のコミットをひとまず消してから、あらためてトピックブランチAから`rebase`し直す、という作戦で対応できたりもします。

トピックブランチB側がこんな感じのコミットログだとします。

```
444444 WIP: トピックブランチBでの実装中のコミット
333333 トピックブランチAの3番目のコミット
222222 トピックブランチAの2番目のコミット
111111 トピックブランチAの1番目のコミット
...
```

とりあえずトピックブランチAの1番目のコミットIDが含まれるように`rebase -i`します。

```
> git rebase -i 111111^
```

自動的にエディターが開いて、こんな感じで表示されます。

```
pick 111111 トピックブランチAの1番目のコミット
pick 222222 トピックブランチAの2番目のコミット
pick 333333 トピックブランチAの3番目のコミット
pick 444444 WIP: トピックブランチBでの実装中のコミット
```

このエディタ上で、トピックブランチA由来のコミットの行を一通り削除します。(前後関係がなければ修正があったコミットだけでもOKです。)
ここでは、トピックブランチAの2番目のコミットに履歴改変が入って、さらに3番目のコミットがそれに依存している、という想定で2行削除することにします。

```
pick 111111 トピックブランチAの1番目のコミット
pick 444444 WIP: トピックブランチBでの実装中のコミット
```

この状態でエディタで保存して、履歴を改変します。

その結果、トピックブランチA由来のコミットとしては、1番目のコミットだけが残っている状態になります。
こんな感じのコミットログです[^3]。

```
555555 WIP: トピックブランチBでの実装中のコミット
111111 トピックブランチAの1番目のコミット
...
```

ここでおもむろにトピックブランチAで`rebase`しなおします。

```
> git rebase topic-a
```

すると、こんな感じのコミットログになります。

```
888888 WIP: トピックブランチBでの実装中のコミット
777777 トピックブランチAの3番目のコミット
666666 トピックブランチAの2番目のコミット(修正済み!)
111111 トピックブランチAの1番目のコミット
...
```

結構手軽なのですが、トピックブランチB側の作業として、いったんトピックブランチAの2番目と3番目のコミットがいなくなっても影響がないような場合しか使えない[^4]ため、状況はかなり限定的です。


## 今回発見した対処方法

前項で説明した方法に近いのですが、`rebase -i`のところの操作がちょっと違います。

```
pick 111111 トピックブランチAの1番目のコミット
pick 222222 トピックブランチAの2番目のコミット
pick 333333 トピックブランチAの3番目のコミット
pick 444444 WIP: トピックブランチBでの実装中のコミット
```

において、トピックブランチA側で修正が入った2番目のコミットだけ、修正後のコミットIDに変更します。

```
pick 111111 トピックブランチAの1番目のコミット
pick 666666 トピックブランチAの2番目のコミット  ←ここのコミットIDだけを変更する
pick 333333 トピックブランチAの3番目のコミット
pick 444444 WIP: トピックブランチBでの実装中のコミット
```

これでエディタを保存して抜けると、しれっとトピックブランチAの2番目の修正済みのコミットが`pick`されるので、以下のようなコミットログになります。

```
888888 WIP: トピックブランチBでの実装中のコミット
777777 トピックブランチAの3番目のコミット
666666 トピックブランチAの2番目のコミット(修正済み!)
111111 トピックブランチAの1番目のコミット
...
```

手順自体がより簡単になっている上に、トピックブランチB側の作業として、トピックブランチAの2番目と3番目に依存していてもまったく問題ありません[^5]。これは便利。


## より洗練された方法

などとドヤ顔で記事を書いていたら、論理的隣人[^6]にもっといい方法を教えてもらいました。
やり方は簡単で、トピックブランチB側で`rebase -i`を実行するときに、ベースのコミット(ブランチ)としてトピックブランチAを指定する、という方法です。

トピックブランチA側で修正が終わった直後は、それぞれのコミットログは以下のようになっています。

トピックブランチA:

```
777777 トピックブランチAの3番目のコミット
666666 トピックブランチAの2番目のコミット(修正済み!)
111111 トピックブランチAの1番目のコミット
...
```

トピックブランチB:

```
444444 WIP: トピックブランチBでの実装中のコミット
333333 トピックブランチAの3番目のコミット
222222 トピックブランチAの2番目のコミット
111111 トピックブランチAの1番目のコミット
...
```

ここでトピックブランチB側で`rebase -i`するのですが、引数にトピックブランチAのブランチ名を指定する、というのがポイントです。

```
> git rebase -i topic-a
```

すると、自動的にエディターが開いて、こんな感じで表示されます。

```
pick 222222 トピックブランチAの2番目のコミット
pick 333333 トピックブランチAの3番目のコミット
pick 444444 WIP: トピックブランチBでの実装中のコミット
```

わかりやすいように勝手に補完して書くと、

```
pick 111111 トピックブランチAの1番目のコミット
pick 666666 トピックブランチAの2番目のコミット(修正済み!)
pick 777777 トピックブランチAの3番目のコミット
  (↑ここより上はtopic-a由来なので確定pick)
 --------------------------------------------
  (↓ここより下はtopic-b由来なので処遇を問われている)
pick 222222 トピックブランチAの2番目のコミット
pick 333333 トピックブランチAの3番目のコミット
pick 444444 WIP: トピックブランチBでの実装中のコミット
```

というイメージです。

トピックブランチB側に存在している`222222`, `333333`, `444444`の扱いをどうしますか？残しますか？と問われているので、今や古くなってしまっているトピックブランチA由来の2つのコミット行を削除します。

```
pick 444444 WIP: トピックブランチBでの実装中のコミット
```

これでエディタを保存して抜けると、晴れて以下のコミットログになります。

```
888888 WIP: トピックブランチBでの実装中のコミット
777777 トピックブランチAの3番目のコミット
666666 トピックブランチAの2番目のコミット(修正済み!)
111111 トピックブランチAの1番目のコミット
...
```

前述の手順の場合、トピックブランチA側のコミットID(`666666`)をコピーしておくなどの前準備が必要になりますが、この手順の場合はそんな手間も不要になります。
こっちの方が洗練されてますね。

`rebase -i`の引数に自分のブランチにはないコミットIDを指定するという頭がなかったので目からウロコです。


## おわりに

というわけで、ちょっとお行儀は悪いものの、マージしてないトピックブランチAを元にトピックブランチBを生やして、前のめりで実装を進める場合に便利なTIPSの紹介でした。

明日は、@korodroid による「海外活動を通じて得た英語の学び方＋α」です。引き続き、[NTTテクノクロスアドベントカレンダー 2021](https://qiita.com/advent-calendar/2021/ntt-tx)をお楽しみください。


[^1]: 仕事ではGitlabユーザーなのでマージリクエストと書いてます。GitHubではPull Requestですね。
[^2]: Gitlab上でマージするとマージコミットが作られますが、コミットグラフが変更になるだけで、コンフリクトはせずにしれっと`rebase`が完了します。
[^3]: 親コミットが変わるとコミットオブジェクト自体は作り直されるので、コミットIDも変わります。
[^4]: トピックブランチB側の実装コードが、トピックブランチAの2番目と3番目のコミットに依存している場合、`rebase -i`で削除しようとした時点でコンフリクトします。
[^5]: トピックブランチB側がトピックブランチAの2番目のコミットに対する追加修正の内容にピンポイントで依存してる場合はコンフリクトしてしまいますが、それはどうしようもないので、頑張ってコンフリクト解消しましょう。
[^6]: 会社の席は隣ですが、昨今の事情等により基本的に在宅勤務なので、物理的には遠方の人です。