💡

Git Worktree を楽に扱うためのスクリプトと Copilot の活用例

に公開

最終的にどうなったか

最終的には、以下のような形で Git Worktree を使っています。

  1. プロンプトの実行

    Issue 番号を入力するだけで実行できます

  2. 実行内容の提案と確認

    Issue 内容をもとに、スクリプトへ渡すパラメータを提案し、実行前に確認します

  3. 必要に応じてその場で修正

    提案内容に調整が必要な場合は、チャット内のターミナル上で修正できます

  4. 問題なければ承認して実行


    承認後、新しいワークスペースが起動します。ウィンドウタイトルが設定されるため、作業内容を把握しやすくなっています

  • GitHub Copilot の カスタムプロンプトIssue 番号を渡すだけ
  • 新しい Worktree が作成され
  • 新規ブランチの作成・チェックアウト
  • VSCode の新しいワークスペース起動
  • ウィンドウタイトル・色の設定
  • 必要なファイルのコピー

までを 一連の流れで自動化しています。

コマンドを覚える必要はなく、
日常的な操作は Copilot のチャットから完結します。


はじめに

Git Worktree は、最近よく見かけるようになった Git の機能です。
AI と並列で開発するケースが増えたことで、
複数ブランチを同時に扱える点が評価されているのだと思います。

一方で、個人的には次の点がずっと気になっていました。

  • 結局は Git Worktree のコマンドを覚える必要がある
  • 導入時の学習コストがやや高い
  • 「業務改善」と言うには、少し手間が多い

便利なのは分かるものの、
日常業務に定着させるのが難しいと感じていました。


Git Worktree をざっくり説明すると

Git Worktree は、

  • 1つの Git リポジトリをルートとして
  • 別ディレクトリに追加の作業環境を作成できる仕組み

です。

ブランチごとにディレクトリが分かれるため、

  • 実装中の作業を止めずにレビュー対応
  • 別チケットの対応を並行して進める

といった使い方が可能になります。

VSCode では GUI から Worktree を確認でき、
新しいウィンドウとして開くこともできます。


個人的に感じているメリット

一番大きいと感じているのは、

今やっている作業を中断しなくていいこと

です。

  • stash やブランチ切り替えが不要
  • コンテキストスイッチが減る
  • 複数のタスクを自然に並行できる

結果として、
作業の流れがかなりスムーズになりました。


ただし、コマンド前提だと定着しづらい

多くの記事では Git Worktree の便利さが紹介されていますが、
読んでいて感じたのは、

コマンドを覚える前提だと、業務改善としては少し厳しい

という点でした。

最近は CLI を直接叩く場面も減っていますし、
たまにしか使わないコマンドはすぐ忘れてしまいます。

そこで、

自分の業務に特化したスクリプトを作る

という方向に舵を切りました。


作成した Git Worktree スクリプトでやっていること

内部的には Git Worktree を使っていますが、
利用者側はそれを意識しなくて済むようにしています。

主に以下の処理をまとめています。

  • Worktree の作成
  • ブランチの作成・チェックアウト
  • VSCode の新規ワークスペース起動
  • ウィンドウタイトル・色の設定
  • 必要なファイルのコピー

正直、今見ると
「少しやりすぎているな」と感じる部分もあります。

スクリプトの全体像(クリックで展開)
#!/bin/bash

# =====================================================================
# Git Worktree作成スクリプト - 使いかたガイド
#
# 概要:
# - 指定したベースブランチから Git Worktree を作成し、必要に応じて新規ブランチを派生します。
# - VS Code を新しいウィンドウで開き、ワークスペースの色設定を自動/手動で適用します。
# - 入力が `origin/<branch>` 形式でも自動的に正規化して動作します。
#
# 引数:
#   1) ベースブランチ名        例: main, base/feature-x
#   2) worktree名              例: feature/new-work, issue/45-new-feature
#   3) 新規ブランチ作成フラグ  y|n(省略可、デフォルト n)
#   4) 色(省略可)            blue|green|purple|cyan|orange(未指定時は自動判定)
#
# 自動色選択ルール:
# - 名前が review-*     → purple(レビュー)
# - 名前が test-*       → cyan(テスト)
# - 名前が experiment-* → orange(実験)
# - 上記以外            → green(開発)
#
# 代表的なユースケース:
# 1) リモートのベースから派生ブランチを作成
#    bash ./scripts/create-worktree.sh origin/base/feature-x issue/45-new-feature y green
#    → ベースが別worktreeで使用中でも、新規ブランチ issue/45-new-feature を派生して作成。
#
# 2) 既存ベースブランチでそのまま作業
#    bash ./scripts/create-worktree.sh main review-feature/ui n purple
#    → main が未使用なら main を checkout。使用中なら自動で新規ブランチ(worktree名由来)を作成。
#
# 3) 色の自動判定を使う
#    bash ./scripts/create-worktree.sh base/feature-y experiment-data-reader y
#    → worktree名が experiment-* なので orange が適用。色は .vscode/settings.json に反映。
#
# 後片付け(削除):
# - Worktree の削除:  git worktree remove <作成先パス>
# - ブランチも削除:   git branch -D <作成したブランチ名>
# =====================================================================

# Git Worktree作成スクリプト
# 引数:
#   $1 - ベースとなるブランチ名
#   $2 - 作成するworktree名
#   $3 - (オプション) 新規ブランチ作成フラグ (y/n)
#   $4 - (オプション) ワークスペースの色 (blue|green|purple|cyan|orange または空文字で自動選択)
#   $5 - (オプション) ウィンドウタイトル (例: "Issue #45 - 新機能実装")

set -e

# スクリプトのディレクトリを取得
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# 色付きメッセージ用の設定
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 使用方法の表示
usage() {
    echo "使用方法: $0 <ベースブランチ名> <worktree名> [y/n] [色] [ウィンドウタイトル]"
    echo ""
    echo "引数:"
    echo "  ベースブランチ名: ベースとなるブランチ (例: main, develop)"
    echo "  worktree名: 作成するworktree名 (例: feature/new-work)"
    echo "  新規ブランチ (オプション): y/n (デフォルト: n)"
    echo "  色 (オプション): ワークスペースの色"
    echo "    - blue: ブルー (メイン)"
    echo "    - green: グリーン (開発)"
    echo "    - purple: パープル (レビュー)"
    echo "    - cyan: シアン (テスト)"
    echo "    - orange: オレンジ (実験)"
    echo "    - 空: 自動選択 (デフォルト)"
    echo "  ウィンドウタイトル (オプション): VS Codeのウィンドウタイトル"
    echo "    例: 'Issue #45 - 新機能実装'"
    echo ""
    echo "例:"
    echo "  $0 main feature/new-work"
    echo "  $0 main feature/new-work y"
    echo "  $0 main review-feature/ui n purple"
    echo "  $0 main issue/45-new-feature y green 'Issue #45 - 新機能実装'"
    echo ""
    echo "このスクリプトは指定されたベースブランチから新しいworktreeを作成し、"
    echo "そのディレクトリでVS Codeを開きます。"
    echo ""
    echo "自動色選択ルール:"
    echo "  - review- で始まる → purple (レビュー)"
    echo "  - test- で始まる → cyan (テスト)"
    echo "  - experiment- で始まる → orange (実験)"
    echo "  - その他 → green (開発)"
    exit 1
}

# Worktree名から自動的に色を選択する関数
select_workspace_color() {
    local worktree_name="$1"
    local manual_color="$2"

    # 手動指定がある場合はそれを優先
    if [ -n "$manual_color" ]; then
        # 有効な色かチェック
        case "$manual_color" in
            blue|green|purple|cyan|orange)
                echo "$manual_color"
                return
                ;;
            *)
                echo -e "${RED}エラー: 無効な色指定です: ${manual_color}${NC}" >&2
                echo -e "${YELLOW}有効な色: blue, green, purple, cyan, orange${NC}" >&2
                exit 1
                ;;
        esac
    fi

    # Worktree名から自動判定
    if [[ "$worktree_name" == review-* ]]; then
        echo "purple"
    elif [[ "$worktree_name" == test-* ]]; then
        echo "cyan"
    elif [[ "$worktree_name" == experiment-* ]]; then
        echo "orange"
    else
        echo "green"
    fi
}

# 引数チェック
if [ $# -lt 2 ]; then
    echo -e "${RED}エラー: 引数が不足しています${NC}"
    usage
fi

BASE_BRANCH="$1"
WORKTREE_NAME="$2"
CREATE_NEW_INPUT="${3:-n}"  # 第3引数(新規ブランチ作成)、デフォルトはn
WORKSPACE_COLOR="${4:-}"  # 第4引数(色指定)、デフォルトは空文字
WINDOW_TITLE="${5:-}"  # 第5引数(ウィンドウタイトル)、デフォルトは空文字

# 第3引数がyの場合、新規ブランチ作成フラグをtrueに
CREATE_NEW_BRANCH_FLAG=false
if [[ "$CREATE_NEW_INPUT" == "y" || "$CREATE_NEW_INPUT" == "Y" ]]; then
    CREATE_NEW_BRANCH_FLAG=true
fi

# ワークスペースの色を決定
SELECTED_COLOR=$(select_workspace_color "$WORKTREE_NAME" "$WORKSPACE_COLOR")

# 入力されたベースブランチの正規化(origin/付きも許容)
ORIGINAL_BASE_BRANCH="$BASE_BRANCH"
REMOTE_NAME="origin"
if [[ "$BASE_BRANCH" == ${REMOTE_NAME}/* ]]; then
    NORMALIZED_LOCAL_BRANCH="${BASE_BRANCH#${REMOTE_NAME}/}"
    NORMALIZED_REMOTE_BRANCH="${REMOTE_NAME}/${NORMALIZED_LOCAL_BRANCH}"
else
    NORMALIZED_LOCAL_BRANCH="$BASE_BRANCH"
    NORMALIZED_REMOTE_BRANCH="${REMOTE_NAME}/${BASE_BRANCH}"
fi

# YAMLファイルから色設定を読み込む関数
read_color_config() {
    local color="$1"
    local config_file="${SCRIPT_DIR}/workspace-colors.yaml"
    local key="$2"

    # YAMLから指定されたキーの値を取得(簡易パーサー)
    local section_found=false
    local result=""

    while IFS= read -r line; do
        # 色セクションの開始を検出
        if [[ "$line" =~ ^[[:space:]]*${color}:[[:space:]]*$ ]]; then
            section_found=true
            continue
        fi

        # 次の色セクションに到達したら終了
        if $section_found && [[ "$line" =~ ^[[:space:]]*[a-z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]{4,} ]]; then
            break
        fi

        # 指定されたキーを検索
        if $section_found && [[ "$line" =~ ${key}:[[:space:]]*\"(.+)\" ]]; then
            result="${BASH_REMATCH[1]}"
            break
        fi
    done < "$config_file"

    echo "$result"
}

# .vscode/settings.jsonファイルを生成する関数
generate_workspace_settings() {
    local worktree_path="$1"
    local color="$2"
    local vscode_dir="${worktree_path}/.vscode"
    local settings_file="${vscode_dir}/settings.json"

    echo -e "${YELLOW}ワークスペース設定ファイルを作成中...${NC}"

    # .vscodeディレクトリを作成
    mkdir -p "$vscode_dir"

    # メインリポジトリの.vscode/settings.jsonをコピー(存在する場合)
    local main_settings="${REPO_ROOT}/.vscode/settings.json"
    if [ -f "$main_settings" ]; then
        echo -e "${YELLOW}メインリポジトリの設定をコピーしています...${NC}"
        cp "$main_settings" "$settings_file"
    fi

    # 色設定を読み込み
    local peacock_color=$(read_color_config "$color" "peacock")
    local title_bg=$(read_color_config "$color" "activeBackground")
    local title_fg=$(read_color_config "$color" "activeForeground")
    local title_inactive_bg=$(read_color_config "$color" "inactiveBackground")
    local title_inactive_fg=$(read_color_config "$color" "inactiveForeground")
    local status_bg=$(read_color_config "$color" "background")
    local status_fg=$(read_color_config "$color" "foreground")
    local activity_bg=""
    local activity_fg=""
    local activity_inactive_fg=""
    local badge_bg=""
    local badge_fg=""

    # activityBarとactivityBarBadgeの設定を個別に読み込み
    local in_activity_bar=false
    local in_activity_badge=false
    local in_color_section=false

    while IFS= read -r line; do
        # 色セクションの検出
        if [[ "$line" =~ ^[[:space:]]*${color}:[[:space:]]*$ ]]; then
            in_color_section=true
            continue
        fi

        # 他の色セクションに入ったら終了
        if $in_color_section && [[ "$line" =~ ^[[:space:]]*[a-z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]{2,} ]]; then
            break
        fi

        if $in_color_section; then
            # activityBar セクション
            if [[ "$line" =~ ^[[:space:]]{2}activityBar:[[:space:]]*$ ]]; then
                in_activity_bar=true
                in_activity_badge=false
                continue
            fi

            # activityBarBadge セクション
            if [[ "$line" =~ ^[[:space:]]{2}activityBarBadge:[[:space:]]*$ ]]; then
                in_activity_badge=true
                in_activity_bar=false
                continue
            fi

            # statusBar や titleBar セクションに入ったら activityBar を抜ける
            if [[ "$line" =~ ^[[:space:]]{2}(statusBar|titleBar):[[:space:]]*$ ]]; then
                in_activity_bar=false
                in_activity_badge=false
                continue
            fi

            # activityBar 内の設定を読み込み
            if $in_activity_bar; then
                if [[ "$line" =~ background:[[:space:]]*\"(.+)\" ]]; then
                    activity_bg="${BASH_REMATCH[1]}"
                elif [[ "$line" =~ foreground:[[:space:]]*\"(.+)\" ]] && [[ ! "$line" =~ inactiveForeground ]]; then
                    activity_fg="${BASH_REMATCH[1]}"
                elif [[ "$line" =~ inactiveForeground:[[:space:]]*\"(.+)\" ]]; then
                    activity_inactive_fg="${BASH_REMATCH[1]}"
                fi
            fi

            # activityBarBadge 内の設定を読み込み
            if $in_activity_badge; then
                if [[ "$line" =~ background:[[:space:]]*\"(.+)\" ]]; then
                    badge_bg="${BASH_REMATCH[1]}"
                elif [[ "$line" =~ foreground:[[:space:]]*\"(.+)\" ]]; then
                    badge_fg="${BASH_REMATCH[1]}"
                fi
            fi
        fi
    done < "${SCRIPT_DIR}/workspace-colors.yaml"

    # 色設定のJSON文字列を作成
    local color_settings=$(cat <<EOF
  "workbench.colorCustomizations": {
    "titleBar.activeBackground": "${title_bg}",
    "titleBar.activeForeground": "${title_fg}",
    "titleBar.inactiveBackground": "${title_inactive_bg}",
    "titleBar.inactiveForeground": "${title_inactive_fg}",
    "statusBar.background": "${status_bg}",
    "statusBar.foreground": "${status_fg}",
    "activityBar.background": "${activity_bg}",
    "activityBar.foreground": "${activity_fg}",
    "activityBar.inactiveForeground": "${activity_inactive_fg}",
    "activityBarBadge.background": "${badge_bg}",
    "activityBarBadge.foreground": "${badge_fg}"
  },
  "peacock.color": "${peacock_color}"
EOF
)

    # 既存のsettings.jsonがある場合はマージ、ない場合は新規作成
    if [ -f "$settings_file" ]; then
        # 既存ファイルから workbench.colorCustomizations と peacock.color を削除してから追加

        # jqがインストールされているか確認
        if command -v jq >/dev/null 2>&1; then
            # jqを使用してマージ
            local temp_file="${settings_file}.tmp"
            jq --argjson colors "{\"workbench.colorCustomizations\": {
                \"titleBar.activeBackground\": \"${title_bg}\",
                \"titleBar.activeForeground\": \"${title_fg}\",
                \"titleBar.inactiveBackground\": \"${title_inactive_bg}\",
                \"titleBar.inactiveForeground\": \"${title_inactive_fg}\",
                \"statusBar.background\": \"${status_bg}\",
                \"statusBar.foreground\": \"${status_fg}\",
                \"activityBar.background\": \"${activity_bg}\",
                \"activityBar.foreground\": \"${activity_fg}\",
                \"activityBar.inactiveForeground\": \"${activity_inactive_fg}\",
                \"activityBarBadge.background\": \"${badge_bg}\",
                \"activityBarBadge.foreground\": \"${badge_fg}\"
            }, \"peacock.color\": \"${peacock_color}\"}" \
            '. + $colors' "$settings_file" > "$temp_file" && mv "$temp_file" "$settings_file"
        elif command -v python >/dev/null 2>&1; then
            # Pythonを使用してマージ(JSONC対応:コメントを削除)
            python <<PYTHON_SCRIPT
import json
import re
import sys

def remove_jsonc_comments(text):
    """JSONCからコメントを削除"""
    # 単一行コメント (//) を削除
    text = re.sub(r'//.*$', '', text, flags=re.MULTILINE)
    # 複数行コメント (/* */) を削除
    text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
    return text

try:
    with open('${settings_file}', 'r', encoding='utf-8') as f:
        content = f.read()

    # コメントを削除してからパース
    clean_json = remove_jsonc_comments(content)
    settings = json.loads(clean_json)

    settings['workbench.colorCustomizations'] = {
        'titleBar.activeBackground': '${title_bg}',
        'titleBar.activeForeground': '${title_fg}',
        'titleBar.inactiveBackground': '${title_inactive_bg}',
        'titleBar.inactiveForeground': '${title_inactive_fg}',
        'statusBar.background': '${status_bg}',
        'statusBar.foreground': '${status_fg}',
        'activityBar.background': '${activity_bg}',
        'activityBar.foreground': '${activity_fg}',
        'activityBar.inactiveForeground': '${activity_inactive_fg}',
        'activityBarBadge.background': '${badge_bg}',
        'activityBarBadge.foreground': '${badge_fg}'
    }
    settings['peacock.color'] = '${peacock_color}'

    with open('${settings_file}', 'w', encoding='utf-8') as f:
        json.dump(settings, f, indent=2, ensure_ascii=False)

    sys.exit(0)
except Exception as e:
    print("Error: " + str(e), file=sys.stderr)
    sys.exit(1)
PYTHON_SCRIPT
        else
            echo -e "${YELLOW}警告: jqまたはPythonが見つかりません。色設定のみのファイルを作成します。${NC}"
            # jqもPythonもない場合は、色設定だけの新しいファイルを作成
            cat > "$settings_file" <<EOF
{
${color_settings}
}
EOF
        fi
    else
        # 新規作成
        cat > "$settings_file" <<EOF
{
${color_settings}
}
EOF
    fi

    if [ $? -eq 0 ]; then
        echo -e "${GREEN}✓ ワークスペース設定ファイルを作成しました${NC}"
        echo "  色: ${color}"
    else
        echo -e "${RED}エラー: ワークスペース設定ファイルの作成に失敗しました${NC}"
    fi
}

# .code-workspaceファイルを生成する関数
generate_code_workspace() {
    local worktree_path="$1"
    local color="$2"
    local window_title="$3"
    local workspace_file="${worktree_path}/workspace.code-workspace"

    echo -e "${YELLOW}.code-workspaceファイルを作成中...${NC}"

    # 色設定を読み込み
    local peacock_color=$(read_color_config "$color" "peacock")
    local title_bg=$(read_color_config "$color" "activeBackground")
    local title_fg=$(read_color_config "$color" "activeForeground")
    local title_inactive_bg=$(read_color_config "$color" "inactiveBackground")
    local title_inactive_fg=$(read_color_config "$color" "inactiveForeground")
    local status_bg=$(read_color_config "$color" "background")
    local status_fg=$(read_color_config "$color" "foreground")
    local activity_bg=""
    local activity_fg=""
    local activity_inactive_fg=""
    local badge_bg=""
    local badge_fg=""

    # activityBarとactivityBarBadgeの設定を個別に読み込み
    local in_activity_bar=false
    local in_activity_badge=false
    local in_color_section=false

    while IFS= read -r line; do
        # 色セクションの検出
        if [[ "$line" =~ ^[[:space:]]*${color}:[[:space:]]*$ ]]; then
            in_color_section=true
            continue
        fi

        # 他の色セクションに入ったら終了
        if $in_color_section && [[ "$line" =~ ^[[:space:]]*[a-z]+:[[:space:]]*$ ]] && [[ ! "$line" =~ ^[[:space:]]{2,} ]]; then
            break
        fi

        if $in_color_section; then
            # activityBar セクション
            if [[ "$line" =~ ^[[:space:]]{2}activityBar:[[:space:]]*$ ]]; then
                in_activity_bar=true
                in_activity_badge=false
                continue
            fi

            # activityBarBadge セクション
            if [[ "$line" =~ ^[[:space:]]{2}activityBarBadge:[[:space:]]*$ ]]; then
                in_activity_badge=true
                in_activity_bar=false
                continue
            fi

            # statusBar や titleBar セクションに入ったら activityBar を抜ける
            if [[ "$line" =~ ^[[:space:]]{2}(statusBar|titleBar):[[:space:]]*$ ]]; then
                in_activity_bar=false
                in_activity_badge=false
                continue
            fi

            # activityBar 内の設定を読み込み
            if $in_activity_bar; then
                if [[ "$line" =~ background:[[:space:]]*\"(.+)\" ]]; then
                    activity_bg="${BASH_REMATCH[1]}"
                elif [[ "$line" =~ foreground:[[:space:]]*\"(.+)\" ]] && [[ ! "$line" =~ inactiveForeground ]]; then
                    activity_fg="${BASH_REMATCH[1]}"
                elif [[ "$line" =~ inactiveForeground:[[:space:]]*\"(.+)\" ]]; then
                    activity_inactive_fg="${BASH_REMATCH[1]}"
                fi
            fi

            # activityBarBadge 内の設定を読み込み
            if $in_activity_badge; then
                if [[ "$line" =~ background:[[:space:]]*\"(.+)\" ]]; then
                    badge_bg="${BASH_REMATCH[1]}"
                elif [[ "$line" =~ foreground:[[:space:]]*\"(.+)\" ]]; then
                    badge_fg="${BASH_REMATCH[1]}"
                fi
            fi
        fi
    done < "${SCRIPT_DIR}/workspace-colors.yaml"

    # .code-workspaceファイルを作成
    cat > "$workspace_file" <<EOF
{
  "folders": [
    {
      "path": "."
    }
  ],
  "settings": {
    "window.title": "${window_title}",
    "workbench.colorCustomizations": {
      "titleBar.activeBackground": "${title_bg}",
      "titleBar.activeForeground": "${title_fg}",
      "titleBar.inactiveBackground": "${title_inactive_bg}",
      "titleBar.inactiveForeground": "${title_inactive_fg}",
      "statusBar.background": "${status_bg}",
      "statusBar.foreground": "${status_fg}",
      "activityBar.background": "${activity_bg}",
      "activityBar.foreground": "${activity_fg}",
      "activityBar.inactiveForeground": "${activity_inactive_fg}",
      "activityBarBadge.background": "${badge_bg}",
      "activityBarBadge.foreground": "${badge_fg}"
    },
    "peacock.color": "${peacock_color}"
  }
}
EOF

    if [ $? -eq 0 ]; then
        echo -e "${GREEN}✓ .code-workspaceファイルを作成しました${NC}"
        echo "  タイトル: ${window_title}"
    else
        echo -e "${RED}エラー: .code-workspaceファイルの作成に失敗しました${NC}"
    fi
}

# worktree名の検証(ディレクトリトラバーサル攻撃の防止)
# 1. '..' や絶対パスのチェック
if [[ "$WORKTREE_NAME" == *".."* ]] || [[ "$WORKTREE_NAME" == /* ]]; then
    echo -e "${RED}エラー: Worktree名に '..' や絶対パスは使用できません${NC}"
    echo "例: feature/new-work, bugfix/issue-123"
    exit 1
fi

# 2. 空の名前チェック
if [[ -z "$WORKTREE_NAME" ]]; then
    echo -e "${RED}エラー: Worktree名を指定してください${NC}"
    exit 1
fi

# 3. basenameでディレクトリトラバーサルを防止
# スラッシュを含む名前(例: feature/new-work)は許可するが、
# basename変換で変わってしまう不正なパスは拒否
# 例: "dir/../../../sensitive" は basename で "sensitive" になるので検出できる
if [[ "$WORKTREE_NAME" == *"/"* ]]; then
    # スラッシュを含む場合、最後のコンポーネントだけbasenameで検証
    LAST_COMPONENT=$(basename "$WORKTREE_NAME")
    # 最後のコンポーネントに .. や . が含まれていないか確認
    if [[ "$LAST_COMPONENT" == *".."* ]] || [[ "$LAST_COMPONENT" == "." ]]; then
        echo -e "${RED}エラー: 無効なWorktree名です${NC}"
        echo "例: feature/new-work, bugfix/issue-123"
        exit 1
    fi
else
    # スラッシュを含まない場合、basename変換で変わらないか確認
    SANITIZED_NAME=$(basename "$WORKTREE_NAME")
    if [[ "$WORKTREE_NAME" != "$SANITIZED_NAME" ]]; then
        echo -e "${RED}エラー: 無効なWorktree名です${NC}"
        echo "例: feature/new-work, bugfix/issue-123"
        exit 1
    fi
fi

# リポジトリのルートディレクトリを取得
if ! REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null); then
    echo -e "${RED}エラー: Gitリポジトリではありません${NC}"
    exit 1
fi

# worktreeの作成先ディレクトリ(リポジトリの親ディレクトリに作成)
# スラッシュをハイフンに変換してディレクトリ名として使用(階層を作らない)
WORKTREE_DIR_NAME="${WORKTREE_NAME//\//-}"
WORKTREE_PATH="${REPO_ROOT}/../${WORKTREE_DIR_NAME}"

echo -e "${YELLOW}=== Git Worktree作成スクリプト ===${NC}"
echo ""
echo "ベースブランチ: ${BASE_BRANCH}"
echo "Worktree名: ${WORKTREE_NAME}"
echo "ディレクトリ名: ${WORKTREE_DIR_NAME}"
echo "作成先: ${WORKTREE_PATH}"
echo ""

# ベースブランチの存在確認
echo -e "${YELLOW}ベースブランチの確認中...${NC}"
if ! git show-ref --verify --quiet "refs/heads/${NORMALIZED_LOCAL_BRANCH}" && \
   ! git show-ref --verify --quiet "refs/remotes/${NORMALIZED_REMOTE_BRANCH}"; then
    echo -e "${RED}エラー: ブランチ '${ORIGINAL_BASE_BRANCH}' が見つかりません${NC}"
    echo "利用可能なブランチ:"
    git branch -a
    exit 1
fi

# ローカルブランチが存在しない場合、リモートから取得(正規化後の名称を使用)
if ! git show-ref --verify --quiet "refs/heads/${NORMALIZED_LOCAL_BRANCH}"; then
    if git show-ref --verify --quiet "refs/remotes/${NORMALIZED_REMOTE_BRANCH}"; then
        echo -e "${YELLOW}リモートブランチからローカルブランチを作成します...${NC}"
        git branch "${NORMALIZED_LOCAL_BRANCH}" "${NORMALIZED_REMOTE_BRANCH}"
    fi
fi

# -bフラグが指定された場合の処理
if [ "${CREATE_NEW_BRANCH_FLAG}" = true ]; then
    echo -e "${YELLOW}新規ブランチを作成します...${NC}"

    # 新しいブランチ名はworktree名をそのまま使用
    NEW_BRANCH="${WORKTREE_NAME}"

    # 新しいブランチが既に存在するかチェック
    if git show-ref --verify --quiet "refs/heads/${NEW_BRANCH}"; then
        echo -e "${RED}エラー: ブランチ '${NEW_BRANCH}' は既に存在します${NC}"
        echo "別のworktree名を使用するか、既存のブランチを削除してください:"
        echo "  git branch -D ${NEW_BRANCH}"
        exit 1
    fi

    # worktreeディレクトリ名はスラッシュをハイフンに変換
    WORKTREE_DIR_NAME_FOR_NEW="${WORKTREE_NAME//\//-}"
    WORKTREE_PATH="${REPO_ROOT}/../${WORKTREE_DIR_NAME_FOR_NEW}"

    BRANCH_TO_USE="${NEW_BRANCH}"
    CREATE_NEW_BRANCH=true
else
    # -bフラグなしの場合、既存ブランチをチェックアウト
    # ベースブランチが既に別のworktreeでチェックアウトされているかチェック
    echo -e "${YELLOW}既存のworktreeを確認中...${NC}"

    # より確実な方法: git worktree listの通常出力を使用
    # 形式: "path commit [branch]" の3列目にブランチ名がある
    EXISTING_WORKTREE=$(git worktree list | grep "\[${NORMALIZED_LOCAL_BRANCH}\]" | awk '{print $1}')

    if [ -n "${EXISTING_WORKTREE}" ]; then
        echo -e "${YELLOW}警告: ブランチ '${BASE_BRANCH}' は既に以下の場所でチェックアウトされています:${NC}"
        echo "  ${EXISTING_WORKTREE}"
        echo ""
        echo -e "${YELLOW}新しいブランチを作成してworktreeを作成します。${NC}"

        # 新しいブランチ名を生成(worktreeディレクトリ名と同じにする)
        NEW_BRANCH="${WORKTREE_DIR_NAME}"

        # 新しいブランチが既に存在するかチェック
        if git show-ref --verify --quiet "refs/heads/${NEW_BRANCH}"; then
            echo -e "${RED}エラー: ブランチ '${NEW_BRANCH}' は既に存在します${NC}"
            echo "別のworktree名を使用するか、既存のブランチを削除してください:"
            echo "  git branch -D ${NEW_BRANCH}"
            exit 1
        fi

        BRANCH_TO_USE="${NEW_BRANCH}"
        CREATE_NEW_BRANCH=true
    else
        BRANCH_TO_USE="${NORMALIZED_LOCAL_BRANCH}"
        CREATE_NEW_BRANCH=false
    fi
fi

# worktreeが既に存在するかチェック
if [ -d "${WORKTREE_PATH}" ]; then
    echo -e "${RED}エラー: ディレクトリ '${WORKTREE_PATH}' は既に存在します${NC}"
    exit 1
fi

# worktreeを作成
echo -e "${YELLOW}Worktreeを作成中...${NC}"
if [ "${CREATE_NEW_BRANCH}" = true ]; then
    echo "新しいブランチ '${BRANCH_TO_USE}' を '${NORMALIZED_LOCAL_BRANCH}' から作成します..."
    if git worktree add -b "${BRANCH_TO_USE}" "${WORKTREE_PATH}" "${NORMALIZED_LOCAL_BRANCH}"; then
        echo -e "${GREEN}✓ Worktreeを作成しました: ${WORKTREE_PATH}${NC}"
        echo -e "${GREEN}✓ 新しいブランチ '${BRANCH_TO_USE}' を作成しました${NC}"
    else
        echo -e "${RED}エラー: Worktreeの作成に失敗しました${NC}"
        exit 1
    fi
else
    if git worktree add "${WORKTREE_PATH}" "${BRANCH_TO_USE}"; then
        echo -e "${GREEN}✓ Worktreeを作成しました: ${WORKTREE_PATH}${NC}"
    else
        echo -e "${RED}エラー: Worktreeの作成に失敗しました${NC}"
        exit 1
    fi
fi

# ワークスペース設定ファイルを生成
generate_workspace_settings "${WORKTREE_PATH}" "${SELECTED_COLOR}"

# ウィンドウタイトルが指定されている場合、.code-workspaceファイルも生成
if [ -n "${WINDOW_TITLE}" ]; then
    generate_code_workspace "${WORKTREE_PATH}" "${SELECTED_COLOR}" "${WINDOW_TITLE}"
fi

# VS Codeを開く
echo -e "${YELLOW}VS Codeを開いています...${NC}"
if command -v code >/dev/null 2>&1; then
    # .code-workspaceファイルがある場合はそれを開く、なければフォルダを開く
    if [ -n "${WINDOW_TITLE}" ] && [ -f "${WORKTREE_PATH}/workspace.code-workspace" ]; then
        code -n "${WORKTREE_PATH}/workspace.code-workspace"
        echo -e "${GREEN}✓ VS Code (.code-workspace)を開きました${NC}"
    else
        # フォルダを新しいウィンドウで開く(色設定が適用される)
        code -n "${WORKTREE_PATH}"
        echo -e "${GREEN}✓ VS Codeを開きました${NC}"
    fi
else
    echo -e "${YELLOW}警告: 'code' コマンドが見つかりません${NC}"
    echo "VS Codeを手動で開いてください: ${WORKTREE_PATH}"
fi

echo ""
echo -e "${GREEN}=== 完了 ===${NC}"
echo "Worktreeパス: ${WORKTREE_PATH}"
if [ "${CREATE_NEW_BRANCH}" = true ]; then
    echo "ブランチ: ${BRANCH_TO_USE} (新規作成)"
    echo "ベースブランチ: ${BASE_BRANCH}"
else
    echo "ブランチ: ${BRANCH_TO_USE}"
fi
echo ""
echo "Worktreeを削除する場合は以下のコマンドを実行してください:"
echo "  git worktree remove ${WORKTREE_PATH}"
if [ "${CREATE_NEW_BRANCH}" = true ]; then
    echo ""
    echo "ブランチも削除する場合は以下のコマンドも実行してください:"
    echo "  git branch -D ${BRANCH_TO_USE}"
fi

VSCode Tasks から実行できるようにする

スクリプトは tasks.json に登録し、
VSCode のコマンドパレットから実行できるようにしました。

  • Ctrl + Shift + P で起動
  • プロンプト形式で必要な情報を入力
  • 実行すると新しい Worktree が立ち上がる

プロンプトにはデフォルト値や前回値の保持も使えるため、
入力の手間はそれほど大きくありません。

tasks.json の設定例(クリックで展開)
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Git Worktree 作成",
      "type": "shell",
      "command": "bash",
      "args": [
        "./scripts/create-worktree.sh",
        "${input:baseBranch}",
        "${input:worktreeName}",
        "${input:createNewBranch}",
        "${input:workspaceColor}",
        "${input:windowTitle}"
      ],
      "options": {
        "cwd": "${workspaceFolder}"
      },
      "group": {
        "kind": "none",
        "isDefault": false
      }
    }
  ],
  "inputs": [
    {
      "id": "baseBranch",
      "type": "promptString",
      "description": "ベースブランチ名(例: main, base/feature-name)",
      "default": "main"
    },
    {
      "id": "worktreeName",
      "type": "promptString",
      "description": "Worktree名(例: issue/123-description)"
    },
    {
      "id": "createNewBranch",
      "type": "pickString",
      "description": "新規ブランチを作成しますか?",
      "options": ["y", "n"],
      "default": "y"
    },
    {
      "id": "workspaceColor",
      "type": "pickString",
      "description": "ワークスペースの色",
      "options": ["", "blue", "green", "purple", "cyan", "orange"],
      "default": ""
    },
    {
      "id": "windowTitle",
      "type": "promptString",
      "description": "ウィンドウタイトル(オプション)",
      "default": ""
    }
  ]
}

Copilot カスタムプロンプトとの組み合わせ

それでも使っているうちに、

「入力自体が少し面倒」

と感じるようになりました。

そこで GitHub Copilot の カスタムプロンプトを用意しました。

  • Issue 番号を渡す
  • Copilot が Issue 内容を確認(GitHub Pull Requests 拡張機能を使用)
  • ブランチ名やタイトルを提案
  • 実行する bash コマンドを生成

実行提案の内容を確認し、
問題なければ 承認するだけで処理が進みます。

必要に応じて修正してから承認できるため、
安心して使える点も気に入っています。

Copilot カスタムプロンプトの例(クリックで展開)
---
mode: "agent"
description: "Issue番号からワークツリーとブランチを自動作成"
model: claude-haiku-4.5
---

あなたは Claude Haiku 4.5 ベースの AI アシスタントで Git Worktree とブランチ作成の専門家です。
Issue 番号を受け取り、以下の手順を実行してください:

## 入力

Issue 番号: ${input:issueNumber:Issue 番号を入力してください(例: 45)}
ベースブランチ: ${input:baseBranch:ベースブランチを入力してください(デフォルト: main)|default:main}

## 実行手順

1. **Issue 内容の確認**

  - `github-pull-request_issue_fetch` ツールを使用して、指定された Issue 番号の内容を取得
  - リポジトリは `YourOrg/your-repo` を使用(ご自身のリポジトリに置き換えてください)
  - タイトルと本文から作業内容を理解する

2. **命名の考案**
  以下のルールに従って名前を考案してください:

  - **ブランチ名**: `issue/{issue番号}-{content-to-represent-in-english}`

    - 例: `issue/45-sensor-integration`
    - 英語で、ハイフン区切り、簡潔でわかりやすく

  - **ウィンドウタイトル**: `Issue #{issue番号} - {日本語の作業内容}`

    - 例:
      - 要件定義: `Issue #45 - センサー統合機能の要件定義`
      - 開発: `Issue #45 - センサー統合の開発`
      - バグ修正: `Issue #78 - データ読み込みエラーの修正`
    - 日本語で、作業内容が明確にわかるように

  - **色の選択**:
    - レビュー系: `purple`
    - テスト系: `cyan`
    - 実験系: `orange`
    - 設計・要件定義: `blue`
    - その他の開発: `green`

3. **コマンドの実行**
  - 以下の情報をユーザーに通知する(確認は不要):
    - **ベースブランチ**: `{ベースブランチ}`
    - **新規ブランチ名**: `{ブランチ名}`
    - **ウィンドウタイトル**: `{ウィンドウタイトル}`
    - **色**: `{色}`
  - `run_in_terminal` ツールで直接実行する:
  ```bash
  bash ./scripts/create-worktree.sh {ベースブランチ} {ブランチ名} y {色} "{ウィンドウタイトル}"
  ```
  - 例:
  ```bash
  bash ./scripts/create-worktree.sh main issue/45-sensor-integration y blue "Issue #45 - センサー統合機能の設計"
  ```
  - 実行結果(ワークツリーのパス、ブランチ名、VS Code の起動)を確認

## 重要な注意事項

- Issue 内容が取得できない場合は、エラーメッセージをユーザーに伝えること
- ブランチ名は既存のブランチと重複しないこと
- コマンド実行前に、実行内容を簡潔にユーザーに伝えること

## 成功基準

✅ Issue 内容を正しく取得できた
✅ 適切なブランチ名とウィンドウタイトルを考案できた
✅ コマンドが正常に実行された
✅ ワークツリーとブランチが作成され、VS Code が新しいウィンドウで開いた


補足:この仕組みは未完成でいい

ここで紹介したスクリプトやプロンプトは、
人によっては 不完全に感じると思います。

  • 個人リポジトリ向けの例
  • 業務では要件がもっと複雑になることもある

例えば、

  • PR 番号からレビュー用 Worktree を起動したい
  • yarn installyarn dev を実行してから開きたい

といったケースもあると思います。

こうした部分は、
Copilot などの AI を使って少しずつ育てていくことで、
自分の業務に合った形にしていけると感じています。


まとめ

Git Worktree 自体は以前からある機能ですが、
そのまま使うよりも、

  • スクリプト化
  • VSCode との統合
  • Copilot との組み合わせ

によって、ようやく業務に定着しました。

同じようなワークフローを繰り返している方の
何かしらのヒントになれば幸いです。

Discussion