本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
Git Worktree AddでDevOpsブランチ管理を効率化
DevOps環境において、複数のブランチでの並行作業や迅速なコンテキストスイッチは日常茶飯事です。従来のgit checkoutコマンドでは、作業中の変更を一時的に退避(stash)するかコミットする必要があり、これが開発フローのボトルネックとなることが少なくありません。本記事では、git worktree addコマンドを活用し、このような課題を解決し、より効率的で安全なブランチ管理を実現する方法をDevOpsエンジニアの視点から解説します。
要件と前提
Git Worktreeとは
git worktreeコマンドは、既存のGitリポジトリに対して、追加の作業ディレクトリ(ワークツリー)を作成する機能です。各ワークツリーはメインリポジトリのオブジェクトデータベースを共有しつつ、独自のHEAD、インデックス、ワーキングツリーを持ちます。これにより、異なるブランチでの作業を同時に行うことが可能になり、コンテキストスイッチのオーバーヘッドを大幅に削減できます。例えば、メイン開発ブランチで作業しつつ、別のワークツリーで緊急のホットフィックスブランチを修正したり、並行して別の機能ブランチの開発を進めたりといった運用が可能です。
前提条件
Gitの基本的な操作(コミット、ブランチ作成、チェックアウトなど)に習熟していること。
Linux環境(bashシェル)で作業を行うこと。
curl、jq、gitコマンドが利用可能であること。systemdの基本的な知識があること。root権限が必要な操作は明示し、最小権限の原則に従うこと。
実装
安全なシェルスクリプトの基本
DevOpsの自動化では、スクリプトの安全性が極めて重要です。以下の原則に従い、堅牢なスクリプトを作成します。
set -euo pipefail:-e: コマンドが失敗した場合、すぐにスクリプトを終了します。-u: 未定義の変数を使用しようとするとエラーになります。-o pipefail: パイプライン内で一つでもコマンドが失敗した場合、パイプライン全体の終了コードを失敗とします。
trap: スクリプト終了時にクリーンアップ処理(一時ディレクトリの削除など)を実行します。一時ディレクトリの利用:
mktemp -dで作成し、trapで確実に削除します。
Git Worktreeの基本的な使い方
既存のGitリポジトリ(例:/path/to/my_project)で作業していると仮定します。
#!/bin/bash
set -euo pipefail
# 一時ディレクトリの作成と終了時のクリーンアップ
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
echo "一時ディレクトリ: $tmpdir"
# メインリポジトリのパス
REPO_PATH="/path/to/my_project"
# 新しいワークツリーを作成するパス
WORKTREE_PATH="${REPO_PATH}_feature_branch"
# 作成するブランチ名
BRANCH_NAME="feature/new-feature-XYZ"
echo "リポジトリパス: $REPO_PATH"
echo "ワークツリーパス: $WORKTREE_PATH"
echo "ブランチ名: $BRANCH_NAME"
# 既存のワークツリーパスが存在するかチェックし、存在する場合はエラーで終了
if [ -d "$WORKTREE_PATH" ]; then
echo "エラー: ワークツリーパス '$WORKTREE_PATH' が既に存在します。操作を中止します。" >&2
exit 1
fi
# メインリポジトリに移動
pushd "$REPO_PATH" > /dev/null
# 新しいブランチを作成し、そのブランチでワークツリーを追加
# -b オプションで新しいブランチを作成し、それに切り替える
echo "ワークツリー '$WORKTREE_PATH' をブランチ '$BRANCH_NAME' で作成します..."
if git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH"; then
echo "ワークツリーが正常に作成されました。"
else
echo "エラー: ワークツリーの作成に失敗しました。" >&2
popd > /dev/null
exit 1
fi
# メインリポジトリから戻る
popd > /dev/null
echo "完了しました。"
echo "新しいワークツリーに移動するには: cd \"$WORKTREE_PATH\""
curlとjqを用いたブランチ管理の自動化
例えば、GitHub APIから特定のレポジトリのブランチリストを取得し、条件に基づいてワークツリーを作成するスクリプトを考えます。ここでは、特定のプレフィックスを持つブランチのみを対象とします。
#!/bin/bash
set -euo pipefail
# 一時ディレクトリの作成と終了時のクリーンアップ
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
echo "一時ディレクトリ: $tmpdir"
# GitHub API設定
GITHUB_ORG="your-github-org"
GITHUB_REPO="your-repo-name"
GITHUB_TOKEN="${GITHUB_TOKEN:-}" # 環境変数から取得、未設定なら空
# メインリポジトリのパス
REPO_BASE_DIR="/path/to/your/git_projects"
TARGET_REPO_PATH="${REPO_BASE_DIR}/${GITHUB_REPO}"
# Worktreeを作成するブランチ名のプレフィックス
BRANCH_PREFIX="feature/"
# APIリクエストのURL
API_URL="https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/branches"
# curlのオプション (TLS、タイムアウト、リトライ)
CURL_OPTS=(
--fail
--silent
--show-error
--location
--retry 5
--retry-delay 3
--connect-timeout 10
--max-time 30
)
# 認証トークンがあれば追加
if [ -n "$GITHUB_TOKEN" ]; then
CURL_OPTS+=( -H "Authorization: token $GITHUB_TOKEN" )
fi
echo "${GITHUB_ORG}/${GITHUB_REPO} のブランチリストを取得します..."
# curlでAPIからブランチリストを取得
BRANCH_LIST_JSON=$(curl "${CURL_OPTS[@]}" "$API_URL")
# エラーチェック
if [ $? -ne 0 ]; then
echo "エラー: GitHub APIからのブランチリスト取得に失敗しました。" >&2
exit 1
fi
# jqでブランチ名を抽出
# 特定のプレフィックスを持つブランチのみをフィルタリング
BRANCH_NAMES=$(echo "$BRANCH_LIST_JSON" | jq -r ".[] | select(.name | startswith(\"$BRANCH_PREFIX\")) | .name")
# メインリポジトリが存在しない場合はクローン
if [ ! -d "$TARGET_REPO_PATH/.git" ]; then
echo "リポジトリ '$TARGET_REPO_PATH' が存在しないため、クローンします。"
git clone "git@github.com:${GITHUB_ORG}/${GITHUB_REPO}.git" "$TARGET_REPO_PATH"
if [ $? -ne 0 ]; then
echo "エラー: リポジトリのクローンに失敗しました。" >&2
exit 1
fi
fi
pushd "$TARGET_REPO_PATH" > /dev/null
# 各ブランチに対してワークツリーを作成
for branch in $BRANCH_NAMES; do
WORKTREE_DIR="${REPO_BASE_DIR}/${GITHUB_REPO}_${branch//\//-}" # スラッシュをハイフンに置換
# 既にワークツリーが存在するか確認
if git worktree list | grep -q "${WORKTREE_DIR}"; then
echo "ワークツリー '$WORKTREE_DIR' は既に存在します。スキップします。"
continue
fi
echo "ブランチ '$branch' のワークツリーを '$WORKTREE_DIR' に作成します..."
if git worktree add -b "$branch" "$WORKTREE_DIR"; then
echo "ワークツリー '$WORKTREE_DIR' が正常に作成されました。"
else
echo "エラー: ブランチ '$branch' のワークツリー作成に失敗しました。" >&2
fi
done
popd > /dev/null
echo "全ての対象ブランチのワークツリー作成処理が完了しました。"
このスクリプトは、指定されたGitHubリポジトリからブランチリストを取得し、feature/プレフィックスを持つブランチに対して個別のワークツリーを作成します。curlコマンドは、--retryや--retry-delayオプションで一時的なネットワークエラーに対応し、jqでJSONレスポンスから必要なブランチ名を抽出しています。
検証
ワークツリーが正しく作成されたことを確認します。
git worktree listコマンドで確認: メインリポジトリのパス(例:/path/to/my_project)に移動し、git worktree listを実行します。cd /path/to/my_project git worktree list
出力例:
/path/to/my_project <commit-hash> [main] /path/to/my_project_feature_branch <commit-hash> [feature/new-feature-XYZ]
この出力は、メインリポジトリと新しく作成されたワークツリーが正しく関連付けられていることを示します。
各ワークツリーでの作業確認: 新しいワークツリーに移動し、
git statusなどでブランチが切り替わっていることを確認します。cd /path/to/my_project_feature_branch git status
On branch feature/new-feature-XYZのような出力が表示されれば成功です。ここでファイルを作成・編集し、コミットすることができます。メインリポジトリ側のブランチには影響しません。
運用
DevOps環境では、ワークツリーの定期的な管理も重要です。ここでは、systemdを用いて、定期的に不要なワークツリーをクリーンアップする例を示します。
Git Worktreeを活用した開発フロー (Mermaid)
Git worktreeを利用した、並行開発の一般的なフローを図で示します。
graph TD
A["メインリポジトリ"] --> B{"新機能/ホットフィックス"};
B -- feature/A --> C[Worktree-A];
B -- hotfix/B --> D[Worktree-B];
C -- 開発/コミット --> C;
D -- 開発/コミット --> D;
C -- プッシュ/PR --> E["リモートリポジトリ"];
D -- プッシュ/PR --> E;
E -- マージ --> A;
このフローでは、メインリポジトリから複数のワークツリーを分岐させ、それぞれで独立した開発を進め、最終的にリモートリポジトリ経由でメインリポジトリにマージする流れを示しています。
systemdによるワークツリーの定期的なクリーンアップ
不要になった、またはデタッチされたワークツリーを定期的に整理するために、git worktree pruneコマンドをsystemdで自動実行します。
1. クリーンアップスクリプトの作成
/usr/local/bin/git_worktree_cleanup.shとして以下のスクリプトを作成します。
注意: このスクリプトは、開発者のホームディレクトリや特定のプロジェクトディレクトリ内で実行されることを想定しています。rootユーザーで実行すべきではありません。systemdユニットのUserオプションで実行ユーザーを指定します。
#!/bin/bash
set -euo pipefail
LOG_FILE="/var/log/git-worktree-cleanup.log"
# 処理対象のベースディレクトリ (例: 開発者のホームディレクトリなど)
# ここには複数のGitリポジトリが存在する可能性がある
BASE_DIR="/home/devops_user/git_repos"
# ログ関数
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
log "Git Worktree クリーンアップを開始します (PID: $$)"
# BASE_DIR以下のすべてのGitリポジトリを検索し、worktree pruneを実行
find "$BASE_DIR" -type d -name ".git" | while read -r git_dir; do
repo_path=$(dirname "$git_dir")
if [ -d "$repo_path" ]; then
log "リポジトリ '$repo_path' で git worktree prune を実行中..."
# 実行ユーザーがディレクトリへのアクセス権を持つことを確認
if ! sudo -u "$(id -un)" test -w "$repo_path"; then
log "警告: ユーザー $(id -un) に '$repo_path' への書き込み権限がありません。スキップします。"
continue
fi
# git worktree prune は現在のリポジトリと関連する全てのワークツリーをチェック
# デタッチされた(存在しない)ワークツリーを削除
if pushd "$repo_path" > /dev/null; then
git worktree prune --verbose 2>&1 | while IFS= read -r line; do
log " $line"
done
log " 現在のワークツリーリスト:"
git worktree list 2>&1 | while IFS= read -r line; do
log " $line"
done
popd > /dev/null
else
log "エラー: '$repo_path' への移動に失敗しました。スキップします。"
fi
fi
done
log "Git Worktree クリーンアップを完了しました。"
スクリプトに実行権限を付与します: chmod +x /usr/local/bin/git_worktree_cleanup.sh
2. systemd Unitファイルの作成
/etc/systemd/system/git-worktree-cleanup.serviceを作成します。
[Unit] Description=Git Worktree Cleanup Service Documentation=https://git-scm.com/docs/git-worktree # このサービスがファイルシステムが利用可能になった後に実行されるようにする After=network-online.target [Service] Type=oneshot # 実行ユーザーを指定。root権限で実行しないように注意! User=devops_user Group=devops_user ExecStart=/usr/local/bin/git_worktree_cleanup.sh # 失敗した場合、5秒後に再起動を試みる Restart=on-failure RestartSec=5s # 標準出力/エラーをログに記録 StandardOutput=journal StandardError=journal # ログファイルはスクリプト内で管理しているため、ここではジャーナルにのみ出力 # 環境変数を指定する場合 (例: export PATH=/usr/local/bin:$PATH) # Environment="PATH=/usr/local/bin:/usr/bin:/bin" [Install] WantedBy=multi-user.target
UserとGroupは、実際にGitリポジトリを所有しているユーザー名とグループ名に置き換えてください。
3. systemd Timerファイルの作成
/etc/systemd/system/git-worktree-cleanup.timerを作成します。
[Unit] Description=Run Git Worktree Cleanup daily [Timer] # 毎日午前3時に実行 OnCalendar=*-*-* 03:00:00 # 起動時にサービスを実行する (タイマーが有効化された時、前回実行時刻から遅延がある場合) Persistent=true [Install] WantedBy=timers.target
4. systemdサービスの有効化と起動
root権限で以下のコマンドを実行します。
sudo systemctl daemon-reload sudo systemctl enable git-worktree-cleanup.timer sudo systemctl start git-worktree-cleanup.timer
5. ログの確認
サービスとタイマーの状態、およびログを確認します。
sudo systemctl status git-worktree-cleanup.service sudo systemctl status git-worktree-cleanup.timer journalctl -u git-worktree-cleanup.service --since "today" tail -f /var/log/git-worktree-cleanup.log
これにより、毎日午前3時(JST)に、指定されたユーザー権限でクリーンアップスクリプトが実行され、不要なワークツリーが整理されます。
root権限の扱いと権限分離
git worktree操作の原則:git worktreeコマンドは、通常、開発者自身のユーザーアカウントで実行されるべきです。Gitリポジトリ内のファイルシステム操作が伴うため、誤ってrootで実行すると、ファイルの所有権がrootに変更され、その後の開発作業で権限エラーが発生する可能性があります。systemdでの権限分離: 上記の
systemdユニットの例では、User=devops_userとGroup=devops_userを指定することで、サービスを特定の非rootユーザーとして実行しています。これにより、スクリプトがroot権限で不必要に動作することを防ぎ、最小権限の原則に準拠しています。sudoの利用: スクリプト内でsudoを使用する場合は、sudo -u "$(id -un)"のように、現在のユーザーとしてコマンドを実行するよう明示的に指定するなど、細心の注意を払う必要があります。
トラブルシュート
ワークツリーの削除
不要になったワークツリーは、以下のコマンドで削除します。
# ワークツリーが存在するパス WORKTREE_PATH="/path/to/my_project_feature_branch" # メインリポジトリのパス MAIN_REPO_PATH="/path/to/my_project" # メインリポジトリから削除コマンドを実行 pushd "$MAIN_REPO_PATH" > /dev/null git worktree remove "$WORKTREE_PATH" popd > /dev/null # ワークツリーのディレクトリも物理的に削除 rm -rf "$WORKTREE_PATH"
git worktree removeコマンドは、Gitの管理下からワークツリーのエントリを削除しますが、物理的なディレクトリは残ることがあります。そのため、rm -rfで明示的に削除する必要があります。
デタッチされたワークツリーのクリーンアップ
ワークツリーのディレクトリが手動で削除されたり、アクセスできなくなったりすると、git worktree listに「(detached)」と表示されることがあります。このようなデタッチされたエントリは、git worktree prune`で整理できます。
cd /path/to/my_project # メインリポジトリに移動 git worktree prune
このコマンドは、ファイルシステム上に存在しない(削除された)ワークツリーのエントリをGitの内部リストから削除します。上記で示したsystemdのクリーンアップスクリプトもこのコマンドを利用しています。
“fatal: ‘path’ is already checked out at ‘path'” エラー
このエラーは、あるワークツリーでチェックアウトされているブランチを、別のワークツリー(またはメインリポジトリ)でチェックアウトしようとした際に発生します。Git Worktreeの設計上、同じブランチを複数のワークツリーで同時にチェックアウトすることはできません。 解決策としては、以下のいずれかを行います。
別のワークツリーでそのブランチを
git checkoutし、作業を終えるか、別のブランチに切り替える。そのブランチの作業を一時的に別のワークツリーに移動させる(まれなケース)。
別のワークツリーで作業中のブランチとは異なる、新しいブランチを作成する。
まとめ
git worktree addは、DevOps環境におけるブランチ管理を大幅に効率化する強力なツールです。複数のブランチでの並行作業をスムーズに行い、コンテキストスイッチのオーバーヘッドを削減することで、開発者の生産性を向上させます。本記事では、安全なシェルスクリプトの書き方、curlとjqを用いた自動化の例、systemdによる定期的なクリーンアップ、そして一般的なトラブルシュートについて解説しました。
{{jst_today}}現在、git worktreeはGitのコア機能として安定しており、バージョン2.5以降で広く利用可能です(Git SCM Documentationより)。これらのプラクティスをDevOpsのワークフローに組み込むことで、より堅牢で効率的な開発・運用体制を構築できるでしょう。
参考文献:
- Git SCM Documentation, “git-worktree”, Accessed on {{jst_today}}, https://git-scm.com/docs/git-worktree

