はじめに
過去、似たような状況でやらかしたことがあります。長期放置ブランチをマージするときに、enum の値を「両方残すべき場所」で「片方だけ採用」してしまい、数か月後にバッチ検証で NoSuchFieldError を出した、というやつです。
「2年以上前に止まった案件のブランチがあるんだけど、最新の develop にマージしてくれ」 — そんな依頼が現場では普通に飛んできます。本当に怖いのは「コンフリクトマーカーが出る場所」そのものではなく、「コンフリクトマーカーを潰した後に、こっそり壊れる場所」 のほうです。
以下、当時練り上げた戦略をまとめます。過去の障害履歴から「重点チェックファイル」を逆算してリスト化する、人力で効くアプローチです。
先にまとめ
長期放置ブランチのマージで効くのは Git の機能ではなく、過去の障害票・リリースメモの棚卸しです。手順は3つです。
- 障害票を「ファイル名 × 出現回数」でランキング化し、「重点チェックファイル」を作る
- 重点ファイルは、機械的にコンフリクトが解消できても目視レビュー必須にする
- リスト化したものを
docs/merge-checklist.mdとしてコミットし、次の担当者に継承する
状況:2年放置ブランチの実態
まず、想定している「放置ブランチ」のスケール感を共有しておきます。
| 項目 | 数値 |
|---|---|
| 放置期間 | 約2年 |
| 関連リポジトリ | 数本(業務ロジック系・フロント系を含む) |
| 案件フォルダの規模 | 数千ファイル / 数百MB級 |
| 経緯 | リリース試行 → トラブル発覚で保留 → 再テスト → さらに保留 |
捨てて作り直す案も当然浮かびます。しかし、業務上のロジック改修が大量に積み上がっていて、ゼロから作り直す方が遥かにコストが高い、というのが現場の実情です。さらに、インフラのクラウド移行が控えていて、「移行後の新基盤で初回本番化する」という旗が立ってしまった。退路は無い、というやつです。
なぜ「機械的なマージ」では不十分なのか
長期放置ブランチをマージするとき、Git が教えてくれる情報は「文字列が衝突した行」だけです。これは想像以上に頼りない情報源です。
過去にやらかした事例
冒頭で触れた失敗を、もう少し詳しく書きます。状況を一般化するとこうです。
- リリース準備中のブランチ A(業務改修)と、別の小さな改修ブランチ B(フロント系の修正)が並行で進んでいた
- B が先にリリースされ、A は B の変更を取り込み直すために手動マージが必要になった
- マージ中、ある enum クラス(メール送信の種別を表す定数集合)に B 側で追加されていた値が、取込み漏れのまま push された
- コンフリクトマーカーは出ていた。しかし、両方の enum 値を統合すべき場面で、人間が片側のブロックだけを採用してしまい、もう片側にあった値が取り込まれなかった
- 数か月後、別件で OS の EOL 対応のバッチ検証をしていたら
NoSuchFieldErrorが出て発覚
ここで効いてくる教訓はひとつだけです。
Git のコンフリクト解消は「差分の場所を教える」までしかやってくれない。両方残すべきか、片方で良いかは、人間の業務知識でしか判断できない。
そして大事なのは、「人間がミスりやすいファイルは、過去にも同じファイルでミスっている」 という経験則です。enum を集約するクラス、DTO の共通定義、バッチのエントリポイント、*.properties 系の設定。これらは何度マージしても、何度かに一度は取込み漏れが起きます。
戦略:障害履歴から「重点チェックファイル」を逆算する
長期放置ブランチのマージで、私が最初にやるのは git merge でも git rebase でもなく、「過去の障害票・リリースメモの棚卸し」です。
ステップ1:障害票・リリースメモを集める
集める対象は以下です。多ければ多いほど良いですが、直近2〜3年分あれば充分なリストが作れます。
- 過去のリリース後トラブル票(特に「マージ起因」「取込み漏れ」「デグレ」とラベル付けされているもの)
- 過去の MR/PR コメント(「ここコンフリクト解消したけど自信ない」のような自白系コメントは金鉱)
- 過去の手戻り作業履歴(コードレビューで指摘されて戻ったマージコミット)
- リリース後の hotfix 履歴
ステップ2:ファイル単位で出現回数をカウントする
集めた情報を、ファイル単位で「トラブル発生実績」として表にします。SQL や grep ですぐ作れます。
重点ファイル候補(出現回数:直近2年)
==========================================
src/.../A/AMain.java 4回
src/.../A/ADomain.java 3回
src/.../dto/BaseDto.java 2回
src/main/resources/application.properties 2回
db/migration/Vyyyymmdd__add_columns.sql 1回
...
ポイントは「今回の差分にこのファイルが含まれているか」ではなく「過去にトラブったか」で先にリストを作ることです。順序を逆にすると、「今回は差分に出てきてないから安全」という錯覚が生まれます。それが落とし穴で、放置ブランチをマージする際に新たに差分に含まれるパターンが普通にあります。
ステップ3:重点ファイルに「なぜ危ないか」のメモを添える
リストだけだと、レビュー時に「ふーん」で流されます。なので各ファイルに、過去のトラブル内容を1〜2行で添えておきます。
## 重点チェックファイル
### src/.../A/ADomain.java
- 過去事例: 数年前、別ブランチで追加された enum 値(メール送信種別)が手動マージで取込み漏れ
- チェック観点: enum / 定数の追加削除を全件目視で突き合わせる
- 検証方法: マージ後に当該 enum を参照しているバッチをローカル実行し、`NoSuchFieldError` が出ないこと
### src/main/resources/application.properties
- 過去事例: プロパティ追加が片方のブランチで漏れ、本番起動時にキー未定義で起動失敗
- チェック観点: キーの増減を A/B 2-way diff で並べて確認
- 検証方法: マージ後の起動時ログにキー未定義の警告が出ていないこと
このリストを MR の説明欄テンプレートに貼り付けるか、リポジトリ内に docs/merge-checklist.md として残します。「機械的にコンフリクトが出なくても、ここだけは必ず目視でレビューする」という運用ルールにする、ということです。
マージ計画のフェーズ分割
重点チェックファイルリストが出来上がったところで、ようやくマージ計画に入ります。今回作った計画書は最終的に10数セクション・数百行規模の md ファイルになりましたが、コアになるのは Phase 1 と Phase 3 です。
Phase 1:事前調査(ここに一番時間を使う)
Phase 1 でやるのは「マージ作業」ではなく「マージ前の地ならし」です。
| 調査項目 | 何を見るか |
|---|---|
| ブランチ差分 |
git log --oneline develop..feature/legacy でコミット数・期間を把握 |
| 依存関係 | 共通ライブラリ・親子リポジトリ間のバージョン差 |
| DB スキーマ | マイグレーション差分。develop 側に未適用のマイグレーションがないか |
| API | 公開エンドポイントの破壊的変更の有無 |
| 運用設定 | cron スケジュール、Slack 通知先、環境変数の差分 |
| 試験資材 | 結合試験書のうち重点ファイル関連の項目はどれか |
Phase 1 が雑だと、Phase 3 で必ず痛い目を見ます。「コンフリクトを早く解消したい」という焦りに負けて事前調査をスキップしがちですが、ここで2日かけたほうが、後工程で5日節約できる、というのが体感です。
Phase 3:マージ実施を4段階に分ける
Phase 3 は一発勝負にせず、4段階に分けます。
Phase 3a: ブランチ作成(develop の最新から派生)
Phase 3b: MR 作成(コンフリクトをわざと顕在化させる)
Phase 3c: コンフリクト解消(IDE で慎重に)
Phase 3d: 解消後の差分を別ファイルに記録
ポイントは 3b の「あえて MR を一度作って、コンフリクトを可視化する」です。ローカルでマージを試すと、コンフリクトの全体像がコマンドラインの出力に流れて消えていきます。GitLab/GitHub の MR UI 上に出すと、コンフリクトしたファイルがリスト化されて画面に残り続けるので、レビューの起点として使いやすいです。
ハイブリッドワークフロー:ブラウザと IDE の役割分担
実作業は、ブラウザと IDE を意図的に使い分けます。
| 作業 | ツール | 理由 |
|---|---|---|
| ブランチ作成 | ブラウザ(GitLab/GitHub) | URL がそのまま記録になる |
| MR 作成 | ブラウザ | コンフリクトリストを UI で見たい |
| 差分閲覧 | ブラウザ | レビュアーと URL で共有できる |
| コンフリクト解消 | IDE(Eclipse / VSCode) | シンタックスハイライト・型補完が効く |
| 解消後の差分検証 | IDE + ターミナル |
git diff で別ファイル保存 |
IDE だけで完結させた方が速い場面もあります。ただし、ブラウザに作業履歴を残しておくと、レビュアーと議論する際の起点が URL になります。「あの MR の、この差分の、この行について」が URL 一発で伝わるのは、想像以上に強いです。
解消後の差分を別ファイルに残す
コンフリクト解消が終わったら、IDE で次のようなコマンドを叩いて、解消差分をファイルとして保存します。
# 解消前後の差分を別ファイルに保存
git diff origin/develop...HEAD -- src/.../A/ADomain.java \
> docs/merge-records/ADomain.diff
このファイルを MR にコメント添付するか、リポジトリの docs/merge-records/ にコミットしておきます。「なぜこの行を残し、なぜこの行を捨てたか」を後から検証できる状態にすることが、再発防止の最後の砦になります。
将来の追加マージに備える運用設計
今回のマージブランチには、おそらく将来また別の塩漬け案件が乗ってきます。そのときに同じ苦労を繰り返さないため、いくつかの仕組みをリポジトリに残しておきます。
-
docs/merge-checklist.md:今回作った重点チェックファイルリストをコミット。次の担当者が継承できる -
docs/merge-records/:個別マージの差分記録を蓄積する場所を用意 - MR テンプレート:「重点チェックファイルが含まれている場合は、チェックリストに沿ったレビュー所見を本文に記載する」と書いておく
- CODEOWNERS(GitHub)/ Code Owners(GitLab Premium 以上):重点ファイル(enum クラス、DTO 共通定義、設定ファイル)に有識者を割り当てて、自動レビュアー指定する
CODEOWNERS が地味に効きます。レビュアー指定を仕組み化してしまえば、「うっかり一人で勝手にマージ」が物理的に発生しなくなります。
まとめ
長期放置ブランチのマージで本当に戦うべき相手は、Git ではなく「過去の自分たちの失敗」です。
| Before(よくあるやり方) | After(今回の戦略) |
|---|---|
git merge を叩いてコンフリクトを順に潰す |
障害票を棚卸しして「重点チェックファイル」を先に作る |
| コンフリクトマーカーが消えれば OK | 機械的に解消できたファイルも、重点リストにあれば目視レビュー |
| 解消した差分はマージコミットに消える |
docs/merge-records/ に解消差分を残す |
| ブラウザか IDE のどちらかで完結させる | ブラウザに作業履歴、IDE に解消作業、と役割分担 |
特に効くのは、過去のトラブル票を grep して「ファイル名の登場回数」でランキングを作るという、地味すぎて誰もやらないアプローチです。AI に計画書を書かせる場合も、コードよりも先に障害票・リリースメモを食わせると、重点ファイル抽出の精度が一気に上がります。
最後にひとつだけ。長期放置ブランチのマージは、終わってからが本番です。今回作った重点ファイルリストを残せたかどうかで、次に同じ状況に陥った後輩を救えるかが決まります。Git のログには残らない、人と人の引き継ぎだけがそれを可能にします。