git merge-treeの活用:ブランチ切り替えやstash不要、わずか1秒でコンフリクトを高速チェック

Git tutorial - IT technology blog
Git tutorial - IT technology blog

クイック検証:30秒でコンフリクトを検知する

feature/loginブランチで10個ほどのファイルを編集中の場面を想像してみてください。作業途中のため、まだコミットできる状態ではありません。そんな時、チームリーダーから「先ほど大規模なPRをmainにマージしたから、rebaseしてコンフリクトが発生しないか確認してほしい」と急に連絡が入ったとします。

多くの人がやりがちな手順は、git stashを実行し、mainに切り替えてpullし、テストマージを試みるという流れでしょう。しかしこの方法は手間がかかるだけでなく、万が一stash popでエラーが起きた場合に作業中のコードを失うリスクもあります。Git 2.36以降であれば、ディスク上のファイルに一切触れることなく、メモリ上で直接コンフリクトをチェックできます。

# リモートから最新コミットをローカルデータベースに取得
git fetch origin

# 現在のブランチとorigin/main間のコンフリクトをチェック
git merge-tree --write-tree HEAD origin/main

Gitはメモリ上で処理を実行し、わずか30〜50ミリ秒ほどで結果を返します。

  • ケース1 — クリーンにマージ可能な場合: Gitは終了ステータス(exit code)0を返し、40文字のTree OIDハッシュ値のみを出力します。
4b825dc642cb6eb9a060e54bf8d69288fbee4904
  • ケース2 — コンフリクトが存在する場合: Gitは一時ツリーのハッシュ値およびエラーとなったファイルの一覧を出力し、exit code 1で終了します。
Auto-merging src/auth.js
CONFLICT (content): Merge conflict in src/auth.js

数千ファイル規模の大規模プロジェクトで、コンフリクトが発生したファイル名だけを手早く抽出したい場合は、--name-onlyフラグを付与します。

git merge-tree --write-tree --name-only HEAD origin/main

技術的な仕組み:なぜこの手法が最適なのか?

従来の「テストマージ」が抱えるコスト

通常のgit mergeコマンドは、ステージングエリア(Index)とワーキングツリー(Working Tree)の双方を更新する必要があります。作業ディレクトリに変更途中のファイルが存在する場合、Gitはデータの上書きを防ぐために処理を中断します。これを回避するため、開発者はよく以下のような手順を踏んでいました。

git stash
git merge --no-commit --no-ff origin/main
# コンフリクトを確認...
git merge --abort
git stash pop

しかし、この一連のコマンド操作には3つの大きなデメリットが伴います。

  1. ファイルのタイムスタンプ(mtime)の書き換え: ViteやNext.js、Webpackなどのビルドツールがファイルの変更を検知し、フルリビルドをトリガーしてしまいます。3,000〜5,000モジュール規模のプロジェクトであれば、再コンパイルを待つだけで15〜30秒もの時間をロスすることになります。
  2. 意図しないGitフックの実行: pre-mergeやpost-checkoutなどの重いスクリプトがバックグラウンドで自動実行されてしまいます。
  3. データ上書きのリスク: マージの中止処理でファイルのクリーンアップが不完全だった場合、その後のgit stash popで複雑なコンフリクトが発生し、保存前の下書きコードを失ってしまう危険性があります。

Plumbingコマンド層の強み

Gitのアーキテクチャには以下の2層が存在します。

  • Porcelain: checkoutやcommit、mergeなど、ユーザーが日常的に扱う高レベルなコマンド群です。これらはファイルシステムに直接変更を加えます。
  • Plumbing: cat-fileやcommit-tree、merge-treeなど、システムのコア処理を担う低レベルな配管(plumbing)コマンド群です。

git merge-treeはまさにこのplumbing層で動作します。3-way mergeアルゴリズムは、.git/objectsディレクトリ内に存在するコミットツリーオブジェクト間で直接計算されます。すべてのデータ処理はメモリおよびGitのオブジェクトデータベース上だけで完結するため、ディスク上のワーキングツリーにファイルが書き込まれることは一切ありません。

新構文(Git 2.36以降)と旧構文の違い

2022年にリリースされたGit 2.36より前は、git merge-treeを実行する際に共通の祖先コミット(merge base)を手動で検索する必要があり、やや扱いづらいものでした。

# 旧構文(冗長で出力フォーマットも読みづらい):
git merge-tree $(git merge-base branchA branchB) branchA branchB

# Git 2.36以降の最新構文(推奨):
git merge-tree --write-tree branchA branchB

--write-treeフラグにより、このコマンドはロジック面においてgit mergeの完全なレプリカとして機能するようになり、merge baseの自動特定やファイル名変更の検知(rename detection)も100%の精度で正確に処理されます。

開発ワークフローにおける実践的な活用例

1. Pull Request作成前の事前チェックスクリプト

以下のBashスクリプトをターミナル環境やツールに組み込むことで、GitHubやGitLabでPRを作成する前にコンフリクトを早期検知できます。

#!/usr/bin/env bash
TARGET="origin/main"
CURRENT=$(git rev-parse --abbrev-ref HEAD)

echo "$CURRENT と $TARGET の間のコンフリクトをスキャン中..."

OUTPUT=$(git merge-tree --write-tree HEAD "$TARGET" 2>&1)
STATUS=$?

if [ $STATUS -eq 0 ]; then
    echo "✅ 安全:ブランチはクリーンです。そのままマージ可能です!"
else
    echo "❌ 警告:以下のファイルでコンフリクトが検出されました:"
    echo "$OUTPUT" | grep -E "(CONFLICT|Auto-merging)"
    exit 1
fi

2. CI/CDパイプラインにおける自動化ボットの最適化

GitHub ActionsやGitLab CIなどのCI/CD環境において、ワーキングツリー全体をチェックアウトして多数のPRのマージ可否(mergeability)を検証すると、多くのリソースを消費します。ベアリポジトリ(bare repository)上でgit merge-treeを実行すれば、サーバーはわずか数十ミリ秒でPRの状態を判定でき、コードをチェックアウトすることなくPR作成者へneeds-rebaseラベルを自動付与できます。

日常的な運用のTips

Git Aliasを設定してコマンドを短縮

~/.gitconfigに一度設定を追加するだけです。

git config --global alias.dry-merge 'merge-tree --write-tree'

これ以降は、以下のようにシンプルに実行できます。

git dry-merge HEAD origin/main

オフラインでも軽快に動作

事前にgit fetchさえ済ませていれば、完全にオフラインで動作します。ChromiumやLinuxカーネルのような超巨大リポジトリであっても、ほぼ一瞬でチェックが完了します。

スクリプト作成時に役立つ終了ステータス(Exit Code)の仕様

  • 0:クリーンにマージ可能(コンフリクトなし)。
  • 1:コンフリクトあり(コンテンツ競合またはファイル名変更の競合)。
  • 0および1以外:構文エラー、またはリポジトリ内に指定したブランチが存在しない場合のエラー。
Share: