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 rebase/git cherry-pickで「同じ変更を別の親の上に載せ直す」と、内容は同じでもSHAが変わった新しいコミットが生成されます。元のコミットが別のブランチに残ったまま、これらをmergeで統合すると二重に見えるわけです。
# 元のコミット
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つの典型パターン
典型的な危険サイン
git logで同じメッセージが2回出現- グラフで枝が合流してから同内容コミットが並行
git log --cherry-markで=マーク多数- PR作成時の差分が想定より大きい
STEP 0:–cherry-markで重複を可視化
Gitには「同内容のコミットを検出する」専用オプションがあります。--cherry-mark/--cherry-pickを使えば、「どれが重複しているか」が一目で分かります。
# 最新情報取得 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が自動的に検出してスキップします。
# 最新取得 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で手動統合
# 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)の履歴書き換えはチームに大きく影響します。原則はそのまま運用し、以降の取り込み方針を改善するのが健全。どうしても整理が必要なら、影響範囲の確認+チーム合意が前提です。
# 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にある同内容コミット」と「ローカルの旧コミット列」が衝突しがち。作業ブランチを作り直すのが最も早く確実です。
# 現在の作業を退避(必要なら) 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)
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で一発復帰できます。
よくある質問
--cherry-markは同内容コミットに=マークを付けて表示、--cherry-pickは同内容コミットを除外して表示します。「何が重複してるか見たい」ならmark、「重複以外のユニークなコミットだけ」ならpick。Patch is already appliedでスキップされるので、未push段階でrebaseすれば重複は整理されます。git branch -D old→git switch -c new。継続する必要があればgit rebase origin/mainで整理し、重複commit群をdropで整理します。git rebase -iでdropするか、git rebase --ontoで意図的にコミットを移動させるのが本道です。git rebase --abortで元に戻し、設定をpull.rebase=trueにしてから改めてrebase→pushの順で進めましょう。関連記事
- 【Git】pull後にマージコミットが大量発生する原因と履歴整理方法 — マージコミット増殖の類似問題
- 【Git】rebaseとmergeの違いと使い分け — 統合戦略の基礎
- 【Git】リモートとローカルの履歴が食い違ったときの同期方法 — pull戦略
- 【Git】mergeコミットを取り消して履歴を元に戻す方法 — マージ取消
- 【Git】リベース途中でエラーになったときの復旧方法 — rebase中のトラブル
- 【Git】特定のコミットまで戻す方法 — reset系の復旧
- 【Git】pushを取り消す方法 — push済み変更の対処
- 【Git】よく使うgitコマンドまとめ — 日常コマンドの早見表
まとめ
- 二重化の原因は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済みなら運用改善で次に進むのが現実的な判断です。

