ひびのログ

日々ではないけどログを出力していくブログ

git rebase でファイル内容が同じなのにコンフリクトする時。そしてそれをコマンドで解消する方法

f:id:tee-talog:20210605221244p:plain

git rebase 使ってる?

git rebase は、「今のブランチを他のブランチにくっつける」という便利なコマンド。
main(master)ブランチにマージする前に rebase する、という運用をしている人も少なくないと思う(というか自分がそう)

これをやると、GitHub などでマージするときに起きるコンフリクトを先に解消できる。
手元でコンフリクトを解消するので、マージ後のコードの内容と同一であることが担保できたり、手元で動作確認できたりする。

逆に、過去のコミットを編集=改竄するということなので、使わないほうがいいという人もいるとのこと。
各々・プロジェクト毎によしなに対応してもらえればと思う。

ファイルの内容が同じなのにコンフリクト!?

さて、rebase を使うとして、場合によっては「マージ後のファイルの内容が同一で、マージではコンフリクトが起きないのに、rebase ではコンフリクトする」という現象が発生する。

具体的にどのような操作を行うと発生するのか、例を見ていく。

実際の操作

  1. main ブランチから、開発ブランチ 1 を切る
  2. 開発ブランチ 1 に「変更 A」を加える
  3. 開発ブランチ 1 に「変更 B」を加える
  4. master ブランチから、開発ブランチ 2 を切る
  5. 開発ブランチ 2 に「変更 A + 変更 B」を加える
  6. 開発ブランチ 2 を main にマージする
  7. 開発ブランチ 1 を main に rebase する
  8. コンフリクト発生!

「変更 A + 変更 B」は、開発ブランチ 1 に加えた「変更 A」「変更 B」と同じ。

ブランチの状態

─────┬───────────────────◯───────── main
     │────────────────◯─-┘      :  開発ブランチ 1
     │             変更 A + B    :
     │                           :
     └──◯────────◯───────────●─-┘  開発ブランチ 2
       変更 A   変更 B      【ここ】

【ここ】の部分で git rebase main をするとコンフリクトする。
ちなみに git checkout main && git merge 開発ブランチ 2 ではコンフリクトしない。

リポジトリ

実際に簡単なコードで再現したものを用意した。

github.com

ブランチ名は以下のように読み替えてほしい。 * 開発ブランチ 1 → feature/add-user-route * 開発ブランチ 2 → feature/add-route

feature/add-route ブランチでルーティングを追加しようとしたが、同じブランチの途中で「ログ出力」という別の処理を追加してしまった。 そこで、「ルーティングの追加(feature/add-user-route)」「ログ出力の追加(feature/add-route)」のブランチに分けようとした結果、今回問題となっている現象が発生してしまった。

git checkout main && git merge feature/add-route とすると、コンフリクトは発生しないが、 git checkout feature/add-route && git rebase main とすると、コンフリクトが発生する。

なんで rebase するときにコンフリクトするのか

マージは、「最終的に出来上がった変更を、マージ先のブランチに 1 コミットで適用する」という動き。
rebase は、「今のブランチを rebase 先のブランチから全コミット適用し直す」という動き。

細かく書くと、git rebase main を実行すると、main ブランチの内容に対して「変更 A をコミット → 変更 B をコミット」という動きになる。コミットが 1 回で終わらない。

「変更 A + B」が適用済みの main ブランチに対して、「変更 A」の状態のファイルをコミットしようとする。
rebase 先・元のファイル内容が異なるのでコンフリクトする。

じゃあどうすればいい?

結論としては、「コンフリクトしたら、rebase 元ブランチ(今回の例では開発ブランチ 2)の内容を適用する」。
開発ブランチ 1 の内容はすべて開発ブランチ 2 の変更として入っているので、開発ブランチ 2 の内容を適用していけば、最終的なファイル内容は同じになる。

とはいえ、ファイルがたくさんあるといちいちファイルの中身を手動で変更するのは面倒。なのでコマンドでサクッとやっちゃう。

コマンドでのコンフリクト解消

git checkout --theirs コンフリクトしたファイル名 を連打
コンフリクトしているファイルが無くなったら git add . && git rebase --continue

git checkout --theirs は、「コンフリクトしている相手の変更を適用する」の意味。
開発ブランチ 2 上で git rebase main コマンドを打っているが、実際には main ブランチに移動してから開発ブランチ 2 のコミットを打ち直しているので、「自分」は main ブランチになっている(と私は理解している)。

ちなみに、main ブランチ側を適用したいなら git checkout --ours コンフリクトしたファイル名 でできる。

これで君も rebase マスターだ!!!!!!!!!

P.S.

なんでマージはカタカナ表記なのにリベースは英語表記なんだろうか……(なにも考えてない)