【Git】merge 済みなのに差分だらけ? 「Already up to date」と Revert の落とし穴
はじめに
-
概要:
git mergeをしても "Already up to date" と言われるのに、git diffを見ると差分がある現象に遭遇した。 -
結論: 過去に
mainブランチで行った「Revert」が原因で、mergeの判定とファイル内容がズレて見える状態になっていた。
1. 発生した事象
develop には入っているはずの実装が、main では反映されていないように見える箇所があった。
「まだ develop の変更が main に取り込まれていないのでは?」と考え、改めて develop を main にマージしようとしたところ、次のような挙動になった。
$ 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 側で積まれた追加コミット
実際にやっていたことを、時系列で並べるとこうなります。
- C1 を feature ブランチで開発
- feature を main にマージ(C1 の機能が main に入る)
- その後 main で、何らかの理由で C1 を Revert(C2)
- 一方で、その機能を含むブランチを develop に取り込み(C1 が入る)
- 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