14
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-12

この記事はNTTテクノクロスアドベントカレンダー 2021 13日目の記事です。

はじめに

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

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

状況設定

  • トピックブランチ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をお楽しみください。

  1. 仕事ではGitlabユーザーなのでマージリクエストと書いてます。GitHubではPull Requestですね。

  2. Gitlab上でマージするとマージコミットが作られますが、コミットグラフが変更になるだけで、コンフリクトはせずにしれっとrebaseが完了します。

  3. 親コミットが変わるとコミットオブジェクト自体は作り直されるので、コミットIDも変わります。

  4. トピックブランチB側の実装コードが、トピックブランチAの2番目と3番目のコミットに依存している場合、rebase -iで削除しようとした時点でコンフリクトします。

  5. トピックブランチB側がトピックブランチAの2番目のコミットに対する追加修正の内容にピンポイントで依存してる場合はコンフリクトしてしまいますが、それはどうしようもないので、頑張ってコンフリクト解消しましょう。

  6. 会社の席は隣ですが、昨今の事情等により基本的に在宅勤務なので、物理的には遠方の人です。

14
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?