git mergeで事前にコンフリクトを確認する方法|git merge-tree --write-treeの使い方

ブログ

複数のブランチを並行して触っていると、「このfeatureブランチをmainにマージしたら衝突するのか、そうでないのか」を、実際にマージする前に知りたい場面があります。ワーキングツリーを汚さずに確認したくて、git merge-tree --write-tree を実際に試してみたので、使い方と挙動をまとめておきます。

git merge-treeとは何か

git merge-tree は、ワーキングツリーやインデックスを一切変更せずにマージ結果をシミュレートするコマンドだと分かりました。実際に git merge を実行するとインデックスと作業ディレクトリが書き換わってしまいますが、git merge-tree はマージ結果のツリーオブジェクトを計算するだけで、リポジトリの状態には触れません。

Gitの公式ドキュメントでは、git-hash-objectgit-mktreegit-commit-tree と同じ「低レベルのplumbingコマンド」に位置づけられています。スクリプトや自動化から呼び出す前提の設計だと理解しました。

なお --write-tree モード(現在の主流の使い方)はGit 2.38(2022年10月リリース)以降で利用できます。それより前のGitに存在する git merge-tree <base> <branch1> <branch2> という旧形式は出力フォーマットが異なるので、バージョンを確認してから使う必要があります。

git --version

基本の使い方

もっとも基本的な形は次の通りです。

git merge-tree --write-tree <branch1> <branch2>

マージベースは自動的に検出されます。今作業中のブランチが main を取り込んだらどうなるかを確認したい場合は、こう実行してみました。

git merge-tree --write-tree HEAD main

競合がなければ、標準出力にはマージ結果のツリーオブジェクトのOIDだけが1行返ります。競合がある場合は、OIDに続けて競合ファイルの情報と、CONFLICT (...) のような情報メッセージが出力されます。

終了コードで判定する

出力を目視で読むだけでなく、スクリプトから使う場合は終了コード(exit status)を見るのが正しい判定方法だと分かりました。

0 … マージが成功し、競合なし
1 … マージに競合がある
それ以外 … マージ自体が開始/完了できないエラー(出力内容は保証されない)

ここは誤解しやすい点ですが、「競合ファイル情報のセクションが空だから競合していない」と判断してはいけません。ディレクトリのリネーム競合のように、個別ファイルの競合として現れないパターンもあるため、必ず終了コードで判定する必要があると公式ドキュメントに明記されていました。

スクリプトでの基本形はこうなります。

#!/bin/bash

OUTPUT=$(git merge-tree --write-tree feature main)
EXIT_CODE=$?

if [ $EXIT_CODE -eq 0 ]; then
  echo "マージ可能: 競合なし"
elif [ $EXIT_CODE -eq 1 ]; then
  echo "マージ不可: 競合あり"
  echo "$OUTPUT"
else
  echo "エラーが発生しました"
  exit $EXIT_CODE
fi

出力される競合情報の読み方

競合が発生すると、ツリーOIDの後ろに競合ファイルの情報が並びます。フォーマットは次の通りです。

<mode> <object> <stage> <filename>

<stage> の数字は、それぞれ次を表すと分かりました。

  • 1 … 共通祖先(マージベース)でのバージョン
  • 2 … マージ先(1つ目に指定したブランチ)でのバージョン
  • 3 … マージ元(2つ目に指定したブランチ)でのバージョン

ファイル名だけ知りたければ --name-only を付けるとシンプルになります。

git merge-tree --write-tree --name-only feature main

競合情報のあとには Auto-merging <file>CONFLICT (content): Merge conflict in <file> といった情報メッセージも続きます。ここで注意が必要なのは、Auto-merging は競合なく自動マージされたファイルのメッセージも含む点で、「メッセージに出てきたファイル=競合ファイル」と早合点しないようにする必要があります。

便利なオプション

実際に使ってみて、覚えておくと役立つと感じたオプションをいくつか挙げておきます。

--quiet

出力を一切抑制し、終了コードだけを知りたいときに使います。競合を検出した時点で早期終了するため、大きめのリポジトリでは処理も軽くなります。

git merge-tree --write-tree --quiet feature main
echo $?

--merge-base

マージベースを自動検出させず、明示的に指定したい場合に使います。

git merge-tree --write-tree --merge-base=<tree-ish> feature main

ただし複数のマージベースがあるケース(クロスマージなど)には対応しておらず、git merge と異なる結果になったり変更が失われたりする可能性があると明記されていたので、通常のブランチ比較では自動検出に任せる方が安全だと感じました。

--allow-unrelated-histories

共通の履歴を持たない2つのブランチをマージしようとするとデフォルトではエラーになりますが、このフラグで強制的に処理させられます。

実務での使い方: 多段ブランチのコンフリクト事前確認

feature-top から feature-sub を派生させるような多段ブランチ構成では、各段階で「親ブランチの変更を取り込んだらどこで衝突するか」を段階的に確認できます。

base=$(git merge-base feature-sub feature-top)
git merge-tree --write-tree "$base" feature-sub feature-top

(この形式は --merge-base オプション経由での明示指定にあたります)

競合が見つかった場合は、実際にそのブランチで親ブランチをマージ(またはリベース)して手動で解消し、再度 git merge-tree で確認する、というワークフローが現実的だと感じました。CIのプルリクエストチェックに組み込んでおけば、レビュー前に「このPRはmainと衝突する」という警告を自動で出せそうだと思いました。

git merge --no-commit --no-ff との違い

似た用途で使われる git merge --no-commit --no-ff とは性質が異なります。

git merge-treegit merge --no-commit --no-ff
ワーキングツリー変更しない変更する
インデックス変更しない変更する
用途プレビュー・スクリプトからの判定実際にマージ内容を確認してからコミット
後始末不要git merge --abort などで戻す必要がある

「今の作業ツリーを一切汚さずに衝突有無だけ知りたい」なら git merge-tree、「実際にマージ結果をワーキングツリーに展開して中身を見ながら解消したい」なら git merge --no-commit --no-ff を使う、と使い分けるのがよさそうだと分かりました。

まとめ

  • git merge-tree --write-tree はワーキングツリー・インデックスを変更せずマージ結果をシミュレートできます
  • 判定は出力の目視ではなく終了コード(0=成功/1=競合/それ以外=エラー)で行います
  • 競合ファイルの情報は <mode> <object> <stage> <filename> 形式で、stage番号は共通祖先/マージ先/マージ元を表します
  • CIのマージ前チェックや多段ブランチのコンフリクト事前確認に組み込みやすいと感じました

参考リンク

一覧へ戻る