🫣

【Git】merge 済みなのに差分だらけ? 「Already up to date」と Revert の落とし穴

に公開

はじめに

  • 概要: git merge をしても "Already up to date" と言われるのに、git diff を見ると差分がある現象に遭遇した。
  • 結論: 過去に main ブランチで行った「Revert」が原因で、merge の判定とファイル内容がズレて見える状態になっていた。

1. 発生した事象

develop には入っているはずの実装が、main では反映されていないように見える箇所があった。
「まだ develop の変更が main に取り込まれていないのでは?」と考え、改めて developmain にマージしようとしたところ、次のような挙動になった。

$ git checkout main
$ git merge develop
Already up to date.

$ git diff main develop
# -> ここで大量の差分(未反映に見えるコード)が表示される
  • git merge develop
    → 「develop のコミットはすでに main の履歴に含まれている」と判定される
  • git diff main develop
    → 「main を develop の内容に近づけるための差分」が表示される

このため、

  • 履歴的には「取り込むべきコミットは無い」
  • ファイル内容としては「両ブランチの差分がまだ存在する」

という、直感的には分かりにくい状態になっていた。


2. 原因:Revert を含む複雑な履歴

図に出てくるコミットの役割はざっくり次の通りです。

  • C0: 初期コミット
  • C1: 機能追加(feature)
  • C2: main 上で C1 を打ち消す Revert
  • C3, C4: C1 を前提に develop 側で積まれた追加コミット

実際にやっていたことを、時系列で並べるとこうなります。

  1. C1 を feature ブランチで開発
  2. feature を main にマージ(C1 の機能が main に入る)
  3. その後 main で、何らかの理由で C1 を Revert(C2)
  4. 一方で、その機能を含むブランチを develop に取り込み(C1 が入る)
  5. develop 側で C1 を前提に C3, C4 を追加

この状態で develop → main をマージしようとすると、
「履歴の見え方」と「ファイル内容の見え方」がズレます。

履歴の観点(git merge が見ているもの)

  • main の履歴上には、いったん C1 が入り、その後 C2 で Revert されています。
  • develop 側のコミット(C1, C3, C4 など)は、コミットグラフ上では main からも辿れる状態になっています。

そのため Git は、

「履歴上、新しく取り込むべきコミットは無い」

と判断し、git merge develop に対して Already up to date. を返します。

ファイル内容の観点(git diff が見ているもの)

一方で、実際のファイル内容は次のようになっています。

  • main: 「C1 の変更を C2 で打ち消した状態」
  • develop: 「C1 + C3 + C4 の変更が反映された状態」

つまり、

  • main は「機能を Revert した状態のファイル」
  • develop は「機能を含んだままさらに開発を進めたファイル」

になっているため、git diff main develop を見ると C1 周りを中心に大きな差分が残ります。

ここでのポイントは次の 2 つです。

  • merge の判定は「コミットが履歴に含まれているか」を見ている
  • diff は「現在のファイル内容の差」だけを見ている

Revert によって「履歴には含まれているが、内容は打ち消されているコミット」があると、
この 2 つの観点のズレが表面化し、「merge は最新と言うのに diff では差分だらけ」という状態になります。


3. 解決策

このような状態で「機能を main に復活させたい」ときの代表的な方法は 2 つ。

方法A:develop のファイル内容を強制的に反映する

履歴よりも「今の状態」を優先して、develop の内容で main を上書きする。

# main ブランチにいる前提
$ git checkout main

# develop の内容で作業ツリーを上書き
$ git checkout develop .

# 差分を確認
$ git diff

# コミットして反映
$ git add .
$ git commit -m "fix: develop の内容を main に強制反映"
  • メリット: シンプルで分かりやすい
  • デメリット: develop に無い main 固有の変更も上書きしてしまう可能性がある
    → main 固有の差分が無い / 捨ててもよいことを確認してから使う

方法B:Revert を Revert する(履歴として復活させる)

Revert コミットそのものを打ち消し、履歴上も「復活させる」意思を記録する。

# main ブランチにいる前提
$ git checkout main

# 過去の Revert コミットを調査
$ git log --oneline --grep=revert

# 見つけた Revert コミットを指定して Revert を反転
$ git revert <Revert コミットのハッシュID>
  • メリット:
    • 「一度やめた機能を、改めて復活させた」ことが履歴に明確に残る
    • main 固有の変更を壊しにくい

4. まとめ

  • git merge の「Already up to date」は、
    対象ブランチのコミットが履歴上すでに含まれているか」で決まる。
  • git diff main develop は、
    現在のファイル内容の差分」だけを見る。
  • 一度 main で Revert した機能を含むブランチを、そのまま main にマージし直すと、
    • コミットグラフ上は「取り込み済み」
    • ファイル内容は Revert の影響で差分あり
      という状態になり得る。
  • 機能を復活させたい場合は、
    • develop の状態で上書きする
    • Revert コミットを Revert する
      のいずれかを選ぶ必要がある。

Revert を多用しているリポジトリでは、
「merge は最新と言うのに diff では大きく差が出る」という状況が起きやすいので、
困ったときはまず「過去に Revert していないか」を疑って git log を確認すると原因に近づきやすいよってお話でした。

株式会社ソニックムーブ

Discussion