CI/CD
PR

git worktree の「No error information」エラーの原因と対処 ―並列実行で起きる競合状態

saratogax
記事内に商品プロモーションを含む場合があります

CI やデプロイツール、あるいは並列で動くコーディングエージェントが、次のようなエラーで突然落ちたことはないでしょうか。

fatal: failed to read ‘.git/worktrees/repo61/commondir’: No error information

この No error information という文言が、いかにも不穏です。

環境によっては Undefined error: 0 と表示されることもあります。

結論から言うと、これはgit worktree を並列に作ったときに起きる競合状態です。

ネットワーク障害でもディスク障害でもなく、あなたのリポジトリが壊れたわけでもありません。

本記事では、このエラーの読み解き方、git コマンドだけで再現する方法、そしてよく紹介される対処法が実は効かないという実測結果までをまとめます。

「No error information」は errno 0 のサイン

まず、このメッセージの正体から押さえます。

C 言語では、システムコールが失敗するとエラー番号が errno という変数に設定され、strerror() でそれを人間が読める文字列に変換します。

ところが errno0、つまり「エラーは発生していない」状態で strerror() を呼ぶと、処理系によって No error informationUndefined error: 0 という文字列が返ってきます。

これを踏まえて、先ほどのエラーをもう一度読んでみてください。

「ファイルの読み込みに失敗した。ただしエラーは起きていない」という、矛盾した報告になっています。

この矛盾こそが決定的な手がかりです。

ファイルを開こうとした瞬間には存在していたのに、読もうとしたときには消えていた。

つまり別のプロセスが横から削除した、という状況を示しています。

切り分けのポイント:エラーメッセージに具体的な原因(No such file or directoryPermission denied)が書かれている場合は、素直にその原因を疑ってください。原因が空っぽのときだけ、競合状態を疑う価値があります。

犯人は worktree の管理ディレクトリの奪い合い

git worktree で作業ツリーを追加すると、git はメインリポジトリの .git/worktrees/ の下に、その worktree 専用の管理ディレクトリを作ります。

ここには commondirgitdirHEAD といったファイルが置かれ、その worktree の状態を保持しています。

一方、git worktree prune は「作業ツリーの実体が消えているエントリ」を見つけて、この管理ディレクトリを削除する掃除屋です。

ここで問題になるのが、worktree を作っている最中は、管理ディレクトリだけ存在して作業ツリーの実体がまだ無いという一瞬があることです。

この瞬間に prune が走ると、prune 側からは「実体の無い残骸」に見えてしまいます。

そして作成中の管理ディレクトリを、掃除のつもりで削除してしまうのです。

削除された側の git worktree add は、自分が今まさに書いていたファイルを見失い、冒頭のエラーを吐いて死にます。

prune を呼んでいなくても起きる

「うちは prune なんて呼んでいない」と思った方こそ注意が必要です。

git worktree add は、内部で暗黙的に prune 相当の掃除を実行します。

つまり add を並列に走らせるだけで、一方の add がもう一方の add を掃除してしまう構図が成立します。

これは私の環境でも実測で確認しました(後述)。

git だけで再現する

この競合は、特殊なツールを用意しなくても git コマンドだけで再現できます。

空のリポジトリを作り、git worktree add をひたすら並列実行するだけです。

#!/bin/bash
set -u

ATTEMPTS="${ATTEMPTS:-200}"
WORK="$(mktemp -d)"
SRC="$WORK/src"
ERRLOG="$WORK/add.err"

mkdir -p "$SRC"
cd "$SRC" || exit 1

git init -q .
git config user.email repro@example.com
git config user.name repro
git commit -q --allow-empty -m base

: > "$ERRLOG"

# add だけを並列実行する。prune は一切呼ばない。
for i in $(seq 1 "$ATTEMPTS"); do
  ( git worktree add -q --detach "$WORK/out/repo$i" 2>>"$ERRLOG" ) &
done
wait

echo "git version: $(git --version)"
if [ -s "$ERRLOG" ]; then
  sort "$ERRLOG" | uniq -c | sort -rn
  echo "失敗件数: $(wc -l < "$ERRLOG" | tr -d ' ') / $ATTEMPTS"
else
  echo "(再現しませんでした)"
fi

手元(git 2.50.1 / macOS)で 200 並列を実行した結果がこちらです。

git version: git version 2.50.1
試行回数: 200 (add のみ / prune 明示呼び出しなし)

3 fatal: failed to read ‘.git/worktrees/repo148/commondir’: Undefined error: 0
3 fatal: failed to read ‘.git/worktrees/repo130/commondir’: Undefined error: 0
1 fatal: failed to read ‘.git/worktrees/repo1/commondir’: Undefined error: 0

--- 失敗件数: 7 / 200 ---

prune を一度も呼んでいないのに、7 件が失敗しました。

これが add の暗黙 prune による自滅の証拠です。

なお競合状態はタイミング依存なので、実行するたびに件数は変動します。

私が同じスクリプトを再実行したときは 2 件でした。

0 件になることもあるので、再現しなければ ATTEMPTS を増やして試してみてください。

これが競合状態だと判断する材料

実運用では、通信エラーやディスク不足など他の原因と区別する必要があります。

競合状態を疑うべきサインを整理しました。

  • エラー原因が空っぽNo error information / Undefined error: 0)。前述のとおり errno 0 でしか出ません
  • 複数の処理が同時に、同じコミットで落ちる。管理ディレクトリの消失に複数のワーカーが巻き込まれた形跡です
  • 再実行すると何事もなく成功する。タイミング依存なので、同じ入力でも結果が変わります
  • 並列度を上げたタイミングから増えた。ワーカー数やリポジトリ数の増加が引き金になります

逆に、単発でぽつんと 1 件だけ失敗し、エラーに具体的な原因が書かれている場合は、別の問題を疑ってください。

対処法:--lock は効かない

この手の競合を調べると、git worktree add --lock を勧める情報によく出会います。

--lock は管理ディレクトリに locked というファイルを置き、そのエントリを prune の対象外にするオプションです。

理屈の上では防御になりそうに見えます。

ところが実測したところ、--lock を付けても失敗は消えませんでした。

git version: git version 2.50.1
試行回数: 200 (add --lock のみ)

1 fatal: failed to read ‘.git/worktrees/repo89/commondir’: Undefined error: 0
1 fatal: failed to read ‘.git/worktrees/repo70/commondir’: Undefined error: 0
1 fatal: failed to read ‘.git/worktrees/repo7/commondir’: Undefined error: 0
1 fatal: failed to read ‘.git/worktrees/repo178/commondir’: Undefined error: 0
1 fatal: failed to read ‘.git/worktrees/repo123/commondir’: Undefined error: 0

--- 失敗件数: 5 / 200 ---

理由を考えると納得がいきます。

--lock が守るのは作成が完了した後の worktree です。

しかし今回壊れているのは作成処理の真っ最中であり、まだロックが効く状態に到達していません。

「作った worktree が後から勝手に消される」問題には有効ですが、「作っている最中に潰される」問題には届かないわけです。

効いたのは呼び出し側の直列化

確実に効いたのは、git worktree add の区間だけを排他制御することでした。

# 排他制御 (mkdir はアトミックなのでロック代わりに使える)
acquire() {
  until mkdir "$WORK/mutex" 2>/dev/null; do :; done
}
release() {
  rmdir "$WORK/mutex" 2>/dev/null
}

for i in $(seq 1 "$ATTEMPTS"); do
  (
    acquire
    git worktree add -q --detach "$WORK/out/repo$i" 2>>"$ERRLOG"
    release
  ) &
done
wait

git version: git version 2.50.1
試行回数: 200 (add を排他制御で直列化)

(失敗ゼロ)

2 回試して、いずれも失敗ゼロでした。

重要なのは、直列化するのは worktree の作成部分だけでよいという点です。

作成が終わった後のビルドやテストは並列のまま走らせられるので、並列化のメリットはほとんど損なわれません。

worktree の作成はせいぜい数十ミリ秒の処理なので、ここを直列にしても全体への影響は軽微です。

対処法まとめ

対処効果コメント
add の区間を直列化◎ 失敗ゼロ最も確実。作成後は並列のままで良い
--lock を付ける× 効果なし作成完了後の保護であり作成中は守れない
リトライを入れる○ 緩和呼び出し側を直せない場合の次善策
リポジトリごと clone に変える○ 回避共有状態が無くなる。ディスクと時間は増える
※表は横スクロールできます

ツール側の実装を直せない立場であれば、リトライで凌ぐのが現実的です。

このエラーは冪等に再実行できる性質のものなので、リトライとの相性は良好です。

並列エージェントで顕在化しつつある

この問題が近年あらためて注目されているのは、コーディングエージェントを並列で動かす構成が広まってきたためです。

複数のエージェントに同時並行で作業させるとき、それぞれを別々の worktree に隔離するのは定番のパターンになっています。

お互いの変更が衝突せず、後から差分を見比べられるので、確かに理にかなった設計です。

ただし「隔離のつもりで作った worktree が、作成時点で共有状態を奪い合う」という落とし穴があります。

実際、Claude Code のリポジトリにも、並列サブエージェントの worktree 隔離が競合で失敗するという Issue が上がっています。

こちらは .git/config のロック競合という別の症状ですが、「worktree の並列作成が共有状態を奪い合う」という根っこは同じです。

並列エージェントを自作する場合は、worktree の作成部分に排他制御を入れておくことをおすすめします。

CI/CD ツールでも起こりうる

私がこの問題に遭遇したのは、GitOps 系の CD ツールを運用している場面でした。

複数のアプリケーションの差分を並列にプレビューする処理が、worktree でリポジトリを複製する設計になっていたためです。

なお念のため補足すると、GitOps ツールがすべて worktree を使っているわけではありません

調べた範囲では、Argo CD はリポジトリごとの一時ディレクトリと参照キャッシュのロックで並行性を扱っており、worktree は使っていませんでした。

Flux の source-controller も、リコンサイルごとに一時ディレクトリへ clone する方式です。

つまり「CD ツールだから危ない」のではなく、worktree で並列化している実装だから危ないという切り分けになります。

お使いのツールが該当するかどうかは、実装を確認してみてください。

ツールを書く側への教訓:黙って失敗する掃除屋

最後に、この問題の調査を通じて痛感した設計上の教訓を書いておきます。

今回のようなツールの実装では、prune の呼び出しでエラーを握りつぶしているケースが少なくありません。

Go であれば、次のような書き方です。

// 掃除なので失敗しても構わない、という判断
_, _ = runGitCommand(ctx, "worktree", "prune")

「掃除処理が失敗しても本筋には影響しない」という判断自体は、一般には妥当です。

しかし競合状態においては、この判断が調査を著しく困難にします。

なぜなら被害者だけが大声でエラーを出し、加害者は完全に沈黙するからです。

ログを見ても add の失敗しか記録されておらず、その裏で prune が動いていた形跡はどこにも残りません。

結果として「ネットワークが不安定なのでは」といった見当違いの調査に時間を溶かすことになります。

共有状態を触る処理については、たとえ結果を無視する掃除処理であっても、デバッグレベルで実行の事実だけは残しておくと、後から助かります。

まとめ

  • No error information / Undefined error: 0 は errno 0 のサイン。「失敗したがエラーは無い」=横から消されたと読む
  • 原因は worktree の管理ディレクトリを巡る競合。add が暗黙に prune を走らせるため、prune を呼んでいなくても起きる
  • git だけで再現可能。200 並列で数件が失敗する(タイミング依存で変動)
  • --lock は効かない。作成完了後の保護であり、作成中の競合には届かない
  • 確実なのは add の区間だけを直列化すること。並列化のメリットはほぼ維持できる
  • 並列エージェントの worktree 隔離でも同種の問題が報告されている

「エラーの内容が空っぽ」という一見すると情報量ゼロのメッセージが、実は競合状態を名指しする強力な手がかりでした。

同じエラーで悩んでいる方の切り分けの助けになれば幸いです。

あわせて読みたい(並列実行と CI/CD)

ABOUT ME
saratoga
saratoga
フリーランスエンジニア
仕事にも趣味にも IT を駆使するフリーランスエンジニア。技術的な TIPS や日々の生活の中で深堀りしてみたくなったことを備忘録として残していきます。
記事URLをコピーしました