【Git】コミット履歴が二重化する原因と修正方法|–cherry-mark検出・rebase整理・PR戦略統一まで完全ガイド

【Git】コミット履歴が二重に並んでしまったときの原因と修正方法 Git

git log --graph --allで履歴を眺めたら「同じコミットメッセージが2回並んでる」「変更内容は同じなのにSHAが違うコミットが横に並んでいる」——そんな履歴の二重化を見かけたことはありませんか?

二重化した履歴の例
*   9f8e7d6 Merge branch 'main'
|\
| * a1b2c3d fix: ログインバグ修正   ← 2回目(rebase後)
* | a9b8c7d fix: ログインバグ修正   ← 1回目(元)
|/
*   e4f5g6a refactor: 共通化

これはrebase/cherry-pickで生成された「同内容だが別SHA」のコミットをその後mergeで取り込んでしまった結果です。Gitは内容ではなくSHAでコミットを識別するため、同内容でも別SHAなら「別のコミット」扱いになり、履歴グラフに並行して現れてしまいます。

この記事では、履歴二重化の原因5パターンと診断方法、--cherry-markで重複を検出するテクニック、未push/push済みそれぞれの整理手順、そして同じ問題を繰り返さないチーム運用ルールまで実務で役立つ形で整理します。

この記事で学べること

  • なぜ履歴が二重化するのか(SHA生成の仕組み)
  • 5つの発生パターンと特徴的なグラフ形状
  • --cherry-mark--cherry-pickで重複検出
  • 未push履歴の直線化手順(rebase/rebase-merges)
  • push済み共有履歴で書き換えるかの判断基準
  • PR運用での再発防止:Squash/Rebase/Merge strategyの選択
  • reflog+backup branchによる安全な作業法
スポンサーリンク

なぜ履歴が二重化するのか

GitのコミットIDは内容+親+メタデータのハッシュで決定されます。そのため、git rebasegit cherry-pickで「同じ変更を別の親の上に載せ直す」と、内容は同じでもSHAが変わった新しいコミットが生成されます。元のコミットが別のブランチに残ったまま、これらをmergeで統合すると二重に見えるわけです。

rebase/cherry-pickのSHA変化
# 元のコミット
A - B - C (SHA: abc123)

# B を別の親 X の上にrebase
A - B - C
     \
      X - C' (SHA: xyz789)  ← 内容は C と同じだが別SHA

# 両者をmergeすると履歴に両方残り二重化
*   Merge
|\
| * C  (abc123)  ← 元
* | C' (xyz789)  ← rebase版

ポイント:Gitは「内容」ではなく「SHA」でコミットを識別します。そのため同じ変更を何度もrebase/cherry-pickすると、別SHAで何回もコピーが生まれます。本来rebaseは「元を置き換える」ものですが、元が別ブランチに残ったままmergeで合流させると二重に映るというのが本質的な原因です。

二重化が起きる5つの典型パターン

パターン 典型フロー
① rebase後のmerge 自分のbranchをrebase→その後mainをmerge
② cherry-pick後のmerge 他branchからcherry-pick→元のbranchをmerge
③ PR Squash後のローカル残存 GitHubでSquash merge→ローカルに旧commitが残っている
④ rebaseとmergeを両方実施 rebase中にpull(merge)してしまった
⑤ 強制push失敗後の誤同期 チームメンバーが古いpullを統合した

典型的な危険サイン

  • git logで同じメッセージが2回出現
  • グラフで枝が合流してから同内容コミットが並行
  • git log --cherry-mark=マーク多数
  • PR作成時の差分が想定より大きい

STEP 0:–cherry-markで重複を可視化

Gitには「同内容のコミットを検出する」専用オプションがあります。--cherry-mark--cherry-pickを使えば、「どれが重複しているか」が一目で分かります。

cherry系オプションで診断
# 最新情報取得
git fetch --prune

# 左右差分+重複マーク
git log --oneline --cherry-mark --left-right origin/main...HEAD
# 出力例:
# < abc1234 fix: Aの変更   ← origin/main 側固有
# > def5678 fix: Aの変更   ← HEAD 側固有
# = 9f8e7d6 refactor: B    ← 両方にある(同内容)

# 重複しているものを除外して表示
git log --oneline --cherry-pick origin/main...HEAD
# "="マーク付きが除外されて、真にユニークなコミットだけ残る

# グラフで視覚的に確認
git log --oneline --graph --decorate --all --cherry-mark -30

# 最近の操作履歴から二重化トリガーを特定
git reflog -30

cherry-mark の記号

  • <:左側(origin/main)固有のコミット
  • >:右側(HEAD)固有のコミット
  • =:両方にある「同内容のコミット」(二重化候補)
  • ---cherry-pickで除外される重複

整理①:未pushの二重化はrebaseで直線化

まだリモートに公開していない作業ブランチの二重化は、git rebase origin/mainで本線の上に自分の変更を並べ直すのがもっとも安全。重複コミットはGitが自動的に検出してスキップします。

rebaseで直線化
# 最新取得
git fetch --prune

# バックアップ(必須ではないが推奨)
git branch backup/before-flatten

# rebase(重複は自動スキップされる)
git rebase origin/main

# conflictが出たら解消→continue
git add .
git rebase --continue

# 結果確認
git log --oneline --graph -10

対話的rebaseで手動統合

rebase -i でdropまたはsquash
# origin/main を基点にインタラクティブ rebase
git rebase -i origin/main

# エディタで:
# pick a1b2c3d fix: Aの変更
# drop d4e5f6a fix: Aの変更   ← 重複を drop
# pick g7h8i9j refactor: B

# マージコミットを保持したいなら
git rebase --rebase-merges -i origin/main

–rebase-mergesの使い所

featureブランチの統合点など意味のあるmergeコミットを残しつつ重複を整理したいときは--rebase-mergesを使います。通常のrebaseだとmerge commitが消えてしまうので、複雑な履歴の整理には必須です。

整理②:push済みの二重化は慎重判断

共有ブランチ(main/develop)の履歴書き換えはチームに大きく影響します。原則はそのまま運用し、以降の取り込み方針を改善するのが健全。どうしても整理が必要なら、影響範囲の確認+チーム合意が前提です。

push済みの整理フロー(例外運用)
# 1. チーム全員に事前通知(作業退避を依頼)
# 2. 整理用ブランチを切る
git switch -c fix/flatten-history

# 3. 整理
git rebase --rebase-merges -i origin/main

# 4. PRで変更内容を確認
git push -u origin fix/flatten-history

# 5. 合意後に main へ反映(force-with-lease)
git switch main
git fetch origin
git reset --hard fix/flatten-history
git push --force-with-lease

# 6. 他メンバーは再同期
# 各自で git fetch && git reset --hard origin/main

警告:共有ブランチのforce pushはCI/webhook/リリースタグ/他メンバーのローカル作業すべてに影響します。「履歴の見た目を綺麗にするため」だけの書き換えはコストに見合わないことが多く、諦めて運用改善に切り替える方が賢明なケースが大半です。どうしても必要ならpushを取り消す方法も参照。

整理③:PR Squash後はブランチ作り直しが速い

GitHub等でSquash mergeで取り込まれた後、ローカルに旧commit列が残っていると、次回pullや再rebaseで「mainにある同内容コミット」と「ローカルの旧コミット列」が衝突しがち。作業ブランチを作り直すのが最も早く確実です。

PR Squash後の作り直し
# 現在の作業を退避(必要なら)
git branch backup/old-work

# mainを最新化
git switch main
git pull origin main

# 作業ブランチを削除
git branch -D feature/xxx

# 最新mainから作り直し
git switch -c feature/xxx

# 新規に作業を続ける

ポイント:PR Squash mergeは「ブランチの全変更を1コミットに圧縮して本線に取り込む」破壊的統合です。ローカルの作業ブランチはすでに「本線に取り込まれた」ので、継続するなら作り直しが鉄則。PR運用の詳細はpull後にマージコミットが大量発生する原因と履歴整理方法も参照。

再発防止:取り込み戦略をチームで統一

二重化の最大の原因は「取り込み方針の不統一」です。rebaseとmergeが混在して使われると、同内容コミットが生まれやすくなります。チームで方針を固定しておけば二重化は激減します。

推奨設定
# pullは常にrebase(直線履歴派)
git config --global pull.rebase true

# FFできない時はpullを止める(安全優先派)
git config --global pull.ff only

# rebase時の未ステージ変更を自動退避
git config --global rebase.autoStash true

# 参照掃除
git config --global fetch.prune true

PR運用で二重化を防ぐルール

  • PRはSquash mergeに統一してmainをクリーンに保つ
  • PRマージ後は作業ブランチを削除(GitHub自動削除)
  • PRがmergeされた後の継続作業は新ブランチで
  • cherry-pickは一時的な救済措置に限定、本番運用は避ける
  • rebaseの結果をpullで取り込まない(pull –rebaseが安全)

実践シナリオ

シナリオ① 自分のbranchで二重化が発覚(未push)

rebaseで整理
git fetch --prune
git log --cherry-mark --left-right origin/main...HEAD
# "=" マーク付き多数 → 二重化確認

# 整理
git branch backup/current
git rebase origin/main
git log --oneline --graph -20

シナリオ② PRマージ後の作業継続で二重化

ブランチ作り直し
git switch main
git pull origin main
git branch -D feature/old
git switch -c feature/new
# 新鮮な状態から作業再開

シナリオ③ 共有mainに二重化commitが押し込まれた

運用で対処
# 履歴書き換えは避ける
# 今後のpullは必ず --rebase
git config --global pull.rebase true

# チームに周知:PR Squashで整理される

シナリオ④ cherry-pickで散在した重複を整理

統合専用ブランチ
git switch -c consolidate/fix
git rebase -i origin/main
# 重複を drop / squash で整理

# 綺麗にしたブランチでPR作成
git push -u origin consolidate/fix

やってはいけない落とし穴

二重化が気になって共有mainをforce push

履歴の見た目を綺麗にするためだけにmain/developをforce pushすると、他メンバーのローカル環境を破壊します。push済み履歴は基本そのまま、以降の取り込み方針で改善を。

rebase中にpullでmergeを走らせる

rebase途中でgit pull(既定はmerge)すると、rebase結果とリモートの両方がmergeで統合されて二重化が加速します。rebase中はpullせず、最後までrebaseを完了させてからpush。pull.rebase=true設定で事故を防げます。

cherry-pickを常用する

cherry-pickは便利ですが、本番運用では二重化の元凶です。「片方だけにバグ修正を入れたい」など特別な理由以外は、マージやrebase --ontoで正規の統合をしましょう。どうしてもcherry-pickする場合は「元を削除」まで含めて設計すること。

PR mergeで作業ブランチを削除しない

PRがmergeされた後も作業ブランチを残したまま続きを開発すると、main側のSquash commitとローカルの旧コミット列が衝突して二重化します。PRマージ後は必ず作業ブランチを削除し、新作業はmainから新ブランチを切って始めましょう。

backup branch を作らずにrebaseを開始

rebase中に想定外の状態になり、git rebase --abortでも戻れないケースがあります。rebase前にはgit branch backup/before-rebaseで退避ブランチを切っておくと、git reset --hard backup/before-rebaseで一発復帰できます。

よくある質問

Q–cherry-markと–cherry-pickの違いは?
A--cherry-markは同内容コミットに=マークを付けて表示--cherry-pickは同内容コミットを除外して表示します。「何が重複してるか見たい」ならmark、「重複以外のユニークなコミットだけ」ならpick。
Qrebaseで勝手に重複が消える?
AGitのrebaseは同内容(=パッチ内容が同じ)のコミットを自動検出してスキップします。「既に取り込まれている変更」と判定されたコミットはPatch is already appliedでスキップされるので、未push段階でrebaseすれば重複は整理されます。
QPR Squash後に自分のブランチが古くなった
Aブランチを作り直すのが最速。git branch -D oldgit switch -c new。継続する必要があればgit rebase origin/mainで整理し、重複commit群をdropで整理します。
Q共有mainに二重commitが入ってしまった。消せる?
A技術的には可能ですが推奨しません。force pushは他メンバーのローカル/CI/タグに影響するため、「そのまま運用」+「今後のPR/pull戦略を改善」が現実的。どうしても必要なら全員合意+再clone指示が前提です。
Qrebaseとmergeの使い分けは?
A個人ブランチはrebaseで直線化、共有ブランチへ取り込む際はmerge(またはsquash merge)が一般的。詳細はrebaseとmergeの違いと使い分けを参照。
Qcherry-pickで二重化しないコツは?
A取り込み先と元の両方に残したくない場合は、cherry-pick後に元コミットをgit rebase -idropするか、git rebase --ontoで意図的にコミットを移動させるのが本道です。
Qrebase中にpullでさらに二重化してしまった
Agit rebase --abortで元に戻し、設定をpull.rebase=trueにしてから改めてrebase→pushの順で進めましょう。

関連記事

まとめ

  • 二重化の原因はrebase/cherry-pick後のmergeで別SHA同内容が並行
  • 5パターン:rebase+merge/cherry-pick+merge/Squash後残存/rebase中pull/force push失敗
  • 診断は--cherry-markで重複検出、=マーク確認
  • 未pushならgit rebase origin/mainで自動的に整理される
  • push済み共有履歴は書き換えずに運用改善で対処が無難
  • PR Squash後はブランチ作り直しが速い
  • 再発防止:pull.rebase=true、PRはSquash統一、PR後ブランチ削除

履歴の二重化は「rebaseとmergeを混在させた結果」に起因する見た目の問題ですが、蓄積するとレビュー・bisect・リリースノート作成に支障が出ます。--cherry-markで定期的にチェックし、チームで取り込み戦略を統一(Squashマージ・pull –rebase等)すれば二重化は自然と減ります。すでに発生したものは未pushならrebaseで整理、push済みなら運用改善で次に進むのが現実的な判断です。