複数プロジェクトを git worktree で並列開発していて、同じ日に「状態が見えない」「デプロイ課金が地味に増える」「退避したはずの個人情報が同期されていた」という3つの落とし穴に同時にハマりました。
どれも追加コストゼロで直せたので、再現できる形で残します。自分の学び直しを兼ねた整理です。
医療と IT のあいだで動きながら、複数プロジェクトを並行で開発している立場で書いています。
TL;DR
| 落とし穴 | 症状 | 対策 | コスト |
|---|---|---|---|
| 状態が見えない | どの worktree が未マージ/dirty か分からない | 全 worktree を1コマンドで一覧化 | 無料 |
| デプロイ課金 | push のたびに Vercel がフルビルド | ローカル事前ビルド + Ignored Build Step | 無料 |
| 退避先が同期 |
~/Documents の退避フォルダが iCloud 同期対象だった |
同期対象外へ mv
|
無料 |
前提:なぜ worktree 並列開発なのか
1リポジトリを複数ドメインで並行開発するために、git worktree でドメインごとに独立した作業ディレクトリを切っています。
🔰 「git worktree」って何?
ひとことで言うと「1つのリポジトリを、複数のディレクトリで同時にチェックアウトできる機能」です。
# 例: 機能ごとに別ディレクトリで作業する
git worktree add -b feature/foo ~/AI/Project.foo origin/main
git worktree add -b feature/bar ~/AI/Project.bar origin/main
git clone を何個もする代わりに、.git を共有したまま作業ツリーだけ分けられます。ブランチの切り替え地獄から解放される反面、「今どの机がどうなってるか」を見失いやすいのが今回の話です。
落とし穴① 全 worktree の状態が見えない
worktree が増えると、git status を各ディレクトリで叩いて回ることになり、全体像が頭に入りません。
特に怖いのが ahead が溜まった worktree。私の場合、ある worktree が 68 commits ahead(68 個 main に未反映)になっていて、危うく取りこぼすところでした。
🔰 「ahead / behind」って何?
リモート(origin/main)を基準にした、自分のブランチの位置関係です。
-
behind =
origin/mainに新しい commit があるのに自分が追従できていない(=pull が必要) - ahead = 自分のローカルに commit があるのに push/merge していない(=取りこぼし候補)
# origin/main を基準に「左=behind / 右=ahead」を数える
git rev-list --left-right --count origin/main...HEAD
# 出力例: 68 0 → 68 behind / 0 ahead
🔰 「dirty」って何?
未コミットの変更がある状態です。git status --porcelain が何か出力すれば dirty。
# tracked の変更数と untracked 数を分けて数える
git status --porcelain | grep -vc '^??' # commit 対象になりうる変更
git status --porcelain | grep -c '^??' # untracked
対策:全 worktree を1表に
各 worktree を回って branch / dirty / ahead / behind を集計するだけのスクリプトを用意しました。要は git worktree list をループするだけです。
#!/usr/bin/env bash
# 全 worktree の状態を1行ずつ出す(雰囲気版)
git worktree list --porcelain | awk '/^worktree /{print $2}' | while read -r wt; do
br=$(git -C "$wt" branch --show-current)
dirty=$(git -C "$wt" status --porcelain | wc -l | tr -d ' ')
# origin/main 基準の behind/ahead
read -r behind ahead < <(git -C "$wt" rev-list --left-right --count origin/main...HEAD 2>/dev/null)
printf "%-28s %-26s dirty=%-3s behind=%-3s ahead=%-3s\n" \
"$(basename "$wt")" "$br" "$dirty" "${behind:-?}" "${ahead:-?}"
done
表にした瞬間「ahead=68 の worktree が最優先」と一目で分かりました。
散らかりは、見えるようにするだけで半分解決します。
落とし穴② push のたびに Vercel がフルビルドして課金される
Vercel は git push を検知して自動でビルドします。請求を見たら 約9割が Build CPU Minutes でした。
主因は2つ。
- 細かい commit を1つずつ main にマージ → 1 commit = 1 Production ビルド
-
*.mdや docs だけの変更でもフルビルドしていた(アプリの中身は変わっていないのに)
🔰 「ビルド」と課金の関係って?
ビルド = ソースを実際に動く成果物(Next.js なら .next/)に変換する工程です。
この変換にCPU 時間がかかり、Vercel はそこに課金します。つまりビルド回数 × ビルド時間 = 料金。回数を減らすのが本丸です。
対策A:本番の前に、ローカルで同じビルドを通す(無料)
Vercel で初めて失敗に気づくと、その失敗ビルドにも時間=課金が乗ります。手元のビルドは無料なので、push 前に同じ工程を回します。
npx tsc --noEmit # 型チェック
npm run lint # lint
npm run build # 本番と同じ next build。ここが通れば Vercel でも通る
これを1コマンドにまとめ、緑のときだけ push する運用にしました。「ローカルで緑 → まとめて1 PR → main 1 commit = Vercel ビルド1回」。
対策B:Ignored Build Step で不要なビルドをスキップ
🔰 「Ignored Build Step」って何?
Vercel の機能で、「このコミット、ビルドする必要ある?」を自前のスクリプトで判定できる仕組みです。
スクリプトの終了コードが 0 ならビルドをスキップ、1 なら実行します(直感と逆なので注意)。
docs / *.md / 非フロントエンドのコードだけの変更なら、Next.js のビルドは不要なのでスキップさせます。
#!/usr/bin/env bash
# Vercel Settings → Git → Ignored Build Step に
# bash scripts/vercel-ignore-build.sh
# と登録する。終了コード 0=スキップ / 1=ビルド。
set -uo pipefail
BASE="${VERCEL_GIT_PREVIOUS_SHA:-HEAD^}"
# 基準コミットが取れない時は安全側でビルド
git rev-parse --verify "$BASE" >/dev/null 2>&1 || { echo "no base → build"; exit 1; }
# ビルドに無関係なパス「だけ」の変更ならスキップ
if git diff --quiet "$BASE" HEAD -- . \
':!*.md' ':!docs' ':!**/.agent-memo.md'; then
echo "docs/md only → skip build"; exit 0
fi
echo "code changed → build"; exit 1
ポイントは git pathspec の 除外構文 :!pattern。「.(全部)から、これらを除外した差分」が空なら、ビルド不要と判定できます。
お金がかかる場所(Vercel ビルド)は最後の1回だけ。
検証はすべて無料のローカルで終わらせる。
落とし穴③ 退避したはずの個人情報が iCloud 同期されていた
これが一番ヒヤッとしました。
個人情報を扱うプロジェクトでは、機密ファイルをリポジトリ外の「退避フォルダ」に隔離していました。隔離できているつもりでした。
ところが Mac の iCloud「デスクトップと書類フォルダ」同期が ON で、退避先がちょうど ~/Documents 配下。つまり隔離したはずの個人情報が自動でクラウドへ同期され続けていたわけです。
🔰 「デスクトップと書類フォルダ同期」って何?
iCloud Drive の機能で、~/Desktop と ~/Documents の中身を自動でクラウドに同期します。
便利ですが「ローカルだけに置いたつもり」が通用しなくなります。この2フォルダの“外”はデフォルトで同期対象外、という性質が今回の鍵でした。
確認コマンド
# CloudDesktop(デスクトップ&書類同期)が有効か
defaults read MobileMeAccounts | grep -i CLOUDDESKTOP
# → com.apple.Dataclass.CloudDesktop が出れば ON
対策:同期対象外へ「中身を見ずに」まるごと移動
# ~/Documents(同期対象)→ ~/AI(home 直下=同期対象外)へ退避
mv ~/Documents/secret-private ~/AI/secret-private
# 元の場所には移動先メモだけ残す(再開時に迷子にならない)
mkdir -p ~/Documents/secret-private
echo "中身は ~/AI/secret-private へ移動しました(iCloud 同期回避)" \
> ~/Documents/secret-private/MOVED.txt
ここで大事なのは 「中身を開かない」 こと。
確認のためにファイルを開くと、外部 AI やプレビュー経由で別のリスクが生まれます。見ずに、まるごと、同期対象外へ。
なお OS の同期設定 OFF 自体はコードからは変えられないので、最終的には System Settings → Apple ID → iCloud →「デスクトップと書類フォルダ」を手で OFF にするか、退避運用を同期対象外パス前提で統一します。
よくある誤解
「並列作業は危ない」
→ 危ないのは並列そのものではなく 状態が不可視なこと。git worktree list ベースの一覧化で解決します。
「従量課金だから仕方ない」
→ 多くは 気づかず何度もビルドしていたムダ。ローカル事前ビルド + Ignored Build Step で回数が激減します。
「クラウド同期 = 安全」
→ バックアップとしては安全でも、“隔離”とは正反対。機密は同期対象パスの外へ。
チェックリスト
- 全 worktree の branch / dirty / ahead / behind を1コマンドで見られるか
-
git rev-list --left-right --count origin/main...HEADで ahead 溜まりを定期確認しているか -
push 前に
npm run buildをローカル(無料)で通しているか - Ignored Build Step で docs/md のみの変更をスキップしているか
-
defaults read MobileMeAccounts | grep CLOUDDESKTOPで同期状態を把握したか -
機密の退避先は
~/Documents/~/Desktopの外にあるか
全部を一度にやらなくて大丈夫です。気づいた1つからで十分前進します。
まとめ — 落とし穴はぜんぶ「見えていない」のサインだった
3つは別々に見えて、根っこは同じでした。状態が見えていないから、不安とムダとリスクが生まれる。
- git →
worktree listで全机を1表に - 課金 → ビルド回数を見える化し、無料のローカルへ寄せる
- 同期 → どこに同期されるかを把握し、機密は対象外へ
見える化した瞬間、「ヤバい」が「ここを直せばいい」に変わります。散らかりは欠点ではなく、仕組みを整えるきっかけです。一歩ずつでいきましょう。
参考資料
- 公開 Qiita: https://qiita.com/TaichiEndoh/items/f4087c3129b2d017cf3f (医療機関インフラ再構築 完全ガイド)
- 公開 note: https://note.com/taichi_endoh/n/ncea1540542fe (医療セキュリティの基本)
著者について
臨床工学技士 × AI エンジニア。教育関係の仕事もしています。
医療と IT のあいだで動きながら、現場目線で発信中です。
- note: https://note.com/taichi_endoh
- Qiita: https://qiita.com/TaichiEndoh
- X: https://x.com/endoh_taichi
質問・情報提供・コラボ提案、いつでも歓迎です。