データファイルにおけるマージコンフリクトという「悪夢」
もしあなたが、package-lock.jsonのコンフリクトを解消するために、一晩中ブラケット記号を凝視した経験があるなら、それはあなただけではありません。数千行のコードの中に<<<<<<< HEADといった記号が入り混じっている光景は、視覚的な悪夢そのものです。
ある古いプロジェクトで、チーム全員が締め切りに追われていた時のことを鮮明に覚えています。featureブランチをdevelopにマージした際、composer.lockファイルで激しいコンフリクトが発生しました。ツールを使わず、自信満々に手作業でコンフリクトマーカーを削除したのですが、カンマを一つ忘れただけで lockファイルの構成を壊してしまい、CI/CDシステムを2時間もストップさせてしまいました。この事故は進捗を遅らせただけでなく、チーム全体のフラストレーションを招きました。その時、私は悟ったのです。「Gitがより正確にできることに、人間が力技で挑んではいけない」と。
デフォルトでは、Gitは行単位(line-based)でコンフリクトを比較します。この仕組みはJava、Python、C++などでは完璧に動作します。しかし、JSONやXML、Lockfileのように構造が厳格なファイルでは、このアプローチはマージ後に構文を壊してしまうことがよくあります。そこで救世主となるのが、Git Merge Driverです。
Git Merge Driverとは一体何か?
Git Merge Driverを、データの衝突を処理するために個別に雇った「審判」だと考えてみてください。Gitに自動的に判断させてエラーを出させる代わりに、独自のロジックに基づいてコンフリクトを自動解決する外部スクリプトを指定するのです。
Gitが両方のブランチでファイルが変更されたことを検知すると、まず.gitattributesファイルをチェックします。そのファイルに専用のMerge Driverが割り当てられていれば、Gitはデフォルトのマージ機能ではなく、即座にそのドライバーを呼び出します。
Merge Driverの動作メカニズム
プロフェッショナルなドライバーは通常、Gitから3つの入力パラメータを受け取ります。
- %O: 共通の祖先(Ancestor) – 2つのブランチの最後の共通点。
- %A: 現在のブランチのバージョン(Ours)。
- %B: マージされるブランチのバージョン(Theirs)。
ドライバーはこれら3つのファイルを読み込み、ロジックを適用してデータを統合し、最終結果を%Aファイルに上書きします。
Git Merge Driverの詳細な設定ガイド
ステップ1:Git設定でドライバーを宣言する
チームメンバー全員と同期するために、プロジェクト単位(local)で設定することをお勧めします。ルートディレクトリでターミナルを開き、以下のコマンドを実行してください。
git config merge.json-merge.name "JSONファイルの自動マージツール"
git config merge.json-merge.driver "python3 path/to/json_merge_script.py %O %A %B"
ここで、json-mergeは識別子です。driverパラメータは、これから作成する処理スクリプトへのパスになります。
ステップ2:.gitattributesで処理権限を割り当てる
.gitattributesファイルを作成または更新して、新しいドライバーに「仕事を依頼」します。
*.json merge=json-merge
package-lock.json merge=json-merge
この行はGitに対して、「.jsonファイルでコンフリクトが発生したら、警告を出すのではなく、すぐにjson-mergeを呼び出して処理せよ」と命令しています。
ステップ3:インテリジェントな処理スクリプトを書く
以下は、JSONのキーを統合するためのシンプルなPythonスクリプトjson_merge_script.pyの例です。
import sys
import json
def merge_json(base_path, ours_path, theirs_path):
try:
with open(base_path) as f: base = json.load(f)
with open(ours_path) as f: ours = json.load(f)
with open(theirs_path) as f: theirs = json.load(f)
# ロジック:両方のキーを統合し、最新の変更を優先する
result = {**base, **ours, **theirs}
with open(ours_path, 'w') as f:
json.dump(result, f, indent=2)
sys.exit(0) # 成功
except Exception:
sys.exit(1) # 失敗。Gitに手動解決を任せる
if __name__ == "__main__":
merge_json(sys.argv[1], sys.argv[2], sys.argv[3])
Lockfile向けの「お手軽」ソリューション:Union Merge
スクリプトを書くのが面倒な場合、Gitには標準でunionドライバーが用意されています。このドライバーは、コンフリクトが発生した際に両方のコードを残します。これは.gitignoreのようなリスト形式のファイルに非常に有効です。
Lockfileに素早く適用するには、.gitattributesに以下を追加します。
package-lock.json merge=union
yarn.lock merge=union
注意:unionを使用すると、lockファイルがわずかに不整合を起こす可能性があります。マージ後にnpm installを再実行して、整合性を確認することをお勧めします。
専用ライブラリの使用(推奨)
車輪の再発明は避けましょう。コミュニティには、NodeJS向けのnpm-merge-driverのような強力なツールが既に存在します。
npx npm-merge-driver install --global
このツールはpackage-lock.jsonを自動的に最適化し、マージ後のファイルが常に有効で、インストールエラーが発生しないようにします。
Merge Driverを使用する際の「極めて重要」な注意点
実体験に基づき、3つのアドバイスがあります。
- 結果の再検証: ドライバーがいかにスマートであっても、マージ後には必ず
npm testを実行したり、構文をチェックしたりしてください。 - チームでの設定共有:
.gitattributesファイルはコミットしてください。.gitconfigの設定については、新しいメンバー向けに自動セットアップスクリプトを用意するのが良いでしょう。 - 機密データの取り扱い: 環境設定ファイル内の重要なセキュリティ設定を、スクリプトが自動的に上書きしてしまわないよう注意してください。
まとめ
Git Merge Driverのカスタマイズは小さな投資ですが、生産性の面で大きなリターンをもたらします。チームから退屈な手作業を排除し、ヒューマンエラーを最小限に抑えることができます。プロジェクトに複雑なJSONファイルが多い場合は、今すぐドライバーを設定しましょう。スムーズなマージができることを願っています!

