git gtr 入門:worktree ごとにポートを振って、dev サーバーを並べて動かす

git gtr 入門:worktree ごとにポートを振って、dev サーバーを並べて動かす

#Git#ai#開発環境

機能開発の途中で、急ぎのバグ修正やPRのレビューが割り込んでくることがあります。 作業中の変更を git stash で退避してブランチを切り替え、用が済んだら戻して stash pop する、という往復は地味に集中を削ります。 AIコーディングエージェントを複数並べて別々のタスクを任せたい場合は、そもそも一つの作業ディレクトリでは足りません。

この問題に対するGit標準の答えが git worktree です。 ただ、素の git worktree はディレクトリを作るところまでしかしてくれません。 .env のコピー、依存関係のインストール、ポートの重複回避は手作業のまま残ります。

この手作業をまとめて引き受けるのが git gtr(Git Worktree Runner) です。 この記事では、worktreeの基本と素の git worktree で残る手間を整理したうえで、git gtrの導入、日常的な使い方、そしてフックでworktreeごとにポートを書き換え、複数のdevサーバーを同時に起動する構成までを順に説明します。 対象はGitの基本操作(ブランチ、コミット、マージ)が分かるエンジニアで、worktreeを使ったことがなくても読めるように書いています。

git worktree とは?

通常、一つのリポジトリのフォルダでチェックアウトできるブランチは一つです。 別のブランチで作業したければ、今のブランチから切り替えるしかありません。

git worktreeは、一つのリポジトリから作業ディレクトリを複数作る機能です。 それぞれのディレクトリで別のブランチをチェックアウトでき、コミット履歴やリモートの設定は全員で共有します。 リポジトリを丸ごと git clone し直すのと違い、コミットなどを保存するオブジェクトデータベースを共有でき、フェッチした結果も各worktreeで使えます。 ただし、チェックアウトしたファイルや node_modules などの容量はworktreeごとに必要です。

# メインの作業ディレクトリ(main ブランチ)はそのまま
cd ~/src/my-app

# 隣に feature/login 用の作業ディレクトリを作る
git worktree add ../my-app-feature-login -b feature/login

# 一覧
git worktree list
# /Users/you/src/my-app                 59c8384 [main]
# /Users/you/src/my-app-feature-login   59c8384 [feature/login]

my-app で機能開発を続けながら、my-app-feature-login では別の作業ができます。 stashもブランチの切り替えも要りません。

一つ制約があります。 通常、同じブランチを二つのworktreeで同時にチェックアウトすることはできません。

git checkout feature/login
# fatal: 'feature/login' is already used by worktree at '.../my-app-feature-login'

片方でコミットして共有のブランチ参照が進んだとき、もう片方の作業ファイルやインデックスとの食い違いが起きるのを防ぐための安全装置です。

なぜ今 worktree が必要になるのか

worktree自体は2015年のGit 2.5からある機能です。 近年あらためて注目されているのは、AIコーディングエージェントの使い方が変わったためです。

Claude CodeやCodexのようなエージェントは、ファイルを書き換え、テストを走らせ、コミットまで進めます。 一つのエージェントに一つのタスクを任せるだけなら、今の作業ディレクトリで十分です。 しかし「認証の改修はエージェントA、管理画面の不具合はエージェントB」と並べた瞬間、同じディレクトリを二者が同時に書き換えることになります。 片方のテストが、もう片方の書きかけのコードで落ちる、という事故が起きます。

worktreeを使えば、エージェントごとに作業ディレクトリとブランチを分けられます。 人間の割り込み対応も同じ理屈で、レビュー用のworktreeを一つ作れば、手元の作業を崩さずにPRを動かして確かめられます。

素の git worktree で残る手間

worktreeは便利ですが、実際に使い始めると、作った直後のディレクトリが「そのままでは動かない」ことに気づきます。 新しいworktreeに入るのはGitで管理されているファイルだけだからです。

feature/login で開発を始めるまでの手順を、素の git worktree で書くと次のようになります。

# 1. 作業ディレクトリを作る(パスとブランチ名を両方指定する)
git worktree add ../my-app-feature-login -b feature/login

# 2. Git 管理外のファイルを手でコピーする
cp .env ../my-app-feature-login/

# 3. 依存関係を入れ直す
cd ../my-app-feature-login
npm install

# 4. メインの dev サーバーが 3000 番を使っているので、.env の PORT を手で書き換える
vi .env

# 5. エディタや AI エージェントを開く
cursor .

# 6. 終わったら片付ける
cd ../my-app
git worktree remove ../my-app-feature-login
git branch -d feature/login

一回なら我慢できますが、worktreeを日に何本も作る使い方では毎回この手順が挟まります。 特に厄介なのが4のポートです。 メインのdevサーバーが3000番を使っている状態で、新しいworktreeでも同じポートを指定すると、サーバーによっては衝突して起動できません。 Viteのように空いている次のポートへ自動で切り替えるツールもありますが、worktreeとポートの対応は起動順に左右されます(Viteのポート設定)。 DockerでDBを立てている場合も同様で、5432番が埋まっているので二つ目のPostgreSQLが上がりません。

Gitには post-checkout フックがあり、git worktree add でも発火します。 これを使って自動化することもできますが、次の制約があります。

観点Git標準の post-checkout フックgit gtrのフック
発火するタイミングcheckout、switch、初回チェックアウトを伴う clone や worktree add の後。第1引数がゼロ埋めになるのは clone でも同じなので、それだけではworktree作成と判別できないgit gtr new などによるworktreeの作成後(postCreate)
削除時のフックなし削除前(preRemove)と削除後(postRemove)がある
受け取れる情報前後のコミットハッシュとフラグREPO_ROOT、WORKTREE_PATH、BRANCH の環境変数
チームでの共有.git/hooks/ はコミットされない。共有するには core.hooksPath などの工夫が要る.gtrconfig をコミットして共有できる
管理外ファイルのコピーフックの中で自分で書くgtr.copy.include で宣言する

Git標準フックの発火条件は、公式の post-checkout の説明に基づいています。

削除時のフックがないのは地味に効きます。 worktreeごとにDockerコンテナを立てた場合、worktreeを消してもコンテナとボリュームは残り続けます。

git gtr とは?

git gtr(Git Worktree Runner)は、git worktree をラップするCLIです。 AIコードレビューサービスのCodeRabbitがGitHubで公開しており、ライセンスはApache-2.0です。 Bashで書かれていて、動作要件はGit 2.17以上とBash 3.2以上です。 macOSとLinuxに対応し、WindowsではGit BashかWSLで動きます。

前節の手順は、git gtrでは次のようになります。

やりたいこと素の git worktreegit gtr
作業ディレクトリを作るgit worktree add ../my-app-feature-login -b feature/logingit gtr new feature/login
.env などをコピーする手で cpgtr.copy.include に書いておけば自動
依存関係を入れる、ポートを書き換える手で実行postCreate フックで自動
エディタで開くcd ../my-app-feature-login && cursor .git gtr editor feature/login
AIエージェントを起動するcd ../my-app-feature-login && claudegit gtr ai feature/login
そのworktreeでコマンドを実行するcd してから実行git gtr run feature/login npm test
片付けるgit worktree remove ... と git branch -d ...git gtr rm feature/login --delete-branch

基本的な考え方は「設定を先に書き、コマンドは短く」です。 コピーするファイル、作成後に走らせるコマンド、使うエディタやAIツールを一度設定しておけば、あとはブランチ名だけで操作できます。 個人の設定は git config の gtr.* キーに保存し、チーム共有の設定は後述する .gtrconfig に書けます。

導入方法

macOSならHomebrewで入ります。

brew tap coderabbitai/tap
brew install git-gtr

Linuxではリポジトリをcloneしてインストールスクリプトを実行します。

git clone https://github.com/coderabbitai/git-worktree-runner.git
cd git-worktree-runner
./install.sh

インストールすると git-gtr というコマンドがPATHに入り、Gitのサブコマンドとして git gtr で呼べるようになります。 環境が整っているかは doctor で確認できます。

git gtr version
git gtr doctor    # Git、エディタ、AI ツールの検出状況を確認する
git gtr adapter   # 対応しているエディタと AI ツールの一覧と、インストール状況

基本操作はmacOS上のgit gtr v2.1.0、Git 2.50、Node.js 26系で確認しています。 ポート割り当て、worktree削除後のオフセット再利用、Composeの設定出力はv2.1.0とv2.11.1の両方で検証し、Node.jsサーバーの3000、3001、3003番での同時起動も確認しました。 v2.11.1では new --porcelain の出力も検証しています。 git gtr trust などの追加機能は、v2.11.1の公式ドキュメントと実装に基づいて説明します。 Homebrewでインストールした場合は brew upgrade git-gtr で更新できます。 cloneしてインストールした場合は、取得元のリポジトリを更新し、インストール手順に沿って更新してください。

動かしてみる

worktree を作る

リポジトリのルートで new を実行します。

cd ~/src/my-app
git gtr new feature/login
==> Creating worktree: feature-login
Location: /Users/you/src/my-app-worktrees/feature-login
Branch: feature/login
==> Fetching remote branches...
==> Creating new branch feature/login from main
[OK] Worktree created with new branch feature/login

worktreeは、既定ではリポジトリの隣にある <リポジトリ名>-worktrees/ の下に作られます。 フォルダ名はブランチ名から決まり、feature/login の / は - に置き換えられて feature-login になります。 作成前に git fetch が走ります。 新しいブランチの既定の起点は、v2.11.1ではリモート追跡ブランチ(通常は origin/main)です。 v2.1.0とは起点の解決方法が異なるため、ローカルのコミットを確実に使いたい場合は --from または --from-current を指定します(v2.11.1の実装)。 指定したブランチがすでに存在する場合は、そのブランチをチェックアウトします。

起点を変えたい場合はオプションで指定します。

git gtr new hotfix --from v1.2.3        # タグや任意のコミットから作る
git gtr new variant-1 --from-current    # 今いるブランチから派生させる

--from-current は、同じタスクを複数のやり方で試すときに便利です。 feature/login にいる状態で variant-1 と variant-2 を作り、それぞれ別のエージェントに実装させて比べられます。 引き継ぐのはコミット済みの内容で、作業中の未コミット変更は引き継ぎません。

一覧と移動

git gtr list
BRANCH                         PATH
------                         ----
main [main repo]               /Users/you/src/my-app
feature/login                  /Users/you/src/my-app-worktrees/feature-login

各コマンドでは、worktreeをブランチ名で指定します。 メインのリポジトリには 1 という特別なIDが割り当てられています。

go はworktreeのパスを出力するコマンドです。 git gtr は子プロセスとして動くため、呼び出し元のシェルのカレントディレクトリは変えられません。 移動するには cd と組み合わせます。

cd "$(git gtr go feature/login)"
cd "$(git gtr go 1)"   # メインのリポジトリに戻る

毎回これを打つのが面倒なら、シェル統合を入れると gtr cd feature/login で移動できるようになります。 ~/.zshrc に追記するスニペットは公式READMEに、Bash、Zsh、Fish向けのものが載っています。 fzfが入っていれば、引数なしの gtr cd でworktreeの対話的な選択画面が開きます。 Homebrewのcoreutilsを入れていると、GNU版の tr が gtr という名前で入っていて衝突することがあります。 その場合は eval "$(git gtr init zsh --as gwtr)" で、シェル関数に別名を付けられます。

worktree でコマンドを実行する

run を使うと、移動せずにworktree内でコマンドを実行できます。

git gtr run feature/login npm test
git gtr run feature/login git status
git gtr run 1 npm run build   # メインのリポジトリで実行

エディタと AI エージェントを開く

使うエディタとAIツールを設定しておきます。 --global を付けると全リポジトリ共通の設定になります。

git gtr config set gtr.editor.default cursor --global
git gtr config set gtr.ai.default claude --global
git gtr editor feature/login              # Cursor で開く
git gtr ai feature/login                  # worktree のディレクトリで Claude Code を起動する
git gtr ai feature/login --ai codex       # このときだけ Codex を使う
git gtr ai feature/login -- --continue    # -- 以降は AI ツールにそのまま渡す

作成と起動を一度に済ませることもできます。

git gtr new feature/login -e -a   # 作成して、エディタを開き、AI ツールを起動する

エディタはCursor、VS Code、Zed、JetBrains系など、AIツールはClaude Code、Codex、Gemini CLI、GitHub Copilot CLI、Aiderなどに対応しています。 一覧にないツールでも、PATHが通ったコマンドなら指定できます。

片付ける

git gtr rm feature/login                    # worktree を削除する(ブランチは残る)
git gtr rm feature/login --delete-branch    # ブランチも一緒に削除する
git gtr clean --merged                      # PR がマージされたブランチの worktree をまとめて削除する

clean --merged はGitHubなら gh、GitLabなら glab を使って、PRの状態を確認します。

Git 管理外のファイルをコピーする

worktreeが「そのままでは動かない」原因の一つ目は、.env のようなGit管理外のファイルです。 コピーしたいファイルをglobパターンで登録しておくと、new のたびにメインのリポジトリからコピーされます。

git gtr config add gtr.copy.include ".env"
git gtr config add gtr.copy.include "**/.env.example"
git gtr config add gtr.copy.exclude "**/.env.production"   # 本番の認証情報はコピーしない

set は単一値の設定を保存し、add は値を追加します。 コピー対象やフックのように複数の値を持つ設定には add を使います。 同じキーに複数の値が登録済みだと、set はエラーになります。 そのキーのローカル設定を入れ替える場合は、git gtr config unset <キー> で既存値を消してから add で登録し直します。

node_modules のような依存関係のディレクトリを丸ごとコピーすることもできます。 npm install を待つより速い代わりに、ディスクは消費します。 コピー元とコピー先で依存関係の定義が違う場合は、コピー後にもインストールが必要です。 Pythonの .venv はスクリプトに絶対パスを含むため、原則としてコピー先で作り直します(Python公式の注意事項)。

git gtr config add gtr.copy.includeDirs "node_modules"
git gtr config add gtr.copy.excludeDirs "node_modules/.cache"

既存のworktreeに後からコピーし直すには copy を使います。

git gtr copy feature/login                 # 設定済みのパターンでコピーする
git gtr copy -a -- ".env*"                 # すべての worktree に .env 系をコピーする
git gtr copy feature/login -n -- "**/.env*"  # 何がコピーされるかだけ確認する

copy は既存のファイルも上書きします。 後述のポート割り当てを済ませた .env にメインの .env をコピーすると、ポートも元に戻るため、コピー後に割り当てスクリプトを実行し直す必要があります。

フックで worktree ごとの環境を作る

ここからが本題です。 worktreeを並べて使うときに最後まで残る手間が、ポートの衝突でした。 git gtrのフックを使うと、worktreeを作った瞬間にそのworktree専用のポートを割り当てられます。

フックの種類と受け取れる情報

v2.11.1のgit gtrには、次の4種類のフックがあります。 postCd はv2.1.0にはありません。

フック実行されるタイミング主な用途
postCreateworktreeの作成後依存関係のインストール、.env の書き換え
preRemoveworktreeの削除前コンテナの停止など、ディレクトリが残っている間に必要な後始末。失敗すると削除を中止する
postRemoveworktreeの削除後通知やログ
postCdシェル統合の gtr cd などで移動した後環境変数の読み直し。今のシェルの中で実行される

postCreate と preRemove は、対象のworktreeをカレントディレクトリにして実行されます。 フックには次の環境変数が渡されます。

変数内容
REPO_ROOTメインのリポジトリのパス
WORKTREE_PATH作成された(削除される)worktreeのパス
BRANCHブランチ名

フックは add で複数登録でき、登録した順に実行されます。

git gtr config add gtr.hook.postCreate "npm install"
git gtr config add gtr.hook.postCreate "npm run build"

ポートを書き換えるスクリプトを用意する

方針は次のとおりです。

  • メインのリポジトリの .env には、基準のポート(Web 3000、API 8000、DB 5432)と PORT_OFFSET=0 を書いておく
  • .env は gtr.copy.include で新しいworktreeにコピーされる
  • postCreate フックで、ほかのworktreeが使っていない最小のオフセットを選び、コピーされた .env のポートを「基準 + オフセット」に書き換える

オフセットを使い回すのは、ポート番号を覚えやすくするためです。 一つ目のworktreeはWebが3001、APIが8001、DBが5433、二つ目は3002、8002、5434、と揃います。 worktreeを削除すれば、そのオフセットは次に作るworktreeで再利用されます。

スクリプトはリポジトリにコミットしておきます。 新しいworktreeの起点となるコミットに入っていれば、フックから実行できます。 以下の例を試す際は、後述する server.cjs と package.json(Dockerも使うなら compose.yaml)を先に用意し、スクリプトと一緒にコミットしてください。 .env はコミットせず、.gitignore に追加しておきます。

#!/usr/bin/env bash
# scripts/assign-ports.sh
# git gtr の postCreate フックから呼ばれ、worktree ごとに重ならないポートを .env に書き込む
set -euo pipefail

env_file="$WORKTREE_PATH/.env"
touch "$env_file"

# 他の worktree の .env に書かれた PORT_OFFSET を集める(メインリポジトリは 0 扱い)
used=" 0 "
while IFS= read -r line; do
  case "$line" in
    "worktree "*)
      wt="${line#worktree }"
      [ "$wt" = "$WORKTREE_PATH" ] && continue
      offset=$(grep -E '^PORT_OFFSET=' "$wt/.env" 2>/dev/null | tail -n 1 | cut -d= -f2 || true)
      [ -n "$offset" ] && used="$used$offset "
      ;;
  esac
done < <(git -C "$REPO_ROOT" worktree list --porcelain)

# 使われていない最小のオフセットを選ぶ
offset=1
while [[ "$used" == *" $offset "* ]]; do
  offset=$((offset + 1))
done

# KEY=VALUE を上書きする(無ければ追記)
set_env() {
  { grep -v -E "^$1=" "$env_file" || true; echo "$1=$2"; } > "$env_file.tmp"
  mv "$env_file.tmp" "$env_file"
}

set_env PORT_OFFSET "$offset"
set_env WEB_PORT $((3000 + offset))
set_env API_PORT $((8000 + offset))
set_env DB_PORT $((5432 + offset))

# Compose のプロジェクト名は小文字英数字、ハイフン、アンダースコアに限る
compose_name=$(printf '%s-%s' "$(basename "$REPO_ROOT")" "$(basename "$WORKTREE_PATH")" |
  LC_ALL=C tr '[:upper:]' '[:lower:]' | LC_ALL=C tr -c 'a-z0-9_-' '-')
case "$compose_name" in
  [a-z0-9]*) ;;
  *) compose_name="wt-$compose_name" ;;
esac
set_env COMPOSE_PROJECT_NAME "$compose_name"

echo "Assigned ports for $BRANCH: web=$((3000 + offset)) api=$((8000 + offset)) db=$((5432 + offset))"

ほかのworktreeの一覧は git worktree list --porcelain から取っています。 set_env は sed -i を使わずに書いています。 sed -i はmacOSとLinuxでオプションの書き方が違うためです。 COMPOSE_PROJECT_NAME は、後述するDocker Composeのための設定です。

メインのリポジトリの .env は次のようにしておきます。

API_KEY=dev-123
PORT_OFFSET=0
WEB_PORT=3000
API_PORT=8000
DB_PORT=5432

フックを登録して worktree を作る

git gtr config add gtr.copy.include ".env"
git gtr config add gtr.hook.postCreate "bash scripts/assign-ports.sh"

スクリプトをコミットしたブランチにいる状態で、worktreeを三つ作ってみます。 ここでは --from-current を付け、push前でもローカルのコミットを起点にします。

git gtr new feature/a --from-current
git gtr new feature/b --from-current
git gtr new feature/c --from-current
Assigned ports for feature/a: web=3001 api=8001 db=5433
Assigned ports for feature/b: web=3002 api=8002 db=5434
Assigned ports for feature/c: web=3003 api=8003 db=5435

feature/b のworktreeの .env は次のように書き換わっています。 API_KEY のような他の値は、メインからコピーされたまま残ります。

API_KEY=dev-123
PORT_OFFSET=2
WEB_PORT=3002
API_PORT=8002
DB_PORT=5434
COMPOSE_PROJECT_NAME=my-app-feature-b

feature/b を削除してから feature/d を作ると、空いたオフセット2が再利用されて3002番になります。

dev サーバーを並べて起動する

アプリ側は、.env の WEB_PORT を読んで起動するようにしておきます。 Node.js 20.6.0以降なら、--env-file オプションで .env を読み込めます。 すでにシェルで WEB_PORT をexportしている場合はそちらが優先されるため、ここではexportしない前提です(Node.js公式の説明)。

// server.cjs
const http = require("node:http");
const port = Number(process.env.WEB_PORT ?? 3000);
http
  .createServer((req, res) => res.end(`hello from ${process.cwd()}\n`))
  .listen(port, () => console.log(`listening on http://localhost:${port}`));
{
  "scripts": {
    "dev": "node --env-file=.env server.cjs"
  }
}

ターミナルを分けて、それぞれのworktreeでdevサーバーを起動します。

git gtr run 1 npm run dev           # メイン   → http://localhost:3000
git gtr run feature/a npm run dev   # feature/a → http://localhost:3001
git gtr run feature/c npm run dev   # feature/c → http://localhost:3003

サーバーはポートを奪い合うことなく同時に立ち上がり、ブラウザのタブを並べて各ブランチの画面を見比べられます。 AIエージェントにworktreeごとのタスクを任せている場合も、各エージェントが自分のポートでサーバーを起動して動作確認できます。

Next.js の場合

Next.jsは、PORT 環境変数を .env からは読み込みません。 公式ドキュメントにも、HTTPサーバーの起動が他のコードの初期化より先に行われるため、.env では設定できないと書かれています。 .env から WEB_PORT を取り出し、-p で渡す小さなラッパーを用意します。

#!/bin/sh
# scripts/dev-next.sh
# Next.js は .env から PORT を読まないので、WEB_PORT を取り出して -p で渡す
port=$(grep -E '^WEB_PORT=' .env 2>/dev/null | tail -n 1 | cut -d= -f2)
exec next dev -p "${port:-3000}"
{
  "scripts": {
    "dev": "sh scripts/dev-next.sh"
  }
}

.env をシェルで source せず、WEB_PORT の行だけを読んでいます。 .env の他の値に記号が含まれていても、シェルの構文として解釈されずに済みます。 この簡易ラッパーは、割り当てスクリプトが生成する WEB_PORT=3001 のような、引用符や行末コメントのない数値を前提にしています。 Viteでは、設定ファイルの評価時点では .env が自動で process.env に入るわけではありません。 loadEnv(mode, process.cwd(), '') で読み込み、返された WEB_PORT を数値にして server.port に渡します(Vite公式の説明)。

Docker Compose のポートとコンテナを分ける

DBやRedisをDocker Composeで立てている場合も、同じ .env が使えます。 Composeは、プロジェクトディレクトリにある .env を、compose.yaml 内の変数の展開に使います。

# compose.yaml
services:
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: postgres
    ports:
      - "${DB_PORT:-5432}:5432"
    volumes:
      - db-data:/var/lib/postgresql/data
volumes:
  db-data:

Composeのプロジェクト名は、指定しなければディレクトリ名になります。 worktreeのフォルダ名はブランチごとに違うので、何もしなくてもコンテナとボリュームは分かれます。 ただ、別のリポジトリに同じ名前のブランチがあると衝突するため、スクリプトでは COMPOSE_PROJECT_NAME に「リポジトリ名-フォルダ名」を書いています。 大文字やドットなどを含む名前は、そのままではComposeが受け付けないため、スクリプト内で使える文字に変換しています(Composeの命名規則)。 変換後の名前が同じになる別リポジトリを併用する場合は、識別用の接頭辞などを追加してください。

docker compose config で確認すると、worktreeごとに値が変わっています。

# メインのリポジトリ
name: my-app
        published: "5432"

# feature/c の worktree
name: my-app-feature-c
        published: "5435"

各worktreeでDBが独立するので、マイグレーションを試しても他のブランチのデータを壊しません。 ただし、ホスト側のアプリの接続先も DB_PORT に合わせる必要があります。 DATABASE_URL に localhost:5432 を固定していると、引き続きメインのDBへ接続してしまいます。 Compose内の別サービスから接続する場合は、ホスト公開ポートではなく db:5432 を使います。

削除するときにコンテナも止める

worktreeを消してもコンテナとボリュームは残るので、preRemove フックで止めておきます。 preRemove は削除前に、worktreeのディレクトリで実行されるので、そのworktreeの compose.yaml と .env がそのまま使えます。

git gtr config add gtr.hook.preRemove "docker compose down -v"

-v はボリュームも削除するオプションです。 worktreeのDBのデータを残しておきたい場合は外してください。 preRemove はGitによる削除可否の確認より先に実行されます。 未コミット変更などでworktreeの削除が失敗しても、down -v が成功していればDBのボリュームは削除済みなので、実行前に作業とデータを確認してください。

このスクリプトの限界

このスクリプトが見ているのは、各worktreeの .env に書かれたオフセットだけです。 worktreeと無関係なプロセスがすでに3001番を使っていても、その空き状況は確認しません。 必要なら lsof -iTCP:<ポート> -sTCP:LISTEN で空きを確かめる処理を足します。 また、git gtr new を同時に複数実行すると、同じオフセットを選ぶ可能性があります。 エージェントに一斉にworktreeを作らせる運用なら、作成だけは順番に行うようにします。

チームで設定を共有する

ここまでの設定は git config --local、つまり自分の .git/config に保存されています。 チーム全員で同じフックを使うには、リポジトリのルートに .gtrconfig を置いてコミットします。 書式は git config と同じです。

# .gtrconfig
[copy]
    include = .env
    exclude = **/.env.production

[hooks]
    postCreate = npm install
    postCreate = bash scripts/assign-ports.sh
    preRemove = docker compose down -v

[defaults]
    editor = cursor
    ai = claude

v2.11.1では、.gtrconfig に書いたフックや、エディタやAIツールの既定値は、各自が git gtr trust で承認するまで使われません。 リポジトリに悪意のあるコマンドを仕込まれても、cloneしただけでは動かないようにするための仕組みです。 承認の対象はフックのコマンド定義とエディタやAIツールの既定値で、それらが変わったら承認し直す必要があります。 呼び出されるスクリプト本体の変更まで検査する仕組みではありません。 v2.1.0にはこの承認機能がないため、共有設定を使う前に更新してください。

git gtr trust   # .gtrconfig に書かれたコマンドを確認して承認する

エディタなどの単一値の設定が重なった場合は、git config --local、.gtrconfig、git config --global の順に優先されます。 一方、コピー対象やフックは複数の設定元の値が結合され、同じ値が重複していれば一つにまとめられます。 ローカルでフックを設定しても、チーム共有のフックを置き換えられるわけではありません。 v2.11.1のフックはGit設定の値を先に、承認済み .gtrconfig の値を後に実行します。 共有設定へ移行したら、以前のローカル設定が意図せず残っていないかも確認してください(設定の実装、フックの実装)。

AI エージェントと組み合わせる

git gtrはAIエージェントからも使えます。 エージェントがシェルを実行できれば、MCPサーバーのような追加の仕組みは要りません。

v2.11.1では、new に --porcelain を付けると、作ったworktreeのパスを機械的に読める形式で出力します。

git gtr new agent/fix-login --porcelain
path	/Users/you/src/my-app-worktrees/agent-fix-login
branch	agent/fix-login
hook_status	ran

hook_status には、フックがすべて実行された(ran)、承認されていない .gtrconfig のフックをスキップした(skipped-untrusted)などの結果が入ります。 公式のエージェント向けガイドには、エージェントに守らせる AGENTS.md の記述例も載っています。 要点は、worktreeは --porcelain で作って返された path の中だけで作業すること、git gtr trust は人間が行うこと、削除やブランチの破棄はユーザーの明示的な指示があるまで行わないこと、の三つです。

人間が複数のエージェントを並べる場合は、ターミナルを分けて次のように起動するだけです。

git gtr new feature/auth -a       # ターミナル1: 認証の改修
git gtr new fix/admin-table -a    # ターミナル2: 管理画面の不具合

作成コマンドは、一つ目のポート割り当てが終わってから二つ目を実行します。 フックが正常に完了し、割り当て先をほかのプロセスが使っていなければ、各エージェントが異なるポートでdevサーバーを起動できます。

つまずきやすい点

最後に、実際に使っていて引っかかりやすい点をまとめます。

管理外のファイルが残っていると削除できない

git gtr rm は内部で git worktree remove を使うため、変更中のファイルやGit管理外のファイルが残っているworktreeは削除できません。 フックが .gitignore に入っていないファイルを生成すると、作業していなくても削除できなくなります。 フックが書き出すファイルは .gitignore に入れておきます。 .gitignore に入っている .env や node_modules は、削除の妨げになりません。 変更を捨ててよいと分かっている場合だけ --force を付けます。

--delete-branch は未マージのブランチも削除する

git gtr rm --delete-branch は内部で git branch -D を使うため、マージされていないコミットがあるブランチも削除します。 v2.1.0での動作確認に加え、v2.11.1の実装でも確認できます。 通常はブランチ削除の確認が出ますが、承諾すると未マージでも削除され、--yes を付けるとその確認も省略されます。 ブランチ参照の削除であり、コミットのデータがその場で消去されるわけではありませんが、未pushの作業への参照を失うことになります。 pushしていない作業があるworktreeでは、--delete-branch を付ける前に git gtr run <ブランチ> git log で中身を確認してください。

.env のコピーは認証情報のコピーでもある

gtr.copy.include で .env をコピーすると、その中のAPIキーもworktreeの数だけ複製されます。 開発用の認証情報に限り、本番用のファイルは gtr.copy.exclude で除外しておきます。 node_modules などのディレクトリにも、キャッシュとしてトークンが含まれることがあります。

同じブランチを二か所で開きたいとき

Gitは通常、同じブランチを複数のworktreeでチェックアウトすることを拒否します。 git gtrでは --force と --name を併用すると制約を外せますが、二か所から同じブランチに書き込むと履歴の衝突が起きやすくなります。 同じ機能を複数のエージェントで分担したい場合も、まずは --from-current で派生ブランチを作り、後でマージする方が安全です。

この記事をシェア