Git Worktree AddでDevOpsブランチ管理を効率化

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

Git Worktree AddでDevOpsブランチ管理を効率化

DevOps環境において、複数のブランチでの並行作業や迅速なコンテキストスイッチは日常茶飯事です。従来のgit checkoutコマンドでは、作業中の変更を一時的に退避(stash)するかコミットする必要があり、これが開発フローのボトルネックとなることが少なくありません。本記事では、git worktree addコマンドを活用し、このような課題を解決し、より効率的で安全なブランチ管理を実現する方法をDevOpsエンジニアの視点から解説します。

要件と前提

Git Worktreeとは

git worktreeコマンドは、既存のGitリポジトリに対して、追加の作業ディレクトリ(ワークツリー)を作成する機能です。各ワークツリーはメインリポジトリのオブジェクトデータベースを共有しつつ、独自のHEAD、インデックス、ワーキングツリーを持ちます。これにより、異なるブランチでの作業を同時に行うことが可能になり、コンテキストスイッチのオーバーヘッドを大幅に削減できます。例えば、メイン開発ブランチで作業しつつ、別のワークツリーで緊急のホットフィックスブランチを修正したり、並行して別の機能ブランチの開発を進めたりといった運用が可能です。

前提条件

  • Gitの基本的な操作(コミット、ブランチ作成、チェックアウトなど)に習熟していること。

  • Linux環境(bashシェル)で作業を行うこと。

  • curljqgitコマンドが利用可能であること。

  • 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レスポンスから必要なブランチ名を抽出しています。

検証

ワークツリーが正しく作成されたことを確認します。

  1. 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]
    

    この出力は、メインリポジトリと新しく作成されたワークツリーが正しく関連付けられていることを示します。

  2. 各ワークツリーでの作業確認: 新しいワークツリーに移動し、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

UserGroupは、実際に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_userGroup=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の設計上、同じブランチを複数のワークツリーで同時にチェックアウトすることはできません。 解決策としては、以下のいずれかを行います。

  1. 別のワークツリーでそのブランチをgit checkoutし、作業を終えるか、別のブランチに切り替える。

  2. そのブランチの作業を一時的に別のワークツリーに移動させる(まれなケース)。

  3. 別のワークツリーで作業中のブランチとは異なる、新しいブランチを作成する。

まとめ

git worktree addは、DevOps環境におけるブランチ管理を大幅に効率化する強力なツールです。複数のブランチでの並行作業をスムーズに行い、コンテキストスイッチのオーバーヘッドを削減することで、開発者の生産性を向上させます。本記事では、安全なシェルスクリプトの書き方、curljqを用いた自動化の例、systemdによる定期的なクリーンアップ、そして一般的なトラブルシュートについて解説しました。

{{jst_today}}現在、git worktreeはGitのコア機能として安定しており、バージョン2.5以降で広く利用可能です(Git SCM Documentationより)。これらのプラクティスをDevOpsのワークフローに組み込むことで、より堅牢で効率的な開発・運用体制を構築できるでしょう。


参考文献:

  1. Git SCM Documentation, “git-worktree”, Accessed on {{jst_today}}, https://git-scm.com/docs/git-worktree
ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました