diff --git a/plugins/compound-engineering/skills/ce-worktree/SKILL.md b/plugins/compound-engineering/skills/ce-worktree/SKILL.md index 877452b..1759bd7 100644 --- a/plugins/compound-engineering/skills/ce-worktree/SKILL.md +++ b/plugins/compound-engineering/skills/ce-worktree/SKILL.md @@ -1,310 +1,77 @@ --- name: ce-worktree -description: This skill manages Git worktrees for isolated parallel development. It handles creating, listing, switching, and cleaning up worktrees with a simple interactive interface, following KISS principles. +description: Create an isolated git worktree for parallel feature work or PR review. Use when starting work that should not disturb the current checkout, or when `ce-work` or `ce-code-review` offers a worktree option. --- -# Git Worktree Manager +# Worktree Creation -This skill provides a unified interface for managing Git worktrees across your development workflow. Whether you're reviewing PRs in isolation or working on features in parallel, this skill handles all the complexity. +Create a worktree under `.worktrees/` with branch-specific setup that `git worktree add` alone does not handle: -## What This Skill Does +- Copies `.env`, `.env.local`, `.env.test`, etc. from the main repo (skips `.env.example`) +- Trusts `mise`/`direnv` configs, with branch-aware safety rules so review branches do not auto-grant trust to untrusted `.envrc` content +- Adds `.worktrees` to `.gitignore` if not already ignored +- Does not modify the main repo checkout — `from-branch` is fetched, not checked out -- **Create worktrees** from main branch with clear branch names -- **List worktrees** with current status -- **Switch between worktrees** for parallel work -- **Clean up completed worktrees** automatically -- **Interactive confirmations** at each step -- **Automatic .gitignore management** for worktree directory -- **Automatic .env file copying** from main repo to new worktrees -- **Automatic dev tool trusting** for mise and direnv configs with review-safe guardrails - -## CRITICAL: Always Use the Manager Script - -**NEVER call `git worktree add` directly.** Always use the `worktree-manager.sh` script. - -The script handles critical setup that raw git commands don't: -1. Copies `.env`, `.env.local`, `.env.test`, etc. from main repo -2. Trusts dev tool configs with branch-aware safety rules: - - mise: auto-trust only when unchanged from a trusted baseline branch - - direnv: auto-allow only for trusted base branches; review worktrees stay manual -3. Ensures `.worktrees` is in `.gitignore` -4. Creates consistent directory structure +## Creating a worktree ```bash -# ✅ CORRECT - Always use the script -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh create feature-name - -# ❌ WRONG - Never do this directly -git worktree add .worktrees/feature-name -b feature-name main +bash scripts/worktree-manager.sh create [from-branch] ``` -## When to Use This Skill +Defaults: +- `from-branch` defaults to origin's default branch (or `main` if that cannot be resolved) +- The new branch is created at `origin/` (or the local ref if the remote is unavailable) -Use this skill in these scenarios: - -1. **Code Review (`/ce-code-review`)**: If NOT already on the target branch (PR branch or requested branch), offer worktree for isolated review -2. **Feature Work (`/ce-work`)**: Always ask if user wants parallel worktree or live branch work -3. **Parallel Development**: When working on multiple features simultaneously -4. **Cleanup**: After completing work in a worktree - -## How to Use - -### In Claude Code Workflows - -The skill is automatically called from `/ce-code-review` and `/ce-work` commands: - -``` -# For review: offers worktree if not on PR branch -# For work: always asks - new branch or worktree? +Examples: +```bash +bash scripts/worktree-manager.sh create feat/login +bash scripts/worktree-manager.sh create fix/email-validation develop ``` -### Manual Usage +After creation, switch to the worktree with `cd .worktrees/`. -You can also invoke the skill directly from bash: +## Other worktree operations + +Use `git` directly — no wrapper is needed and none is provided: ```bash -# Create a new worktree (copies .env files automatically) -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh create feature-login - -# List all worktrees -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh list - -# Switch to a worktree -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh switch feature-login - -# Copy .env files to an existing worktree (if they weren't copied) -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh copy-env feature-login - -# Clean up completed worktrees -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh cleanup +git worktree list # list worktrees +git worktree remove .worktrees/ # remove a worktree +cd .worktrees/ # switch to a worktree +cd "$(git rev-parse --show-toplevel)" # return to main checkout ``` -## Commands - -### `create [from-branch]` - -Creates a new worktree with the given branch name. - -**Options:** -- `branch-name` (required): The name for the new branch and worktree -- `from-branch` (optional): Base branch to create from (defaults to `main`) - -**Example:** +To copy `.env*` files into an existing worktree created without them, run this from the main repo (not from inside the worktree, since branch names often contain slashes like `feat/login`): ```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh create feature-login +cp .env* .worktrees// ``` -**What happens:** -1. Checks if worktree already exists -2. Updates the base branch from remote -3. Creates new worktree and branch -4. **Copies all .env files from main repo** (.env, .env.local, .env.test, etc.) -5. **Trusts dev tool configs** with branch-aware safety rules: - - trusted bases (`main`, `develop`, `dev`, `trunk`, `staging`, `release/*`) compare against themselves - - other branches compare against the default branch - - direnv auto-allow is skipped on non-trusted bases because `.envrc` can source unchecked files -6. Shows path for cd-ing to the worktree +## Dev tool trust behavior -### `list` or `ls` +When mise or direnv configs are present, the script attempts to trust them so hooks and scripts do not block on interactive prompts. Trust is baseline-checked against a reference branch: -Lists all available worktrees with their branches and current status. +- **Trusted base branches** (`main`, `develop`, `dev`, `trunk`, `staging`, `release/*`): the new worktree's configs are compared against that branch; unchanged configs are auto-trusted. `direnv allow` is permitted. +- **Other branches** (feature branches, PR review branches): configs are compared against the default branch; `direnv allow` is skipped regardless, because `.envrc` can source files that direnv does not validate. -**Example:** -```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh list -``` +Modified configs are never auto-trusted. The script prints the manual trust command to run after review. -**Output shows:** -- Worktree name -- Branch name -- Which is current (marked with ✓) -- Main repo status +## When to create a worktree -### `switch ` or `go ` +Create a worktree when: +- Reviewing a PR while keeping the main checkout free for other work +- Running multiple features in parallel without branch-switching overhead +- Keeping the default branch free of in-progress state -Switches to an existing worktree and cd's into it. +Do not create a worktree for single-task work that can happen on a branch in the main checkout. -**Example:** -```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh switch feature-login -``` +## Integration -**Optional:** -- If name not provided, lists available worktrees and prompts for selection - -### `cleanup` or `clean` - -Interactively cleans up inactive worktrees with confirmation. - -**Example:** -```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh cleanup -``` - -**What happens:** -1. Lists all inactive worktrees -2. Asks for confirmation -3. Removes selected worktrees -4. Cleans up empty directories - -## Workflow Examples - -### Code Review with Worktree - -```bash -# Claude Code recognizes you're not on the PR branch -# Offers: "Use worktree for isolated review? (y/n)" - -# You respond: yes -# Script runs (copies .env files automatically): -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh create pr-123-feature-name - -# You're now in isolated worktree for review with all env vars -cd .worktrees/pr-123-feature-name - -# After review, return to main: -cd ../.. -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh cleanup -``` - -### Parallel Feature Development - -```bash -# For first feature (copies .env files): -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh create feature-login - -# Later, start second feature (also copies .env files): -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh create feature-notifications - -# List what you have: -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh list - -# Switch between them as needed: -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh switch feature-login - -# Return to main and cleanup when done: -cd . -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh cleanup -``` - -## Key Design Principles - -### KISS (Keep It Simple, Stupid) - -- **One manager script** handles all worktree operations -- **Simple commands** with sensible defaults -- **Interactive prompts** prevent accidental operations -- **Clear naming** using branch names directly - -### Opinionated Defaults - -- Worktrees always created from **main** (unless specified) -- Worktrees stored in **.worktrees/** directory -- Branch name becomes worktree name -- **.gitignore** automatically managed - -### Safety First - -- **Confirms before creating** worktrees -- **Confirms before cleanup** to prevent accidental removal -- **Won't remove current worktree** -- **Clear error messages** for issues - -## Integration with Workflows - -### `/ce-code-review` - -Instead of always creating a worktree: - -``` -1. Check current branch -2. If ALREADY on target branch (PR branch or requested branch) → stay there, no worktree needed -3. If DIFFERENT branch than the review target → offer worktree: - "Use worktree for isolated review? (y/n)" - - yes → call ce-worktree skill - - no → proceed with PR diff on current branch -``` - -### `/ce-work` - -Always offer choice: - -``` -1. Ask: "How do you want to work? - 1. New branch on current worktree (live work) - 2. Worktree (parallel work)" - -2. If choice 1 → create new branch normally -3. If choice 2 → call ce-worktree skill to create from main -``` +`ce-work` and `ce-code-review` offer this skill as an option. When the user selects "worktree" in those flows, invoke `bash scripts/worktree-manager.sh create ` with a meaningful branch name derived from the work description (e.g., `feat/crowd-sniff`, `fix/email-validation`). Avoid auto-generated names like `worktree-jolly-beaming-raven` that obscure the work. ## Troubleshooting -### "Worktree already exists" +**"Worktree already exists"**: the path is already in use. Either switch to it (`cd .worktrees/`) or remove it (`git worktree remove .worktrees/`) before recreating. -If you see this, the script will ask if you want to switch to it instead. +**"Cannot remove worktree: it is the current worktree"**: `cd` out of the worktree first, then `git worktree remove`. -### "Cannot remove worktree: it is the current worktree" - -Switch out of the worktree first (to main repo), then cleanup: - -```bash -cd $(git rev-parse --show-toplevel) -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh cleanup -``` - -### Lost in a worktree? - -See where you are: - -```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh list -``` - -### .env files missing in worktree? - -If a worktree was created without .env files (e.g., via raw `git worktree add`), copy them: - -```bash -bash ${CLAUDE_PLUGIN_ROOT}/skills/ce-worktree/scripts/worktree-manager.sh copy-env feature-name -``` - -Navigate back to main: - -```bash -cd $(git rev-parse --show-toplevel) -``` - -## Technical Details - -### Directory Structure - -``` -.worktrees/ -├── feature-login/ # Worktree 1 -│ ├── .git -│ ├── app/ -│ └── ... -├── feature-notifications/ # Worktree 2 -│ ├── .git -│ ├── app/ -│ └── ... -└── ... - -.gitignore (updated to include .worktrees) -``` - -### How It Works - -- Uses `git worktree add` for isolated environments -- Each worktree has its own branch -- Changes in one worktree don't affect others -- Share git history with main repo -- Can push from any worktree - -### Performance - -- Worktrees are lightweight (just file system links) -- No repository duplication -- Shared git objects for efficiency -- Much faster than cloning or stashing/switching +**Dev tool trust was skipped**: the script prints the manual command. Review the config diff (`git diff -- .envrc`), then run the printed command from the worktree directory. diff --git a/plugins/compound-engineering/skills/ce-worktree/scripts/worktree-manager.sh b/plugins/compound-engineering/skills/ce-worktree/scripts/worktree-manager.sh index 3a05944..98f3f91 100755 --- a/plugins/compound-engineering/skills/ce-worktree/scripts/worktree-manager.sh +++ b/plugins/compound-engineering/skills/ce-worktree/scripts/worktree-manager.sh @@ -1,76 +1,89 @@ #!/bin/bash +# +# Create a new git worktree with environment files and dev-tool trust. +# +# The distinctive work this script does (vs. raw `git worktree add`): +# 1. Copies .env* files from the main repo (skipping .env.example) +# 2. Trusts mise/direnv configs with branch-aware safety rules, +# so hooks and scripts don't block on interactive trust prompts +# 3. Ensures .worktrees is gitignored (via `git check-ignore`) +# +# List / remove / switch operations are NOT provided here. Use git directly: +# git worktree list +# git worktree remove +# cd # switching is just `cd` -# Git Worktree Manager -# Handles creating, listing, switching, and cleaning up Git worktrees -# KISS principle: Simple, interactive, opinionated +set -euo pipefail -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Get repo root -GIT_ROOT=$(git rev-parse --show-toplevel) +# Resolve the main worktree's working tree, not the current worktree's toplevel. +# `git worktree list --porcelain` always emits the main worktree first. This +# handles normal repos, linked worktrees (where --show-toplevel would return +# the nested worktree), submodules (where --git-common-dir points under +# .git/modules), and --separate-git-dir setups (where --git-common-dir points +# to an external path). Parse with `sed` to preserve paths containing spaces +# (awk '{print $2}' would truncate them). +GIT_ROOT=$(git worktree list --porcelain | sed -n 's/^worktree //p' | head -n 1) WORKTREE_DIR="$GIT_ROOT/.worktrees" -# Ensure .worktrees is in .gitignore -ensure_gitignore() { - if ! grep -q "^\.worktrees$" "$GIT_ROOT/.gitignore" 2>/dev/null; then - echo ".worktrees" >> "$GIT_ROOT/.gitignore" - fi +usage() { + cat <<'EOF' +Usage: worktree-manager.sh create [from-branch] + +Creates .worktrees/ with branched from +[from-branch] (default: origin's default branch, or main). + +The main repo checkout is not modified; from-branch is fetched but +not checked out. +EOF } -# Copy .env files from main repo to worktree -copy_env_files() { - local worktree_path="$1" - - echo -e "${BLUE}Copying environment files...${NC}" - - # Find all .env* files in root (excluding .env.example which should be in git) - local env_files=() - for f in "$GIT_ROOT"/.env*; do - if [[ -f "$f" ]]; then - local basename=$(basename "$f") - # Skip .env.example (that's typically committed to git) - if [[ "$basename" != ".env.example" ]]; then - env_files+=("$basename") - fi - fi - done - - if [[ ${#env_files[@]} -eq 0 ]]; then - echo -e " ${YELLOW}ℹ️ No .env files found in main repository${NC}" +# Ensure .worktrees is ignored in the main repo. Runs `git check-ignore` from +# the main repo root so it sees the main repo's .gitignore (which is not +# inherited by linked worktrees). Falls back to a grep guard to avoid +# duplicate entries when check-ignore misses an uncommitted gitignore rule. +ensure_gitignore() { + if (cd "$GIT_ROOT" && git check-ignore -q .worktrees) 2>/dev/null; then return fi - - local copied=0 - for env_file in "${env_files[@]}"; do - local source="$GIT_ROOT/$env_file" - local dest="$worktree_path/$env_file" - - if [[ -f "$dest" ]]; then - echo -e " ${YELLOW}⚠️ $env_file already exists, backing up to ${env_file}.backup${NC}" - cp "$dest" "${dest}.backup" - fi - - cp "$source" "$dest" - echo -e " ${GREEN}✓ Copied $env_file${NC}" - copied=$((copied + 1)) - done - - echo -e " ${GREEN}✓ Copied $copied environment file(s)${NC}" + if grep -Fxq ".worktrees" "$GIT_ROOT/.gitignore" 2>/dev/null; then + return + fi + echo ".worktrees" >> "$GIT_ROOT/.gitignore" + echo "Added .worktrees to .gitignore" +} + +# Copy .env* files (except .env.example) from main repo to worktree. +# Backs up any pre-existing destination file. +copy_env_files() { + local worktree_path="$1" + local copied=0 + + shopt -s nullglob + for source in "$GIT_ROOT"/.env*; do + [[ -f "$source" ]] || continue + local name + name=$(basename "$source") + [[ "$name" == ".env.example" ]] && continue + + local dest="$worktree_path/$name" + if [[ -f "$dest" ]]; then + cp "$dest" "${dest}.backup" + echo " Backed up existing $name to ${name}.backup" + fi + cp "$source" "$dest" + echo " Copied $name" + copied=$((copied + 1)) + done + shopt -u nullglob + + if [[ $copied -eq 0 ]]; then + echo " No .env files in main repo" + fi } -# Resolve the repository default branch, falling back to main when origin/HEAD -# is unavailable (for example in single-branch clones). get_default_branch() { local head_ref head_ref=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || true) - if [[ -n "$head_ref" ]]; then echo "${head_ref#refs/remotes/origin/}" else @@ -78,423 +91,149 @@ get_default_branch() { fi } -# Auto-trust is only safe when the worktree is created from a long-lived branch -# the developer already controls. Review/PR branches should fall back to the -# default branch baseline and require manual direnv approval. +# Auto-trust is only safe when the worktree is based on a long-lived branch +# the developer already controls. Review/PR branches fall back to the default +# branch baseline and require manual direnv approval. is_trusted_base_branch() { local branch="$1" local default_branch="$2" - [[ "$branch" == "$default_branch" ]] && return 0 - case "$branch" in - develop|dev|trunk|staging|release/*) - return 0 - ;; - *) - return 1 - ;; + develop|dev|trunk|staging|release/*) return 0 ;; + *) return 1 ;; esac } -# Trust development tool configs in a new worktree. -# Worktrees get a new filesystem path that tools like mise and direnv -# have never seen. Without trusting, these tools block with interactive -# prompts or refuse to load configs, which breaks hooks and scripts. -# -# Safety: auto-trusts only configs unchanged from a trusted baseline branch. -# Review/PR branches fall back to the default-branch baseline, and direnv -# auto-allow is limited to trusted base branches because .envrc can source -# additional files that direnv does not validate. -# -# TOCTOU between hash-check and trust is acceptable for local dev use. -trust_dev_tools() { - local worktree_path="$1" - local base_ref="$2" - local allow_direnv_auto="$3" - local trusted=0 - local skipped_messages=() - local manual_commands=() - - # mise: trust the specific config file if present and unchanged - if command -v mise &>/dev/null; then - for f in .mise.toml mise.toml .tool-versions; do - if [[ -f "$worktree_path/$f" ]]; then - if _config_unchanged "$f" "$base_ref" "$worktree_path"; then - if (cd "$worktree_path" && mise trust "$f" --quiet); then - trusted=$((trusted + 1)) - else - echo -e " ${YELLOW}Warning: 'mise trust $f' failed -- run manually in $worktree_path${NC}" - fi - else - skipped_messages+=("mise trust $f (config differs from $base_ref)") - manual_commands+=("mise trust $f") - fi - break - fi - done - fi - - # direnv: allow .envrc - if command -v direnv &>/dev/null; then - if [[ -f "$worktree_path/.envrc" ]]; then - if [[ "$allow_direnv_auto" != "true" ]]; then - skipped_messages+=("direnv allow (.envrc auto-allow is disabled for non-trusted base branches)") - manual_commands+=("direnv allow") - elif _config_unchanged ".envrc" "$base_ref" "$worktree_path"; then - if (cd "$worktree_path" && direnv allow); then - trusted=$((trusted + 1)) - else - echo -e " ${YELLOW}Warning: 'direnv allow' failed -- run manually in $worktree_path${NC}" - fi - else - skipped_messages+=("direnv allow (.envrc differs from $base_ref)") - manual_commands+=("direnv allow") - fi - fi - fi - - if [[ $trusted -gt 0 ]]; then - echo -e " ${GREEN}✓ Trusted $trusted dev tool config(s)${NC}" - fi - - if [[ ${#skipped_messages[@]} -gt 0 ]]; then - echo -e " ${YELLOW}Skipped auto-trust for config(s) requiring manual review:${NC}" - for item in "${skipped_messages[@]}"; do - echo -e " - $item" - done - if [[ ${#manual_commands[@]} -gt 0 ]]; then - local joined - joined=$(printf ' && %s' "${manual_commands[@]}") - echo -e " ${BLUE}Review the diff, then run manually: cd $worktree_path${joined}${NC}" - fi - fi -} - -# Check if a config file is unchanged from the base branch. -# Returns 0 (true) if the file is identical to the base branch version. -# Returns 1 (false) if the file was added or modified by this branch. -# -# Note: rev-parse returns the stored blob hash; hash-object on a path applies -# gitattributes filters. A mismatch causes a false negative (trust skipped), -# which is the safe direction. -_config_unchanged() { - local file="$1" - local base_ref="$2" - local worktree_path="$3" - - # Reject symlinks -- trust only regular files with verifiable content +# Return 0 if worktree's copy of $file has the same blob hash as $base_ref's. +# Symlinks are rejected (can't verify content). +config_unchanged() { + local file="$1" base_ref="$2" worktree_path="$3" [[ -L "$worktree_path/$file" ]] && return 1 - - # Get the blob hash directly from git's object database via rev-parse - local base_hash + local base_hash worktree_hash base_hash=$(git rev-parse "$base_ref:$file" 2>/dev/null) || return 1 - - local worktree_hash worktree_hash=$(git hash-object "$worktree_path/$file") || return 1 - [[ "$base_hash" == "$worktree_hash" ]] } -# Create a new worktree +# Trust dev tool configs (mise, direnv) so hooks/scripts don't block on +# interactive trust prompts. Auto-trusts only when the config matches the +# trusted baseline branch. +trust_dev_tools() { + local worktree_path="$1" base_ref="$2" allow_direnv_auto="$3" + local trusted=0 + local manual=() + + if command -v mise &>/dev/null; then + for f in .mise.toml mise.toml .tool-versions; do + [[ -f "$worktree_path/$f" ]] || continue + if config_unchanged "$f" "$base_ref" "$worktree_path" \ + && (cd "$worktree_path" && mise trust "$f" --quiet); then + trusted=$((trusted + 1)) + else + manual+=("mise trust $f") + fi + break + done + fi + + if command -v direnv &>/dev/null && [[ -f "$worktree_path/.envrc" ]]; then + if [[ "$allow_direnv_auto" == "true" ]] \ + && config_unchanged ".envrc" "$base_ref" "$worktree_path" \ + && (cd "$worktree_path" && direnv allow); then + trusted=$((trusted + 1)) + else + manual+=("direnv allow") + fi + fi + + [[ $trusted -gt 0 ]] && echo " Trusted $trusted dev tool config(s)" + if [[ ${#manual[@]} -gt 0 ]]; then + echo " Manual review required for: ${manual[*]}" + echo " Review the diff, then run from $worktree_path" + fi +} + create_worktree() { - local branch_name="$1" - local from_branch="${2:-main}" + local branch_name="${1:-}" + local from_branch="${2:-}" if [[ -z "$branch_name" ]]; then - echo -e "${RED}Error: Branch name required${NC}" + echo "Error: branch name required" >&2 + usage >&2 exit 1 fi - local worktree_path="$WORKTREE_DIR/$branch_name" + local default_branch + default_branch=$(get_default_branch) + from_branch="${from_branch:-$default_branch}" - # Check if worktree already exists + local worktree_path="$WORKTREE_DIR/$branch_name" if [[ -d "$worktree_path" ]]; then - echo -e "${YELLOW}Worktree already exists at: $worktree_path${NC}" - echo -e "Switch to it instead? (y/n)" - read -r response - if [[ "$response" == "y" ]]; then - switch_worktree "$branch_name" - fi - return + echo "Error: worktree already exists at $worktree_path" >&2 + echo "Use 'cd $worktree_path' to switch, or 'git worktree remove' first." >&2 + exit 1 fi - echo -e "${BLUE}Creating worktree: $branch_name${NC}" - echo " From: $from_branch" - echo " Path: $worktree_path" + echo "Creating worktree $branch_name from $from_branch" - # Update main branch - echo -e "${BLUE}Updating $from_branch...${NC}" - git checkout "$from_branch" - git pull origin "$from_branch" || true - - # Create worktree mkdir -p "$WORKTREE_DIR" ensure_gitignore - echo -e "${BLUE}Creating worktree...${NC}" - git worktree add -b "$branch_name" "$worktree_path" "$from_branch" + # Fetch from-branch without touching the main checkout. + if ! git fetch origin "$from_branch" --quiet; then + echo "Warning: could not fetch origin/$from_branch; using local ref" >&2 + fi - # Copy environment files + # Prefer origin/ if available, else fall back to local ref. + local base_ref="origin/$from_branch" + if ! git rev-parse --verify "$base_ref" &>/dev/null; then + base_ref="$from_branch" + fi + + git worktree add -b "$branch_name" "$worktree_path" "$base_ref" + + echo "Environment files:" copy_env_files "$worktree_path" - # Trust dev tool configs (mise, direnv) so hooks and scripts work immediately. - # Long-lived integration branches can use themselves as the trust baseline, - # while review/PR branches fall back to the default branch and require manual - # direnv approval. - local default_branch - default_branch=$(get_default_branch) + echo "Dev tool trust:" local trust_branch="$default_branch" local allow_direnv_auto="false" if is_trusted_base_branch "$from_branch" "$default_branch"; then trust_branch="$from_branch" allow_direnv_auto="true" fi - - if ! git fetch origin "$trust_branch" --quiet; then - echo -e " ${YELLOW}Warning: could not fetch origin/$trust_branch -- trust check may use stale data${NC}" + # Refresh the trust baseline before the hash-baseline check. Without this, + # a stale origin/ can cause auto-trust against an outdated + # baseline when from_branch is untrusted (feature/review branches). + if [[ "$trust_branch" != "$from_branch" ]]; then + if ! git fetch origin "$trust_branch" --quiet; then + echo " Warning: could not fetch origin/$trust_branch; baseline may be stale" >&2 + fi fi - # Skip trust entirely if the baseline ref doesn't exist locally. - if git rev-parse --verify "origin/$trust_branch" &>/dev/null; then - trust_dev_tools "$worktree_path" "origin/$trust_branch" "$allow_direnv_auto" + local trust_ref="origin/$trust_branch" + if git rev-parse --verify "$trust_ref" &>/dev/null; then + trust_dev_tools "$worktree_path" "$trust_ref" "$allow_direnv_auto" else - echo -e " ${YELLOW}Skipping dev tool trust -- origin/$trust_branch not found locally${NC}" + echo " Skipped: $trust_ref not available locally" fi - echo -e "${GREEN}✓ Worktree created successfully!${NC}" - echo "" - echo "To switch to this worktree:" - echo -e "${BLUE}cd $worktree_path${NC}" echo "" + echo "Worktree ready: $worktree_path" + echo "Switch with: cd $worktree_path" } -# List all worktrees -list_worktrees() { - echo -e "${BLUE}Available worktrees:${NC}" - echo "" - - if [[ ! -d "$WORKTREE_DIR" ]]; then - echo -e "${YELLOW}No worktrees found${NC}" - return - fi - - local count=0 - for worktree_path in "$WORKTREE_DIR"/*; do - if [[ -d "$worktree_path" && -e "$worktree_path/.git" ]]; then - count=$((count + 1)) - local worktree_name=$(basename "$worktree_path") - local branch=$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") - - if [[ "$PWD" == "$worktree_path" ]]; then - echo -e "${GREEN}✓ $worktree_name${NC} (current) → branch: $branch" - else - echo -e " $worktree_name → branch: $branch" - fi - fi - done - - if [[ $count -eq 0 ]]; then - echo -e "${YELLOW}No worktrees found${NC}" - else - echo "" - echo -e "${BLUE}Total: $count worktree(s)${NC}" - fi - - echo "" - echo -e "${BLUE}Main repository:${NC}" - local main_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") - echo " Branch: $main_branch" - echo " Path: $GIT_ROOT" -} - -# Switch to a worktree -switch_worktree() { - local worktree_name="$1" - - if [[ -z "$worktree_name" ]]; then - list_worktrees - echo -e "${BLUE}Switch to which worktree? (enter name)${NC}" - read -r worktree_name - fi - - local worktree_path="$WORKTREE_DIR/$worktree_name" - - if [[ ! -d "$worktree_path" ]]; then - echo -e "${RED}Error: Worktree not found: $worktree_name${NC}" - echo "" - list_worktrees - exit 1 - fi - - echo -e "${GREEN}Switching to worktree: $worktree_name${NC}" - cd "$worktree_path" - echo -e "${BLUE}Now in: $(pwd)${NC}" -} - -# Copy env files to an existing worktree (or current directory if in a worktree) -copy_env_to_worktree() { - local worktree_name="$1" - local worktree_path - - if [[ -z "$worktree_name" ]]; then - # Check if we're currently in a worktree - local current_dir=$(pwd) - if [[ "$current_dir" == "$WORKTREE_DIR"/* ]]; then - worktree_path="$current_dir" - worktree_name=$(basename "$worktree_path") - echo -e "${BLUE}Detected current worktree: $worktree_name${NC}" - else - echo -e "${YELLOW}Usage: worktree-manager.sh copy-env [worktree-name]${NC}" - echo "Or run from within a worktree to copy to current directory" - list_worktrees - return 1 - fi - else - worktree_path="$WORKTREE_DIR/$worktree_name" - - if [[ ! -d "$worktree_path" ]]; then - echo -e "${RED}Error: Worktree not found: $worktree_name${NC}" - list_worktrees - return 1 - fi - fi - - copy_env_files "$worktree_path" - echo "" -} - -# Clean up completed worktrees -cleanup_worktrees() { - if [[ ! -d "$WORKTREE_DIR" ]]; then - echo -e "${YELLOW}No worktrees to clean up${NC}" - return - fi - - echo -e "${BLUE}Checking for completed worktrees...${NC}" - echo "" - - local found=0 - local to_remove=() - - for worktree_path in "$WORKTREE_DIR"/*; do - if [[ -d "$worktree_path" && -e "$worktree_path/.git" ]]; then - local worktree_name=$(basename "$worktree_path") - - # Skip if current worktree - if [[ "$PWD" == "$worktree_path" ]]; then - echo -e "${YELLOW}(skip) $worktree_name - currently active${NC}" - continue - fi - - found=$((found + 1)) - to_remove+=("$worktree_path") - echo -e "${YELLOW}• $worktree_name${NC}" - fi - done - - if [[ $found -eq 0 ]]; then - echo -e "${GREEN}No inactive worktrees to clean up${NC}" - return - fi - - echo "" - echo -e "Remove $found worktree(s)? (y/n)" - read -r response - - if [[ "$response" != "y" ]]; then - echo -e "${YELLOW}Cleanup cancelled${NC}" - return - fi - - echo -e "${BLUE}Cleaning up worktrees...${NC}" - for worktree_path in "${to_remove[@]}"; do - local worktree_name=$(basename "$worktree_path") - git worktree remove "$worktree_path" --force 2>/dev/null || true - echo -e "${GREEN}✓ Removed: $worktree_name${NC}" - done - - # Clean up empty directory if nothing left - if [[ -z "$(ls -A "$WORKTREE_DIR" 2>/dev/null)" ]]; then - rmdir "$WORKTREE_DIR" 2>/dev/null || true - fi - - echo -e "${GREEN}Cleanup complete!${NC}" -} - -# Main command handler main() { - local command="${1:-list}" - + local command="${1:-}" + shift || true case "$command" in - create) - create_worktree "$2" "$3" - ;; - list|ls) - list_worktrees - ;; - switch|go) - switch_worktree "$2" - ;; - copy-env|env) - copy_env_to_worktree "$2" - ;; - cleanup|clean) - cleanup_worktrees - ;; - help) - show_help - ;; + create) create_worktree "$@" ;; + ""|help|-h|--help) usage ;; *) - echo -e "${RED}Unknown command: $command${NC}" - echo "" - show_help + echo "Error: unknown command '$command'" >&2 + usage >&2 exit 1 ;; esac } -show_help() { - cat << EOF -Git Worktree Manager - -Usage: worktree-manager.sh [options] - -Commands: - create [from-branch] Create new worktree (copies .env files automatically) - (from-branch defaults to main) - list | ls List all worktrees - switch | go [name] Switch to worktree - copy-env | env [name] Copy .env files from main repo to worktree - (if name omitted, uses current worktree) - cleanup | clean Clean up inactive worktrees - help Show this help message - -Environment Files: - - Automatically copies .env, .env.local, .env.test, etc. on create - - Skips .env.example (should be in git) - - Creates .backup files if destination already exists - - Use 'copy-env' to refresh env files after main repo changes - -Dev Tool Trust: - - Trusts mise config (.mise.toml, mise.toml, .tool-versions) and direnv (.envrc) - - Uses trusted base branches directly (main, develop, dev, trunk, staging, release/*) - - Other branches fall back to the default branch as the trust baseline - - direnv auto-allow is skipped on non-trusted base branches; review manually first - - Modified configs are flagged for manual review - - Only runs if the tool is installed and config exists - - Prevents hooks/scripts from hanging on interactive trust prompts - -Examples: - worktree-manager.sh create feature-login - worktree-manager.sh create feature-auth develop - worktree-manager.sh switch feature-login - worktree-manager.sh copy-env feature-login - worktree-manager.sh copy-env # copies to current worktree - worktree-manager.sh cleanup - worktree-manager.sh list - -EOF -} - -# Run main "$@"