diff --git a/.extensionignore b/.extensionignore index 369bf6b..86f79e2 100644 --- a/.extensionignore +++ b/.extensionignore @@ -1,5 +1,6 @@ # Development and publishing files — not needed when installed PUBLISH.md catalog-entry.json +tests/ .git/ .github/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..04f7aec --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + + - name: Configure git for tests + run: | + git config --global user.name "test" + git config --global user.email "test@test.com" + + - name: Run create-worktree tests + run: bash tests/test-create-worktree.sh + + - name: Run post-install tests + run: bash tests/test-post-install.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e458ed5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.worktrees/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4486415..8e104f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 1.2.0 (2026-04-14) + +### Changed +- Default layout switched from `sibling` to `nested` — worktrees now created at `.worktrees//` inside the repo by default +- Sibling layout (`../--`) remains available via `layout: "sibling"` in config + +### Added +- `post_install` lifecycle script — adds `.worktrees/` to `.gitignore` at install time (not just at first worktree creation) +- README section "How worktrees stay isolated" documenting gitignore + commit isolation model + +## 1.1.0 (2026-04-13) + +### Added +- `modifies_hooks` declaration: automatically disables `before_specify -> speckit.git.feature` on install (with user consent) so the primary checkout stays on a stable branch +- Requires Spec Kit with `modifies_hooks` support ([github/spec-kit#2209](https://github.com/github/spec-kit/pull/2209)) + ## 1.0.0 (2026-04-13) ### Added diff --git a/README.md b/README.md index 201c49c..5d6ab4e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # spec-kit-worktree-parallel +[![Tests](https://github.com/dango85/spec-kit-worktree-parallel/actions/workflows/test.yml/badge.svg)](https://github.com/dango85/spec-kit-worktree-parallel/actions/workflows/test.yml) + A [Spec Kit](https://github.com/github/spec-kit) extension for **default-on** git worktree isolation — work on multiple features (or run parallel agents) without checkout switching. ## Why another worktree extension? @@ -7,7 +9,7 @@ A [Spec Kit](https://github.com/github/spec-kit) extension for **default-on** gi The community [spec-kit-worktree](https://github.com/Quratulain-bilal/spec-kit-worktree) extension is a good starting point. This extension differs in three ways: 1. **Default-on** — worktrees are created automatically after `/speckit.specify`. Opt *out* with `--in-place`, rather than opting in. -2. **Sibling-dir layout** — worktrees live at `../--` by default, so each feature gets its own top-level IDE window. Nested `.worktrees/` is available as an option. +2. **Nested layout by default** — worktrees live at `.worktrees//` inside the repo (gitignored, self-contained). Sibling-dir layout (`../--`) is available as an option for IDE-per-feature workflows. 3. **Deterministic bash script** — a real script (`create-worktree.sh`) with `--json` output, `--dry-run`, and `SPECIFY_WORKTREE_PATH` override, suitable for CI and scripted workflows. ## Installation @@ -18,20 +20,7 @@ specify extension add --from https://github.com/dango85/spec-kit-worktree-parall ## Layout modes -### Sibling (default) - -Each worktree is a sibling directory of the primary clone: - -``` -parent/ -├── my-project/ ← primary checkout (main) -├── my-project--005-user-auth/ ← worktree (005-user-auth branch) -├── my-project--006-chat/ ← worktree (006-chat branch) -``` - -Open each directory in its own IDE window. No `.gitignore` changes needed. - -### Nested +### Nested (default) Worktrees live inside the repo under `.worktrees/` (auto-gitignored): @@ -44,19 +33,39 @@ my-project/ ├── src/ ``` -Switch with `layout: nested` in `worktree-config.yml`. +Self-contained — everything stays in one directory. `.worktrees/` is added to `.gitignore` at install time so worktree directories are never accidentally committed to the main repo. Work inside each worktree is committed on its own feature branch. + +### Sibling + +Each worktree is a sibling directory of the primary clone: + +``` +parent/ +├── my-project/ ← primary checkout (main) +├── my-project--005-user-auth/ ← worktree (005-user-auth branch) +├── my-project--006-chat/ ← worktree (006-chat branch) +``` + +Open each directory in its own IDE window. Switch with `layout: "sibling"` in `worktree-config.yml`. ## Configuration Create `.specify/extensions/worktrees/worktree-config.yml` to override defaults: ```yaml -layout: "sibling" # sibling | nested +layout: "nested" # nested | sibling auto_create: true # false to prompt instead of auto-creating sibling_pattern: "{{repo}}--{{branch}}" dotworktrees_dir: ".worktrees" ``` +## How worktrees stay isolated + +- **On install** (`specify extension add`): `.worktrees/` is added to `.gitignore` so the directory is ignored before any worktree exists +- **On create** (`/speckit.worktrees.create`): the script double-checks `.gitignore` as a safety net +- **Commits stay on the right branch**: each worktree has its own working tree and index — `git add` and `git commit` inside a worktree only affect that worktree's branch, not the main repo +- **Cleanup**: `/speckit.worktrees.clean` removes worktree directories; it never deletes the git branch itself + ## Commands | Command | Description | Modifies files? | @@ -65,20 +74,39 @@ dotworktrees_dir: ".worktrees" | `/speckit.worktrees.list` | Dashboard: status, artifacts, tasks | No | | `/speckit.worktrees.clean` | Remove merged/stale worktrees | Yes | -## Hook +## Hooks **`after_specify`** — automatically creates a worktree after a new feature is specified. Controlled by the `auto_create` config value. +## Hook overrides + +This extension declares `modifies_hooks` in `extension.yml` to **disable** the git extension's `before_specify -> speckit.git.feature` hook on install. This keeps the primary checkout on a stable branch (e.g. `main`) while worktrees handle feature branch isolation. + +During `specify extension add`, you will see a consent prompt: + +``` +Extension 'worktrees' requests the following hook modifications: + + Hook Target Extension Command Action Reason + before_specify git speckit.git.feature disable Worktree-parallel keeps primary checkout... + +Apply these modifications? [Y/n]: +``` + +Answering **Y** disables the hook. Answering **N** installs the extension without modifying hooks (you can disable it manually in `.specify/extensions.yml`). Removing the extension via `specify extension remove worktrees` restores the original hook state. + +**Requires**: Spec Kit with `modifies_hooks` support (see [github/spec-kit#2209](https://github.com/github/spec-kit/pull/2209)). + ## Script usage The bash script can be called directly for automation: ```bash -# Create a sibling worktree for branch 005-user-auth +# Create a nested worktree for branch 005-user-auth (default) bash scripts/bash/create-worktree.sh --json 005-user-auth -# Nested layout -bash scripts/bash/create-worktree.sh --json --layout nested 005-user-auth +# Sibling layout instead +bash scripts/bash/create-worktree.sh --json --layout sibling 005-user-auth # Explicit path bash scripts/bash/create-worktree.sh --json --path /tmp/my-worktree 005-user-auth diff --git a/commands/speckit.worktrees.create.md b/commands/speckit.worktrees.create.md index 9465903..b331c3e 100644 --- a/commands/speckit.worktrees.create.md +++ b/commands/speckit.worktrees.create.md @@ -28,7 +28,7 @@ Read configuration from `.specify/extensions/worktrees/worktree-config.yml` if i | Key | Default | Description | |-----|---------|-------------| -| `layout` | `sibling` | `sibling` — worktree at `../--` (IDE-friendly); `nested` — at `.worktrees//` inside repo | +| `layout` | `nested` | `nested` — worktree at `.worktrees//` inside repo (self-contained); `sibling` — at `../--` (IDE-friendly) | | `auto_create` | `true` | When `true`, the `after_specify` hook creates a worktree without prompting | | `sibling_pattern` | `{{repo}}--{{branch}}` | Name pattern for sibling directories | | `dotworktrees_dir` | `.worktrees` | Subdirectory name for nested layout | diff --git a/extension.yml b/extension.yml index 3dafd8b..2a9f1ab 100644 --- a/extension.yml +++ b/extension.yml @@ -3,7 +3,7 @@ schema_version: "1.0" extension: id: worktrees name: "Worktrees" - version: "1.0.0" + version: "1.2.0" description: "Default-on worktree isolation for parallel agents — sibling or nested layout" author: "dango85" repository: "https://github.com/dango85/spec-kit-worktree-parallel" @@ -39,6 +39,18 @@ hooks: optional: false description: "Auto-spawn a worktree after a new feature is specified" +lifecycle: + post_install: + script: scripts/bash/post-install.sh + description: "Add .worktrees/ to .gitignore so worktree directories are never committed to the main repo" + +modifies_hooks: + - hook: before_specify + extension: git + command: speckit.git.feature + action: disable + reason: "Worktree-parallel keeps primary checkout on a stable branch (e.g. main); branch creation is handled by git worktree add -b during after_specify" + tags: - "worktree" - "git" @@ -48,7 +60,7 @@ tags: config: defaults: - layout: "sibling" + layout: "nested" auto_create: true sibling_pattern: "{{repo}}--{{branch}}" dotworktrees_dir: ".worktrees" diff --git a/scripts/bash/create-worktree.sh b/scripts/bash/create-worktree.sh index 82d2465..96d8ed6 100755 --- a/scripts/bash/create-worktree.sh +++ b/scripts/bash/create-worktree.sh @@ -20,7 +20,7 @@ set -euo pipefail # --- defaults --- -LAYOUT="sibling" +LAYOUT="nested" WORKTREE_PATH_OVERRIDE="" IN_PLACE=false JSON_MODE=false @@ -45,7 +45,7 @@ while [[ $# -gt 0 ]]; do echo "Usage: $0 [options] " echo "" echo "Options:" - echo " --layout sibling|nested Worktree location strategy (default: sibling)" + echo " --layout nested|sibling Worktree location strategy (default: nested)" echo " --path Explicit worktree path (overrides layout)" echo " --in-place Skip worktree creation (no-op exit 0)" echo " --json Output JSON instead of key=value" diff --git a/scripts/bash/post-install.sh b/scripts/bash/post-install.sh new file mode 100755 index 0000000..23ef96b --- /dev/null +++ b/scripts/bash/post-install.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# post-install.sh — runs after `specify extension add worktrees` +# Ensures .worktrees/ is in .gitignore immediately so the directory +# is ignored before any worktree is ever created. + +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0 + +# Load dotworktrees_dir from config, fall back to .worktrees +CONFIG_FILE="$REPO_ROOT/.specify/extensions/worktrees/worktree-config.yml" +DOTWORKTREES_DIR=".worktrees" +if [[ -f "$CONFIG_FILE" ]]; then + val=$(grep -E "^dotworktrees_dir:" "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/^[^:]*: *//; s/ *#.*//; s/^"//; s/"$//' || true) + if [[ -n "$val" ]]; then DOTWORKTREES_DIR="$val"; fi +fi + +GITIGNORE="$REPO_ROOT/.gitignore" + +if ! grep -qxF "$DOTWORKTREES_DIR/" "$GITIGNORE" 2>/dev/null; then + echo "$DOTWORKTREES_DIR/" >> "$GITIGNORE" + echo "[worktrees] Added '$DOTWORKTREES_DIR/' to .gitignore" >&2 +fi diff --git a/tests/test-create-worktree.sh b/tests/test-create-worktree.sh new file mode 100755 index 0000000..f90795c --- /dev/null +++ b/tests/test-create-worktree.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# Tests for create-worktree.sh +# Usage: bash tests/test-create-worktree.sh +# +# Creates a temporary git repo, runs all tests, cleans up. +# Exit code 0 = all passed, 1 = failures. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CREATE_SCRIPT="$SCRIPT_DIR/scripts/bash/create-worktree.sh" +PASS=0 +FAIL=0 +TOTAL=0 + +# --- helpers --- + +ORIG_DIR="$(pwd)" + +setup_temp_repo() { + TEMP_DIR=$(python3 -c "import os,tempfile; print(os.path.realpath(tempfile.mkdtemp()))") + git -C "$TEMP_DIR" init -b main >/dev/null 2>&1 + echo "init" > "$TEMP_DIR/README.md" + git -C "$TEMP_DIR" add . && git -C "$TEMP_DIR" commit -m "init" >/dev/null 2>&1 + mkdir -p "$TEMP_DIR/specs" + cd "$TEMP_DIR" + echo "$TEMP_DIR" +} + +cleanup() { + cd "$ORIG_DIR" + if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "$TEMP_DIR" ]]; then + git -C "$TEMP_DIR" worktree prune 2>/dev/null || true + rm -rf "${TEMP_DIR}"--* 2>/dev/null || true + rm -rf "$TEMP_DIR" + TEMP_DIR="" + fi +} + +assert_eq() { + local label="$1" expected="$2" actual="$3" + TOTAL=$((TOTAL + 1)) + if [[ "$expected" == "$actual" ]]; then + PASS=$((PASS + 1)) + echo " PASS: $label" + else + FAIL=$((FAIL + 1)) + echo " FAIL: $label" + echo " expected: $expected" + echo " actual: $actual" + fi +} + +assert_contains() { + local label="$1" needle="$2" haystack="$3" + TOTAL=$((TOTAL + 1)) + if echo "$haystack" | grep -qF "$needle"; then + PASS=$((PASS + 1)) + echo " PASS: $label" + else + FAIL=$((FAIL + 1)) + echo " FAIL: $label" + echo " expected to contain: $needle" + echo " actual: $haystack" + fi +} + +assert_exit() { + local label="$1" expected_code="$2" + shift 2 + TOTAL=$((TOTAL + 1)) + set +e + "$@" >/dev/null 2>&1 + local actual_code=$? + set -e + if [[ "$actual_code" -eq "$expected_code" ]]; then + PASS=$((PASS + 1)) + echo " PASS: $label" + else + FAIL=$((FAIL + 1)) + echo " FAIL: $label (expected exit $expected_code, got $actual_code)" + fi +} + +# --- tests --- + +echo "=== create-worktree.sh tests ===" +echo "" + +# Test 1: --help exits 0 +echo "[1] --help exits 0" +assert_exit "--help exits 0" 0 bash "$CREATE_SCRIPT" --help + +# Test 2: missing branch name exits 1 +echo "[2] missing branch name exits 1" +assert_exit "no branch exits 1" 1 bash "$CREATE_SCRIPT" --json + +# Test 3: dry-run nested (default) +echo "[3] dry-run nested layout (default)" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +output=$(bash "$CREATE_SCRIPT" --json --dry-run --repo-root "$TEMP_DIR" 005-test-feature) +assert_contains "output is JSON" '"branch":"005-test-feature"' "$output" +assert_contains "layout is nested" '"layout":"nested"' "$output" +assert_contains "path contains .worktrees" '.worktrees/005-test-feature' "$output" +assert_contains "dry_run is true" '"dry_run":true' "$output" +cleanup; trap - EXIT + +# Test 4: dry-run sibling layout +echo "[4] dry-run sibling layout" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +output=$(bash "$CREATE_SCRIPT" --json --dry-run --layout sibling --repo-root "$TEMP_DIR" 005-test-feature) +assert_contains "layout is sibling" '"layout":"sibling"' "$output" +base=$(basename "$TEMP_DIR") +assert_contains "sibling path pattern" "${base}--005-test-feature" "$output" +cleanup; trap - EXIT + +# Test 5: dry-run with explicit --path +echo "[5] dry-run with explicit --path" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +output=$(bash "$CREATE_SCRIPT" --json --dry-run --repo-root "$TEMP_DIR" --path /tmp/custom-wt 005-test-feature) +assert_contains "uses explicit path" '"/tmp/custom-wt"' "$output" +cleanup; trap - EXIT + +# Test 6: --in-place skips worktree +echo "[6] --in-place skips worktree" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +output=$(bash "$CREATE_SCRIPT" --json --in-place 005-test-feature) +assert_contains "worktree is false" '"worktree":false' "$output" +assert_contains "path is empty" '"path":""' "$output" +cleanup; trap - EXIT + +# Test 7: SPECIFY_WORKTREE_PATH env override +echo "[7] SPECIFY_WORKTREE_PATH env override" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +output=$(SPECIFY_WORKTREE_PATH=/tmp/env-override bash "$CREATE_SCRIPT" --json --dry-run --repo-root "$TEMP_DIR" 005-test-feature) +assert_contains "uses env path" '"/tmp/env-override"' "$output" +cleanup; trap - EXIT + +# Test 8: real worktree creation (nested) +echo "[8] real worktree creation (nested)" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +output=$(bash "$CREATE_SCRIPT" --json --repo-root "$TEMP_DIR" 005-real-test 2>/dev/null) +assert_contains "worktree is true" '"worktree":true' "$output" +wt_path="$TEMP_DIR/.worktrees/005-real-test" +TOTAL=$((TOTAL + 1)) +if [[ -d "$wt_path" ]]; then + PASS=$((PASS + 1)) + echo " PASS: worktree directory exists" +else + FAIL=$((FAIL + 1)) + echo " FAIL: worktree directory does not exist at $wt_path" +fi +# verify branch in worktree +branch=$(git -C "$wt_path" branch --show-current 2>/dev/null) +assert_eq "worktree is on correct branch" "005-real-test" "$branch" +# verify .gitignore was updated +TOTAL=$((TOTAL + 1)) +if grep -qxF ".worktrees/" "$TEMP_DIR/.gitignore" 2>/dev/null; then + PASS=$((PASS + 1)) + echo " PASS: .worktrees/ in .gitignore" +else + FAIL=$((FAIL + 1)) + echo " FAIL: .worktrees/ not in .gitignore" +fi +# cleanup worktree +git -C "$TEMP_DIR" worktree remove "$wt_path" 2>/dev/null || true +cleanup; trap - EXIT + +# Test 9: real worktree creation (sibling) +echo "[9] real worktree creation (sibling)" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +output=$(bash "$CREATE_SCRIPT" --json --layout sibling --repo-root "$TEMP_DIR" 005-sibling-test 2>/dev/null) +assert_contains "worktree is true" '"worktree":true' "$output" +base=$(basename "$TEMP_DIR") +sibling_path="$(dirname "$TEMP_DIR")/${base}--005-sibling-test" +TOTAL=$((TOTAL + 1)) +if [[ -d "$sibling_path" ]]; then + PASS=$((PASS + 1)) + echo " PASS: sibling worktree directory exists" +else + FAIL=$((FAIL + 1)) + echo " FAIL: sibling worktree directory does not exist at $sibling_path" +fi +branch=$(git -C "$sibling_path" branch --show-current 2>/dev/null) +assert_eq "sibling worktree on correct branch" "005-sibling-test" "$branch" +git -C "$TEMP_DIR" worktree remove "$sibling_path" 2>/dev/null || true +cleanup; trap - EXIT + +# Test 10: duplicate worktree path blocked +echo "[10] duplicate worktree path blocked" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +bash "$CREATE_SCRIPT" --json --repo-root "$TEMP_DIR" 005-dup-test >/dev/null 2>&1 +assert_exit "second create fails" 1 bash "$CREATE_SCRIPT" --json --repo-root "$TEMP_DIR" 005-dup-test +git -C "$TEMP_DIR" worktree remove "$TEMP_DIR/.worktrees/005-dup-test" 2>/dev/null || true +cleanup; trap - EXIT + +# Test 11: config file overrides default layout +echo "[11] config file overrides default layout" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +mkdir -p "$TEMP_DIR/.specify/extensions/worktrees" +echo 'layout: "sibling"' > "$TEMP_DIR/.specify/extensions/worktrees/worktree-config.yml" +output=$(bash "$CREATE_SCRIPT" --json --dry-run --repo-root "$TEMP_DIR" 005-config-test) +assert_contains "config overrides to sibling" '"layout":"sibling"' "$output" +cleanup; trap - EXIT + +# Test 12: branch with slashes handled +echo "[12] branch name with slashes sanitized" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +output=$(bash "$CREATE_SCRIPT" --json --dry-run --repo-root "$TEMP_DIR" feature/user-auth) +assert_contains "slashes replaced" 'feature-user-auth' "$output" +cleanup; trap - EXIT + +# --- summary --- +echo "" +echo "=== Results: $PASS/$TOTAL passed, $FAIL failed ===" + +if [[ "$FAIL" -gt 0 ]]; then + exit 1 +fi diff --git a/tests/test-post-install.sh b/tests/test-post-install.sh new file mode 100755 index 0000000..a885d7e --- /dev/null +++ b/tests/test-post-install.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Tests for post-install.sh +# Usage: bash tests/test-post-install.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +POST_INSTALL="$SCRIPT_DIR/scripts/bash/post-install.sh" +PASS=0 +FAIL=0 +TOTAL=0 + +ORIG_DIR="$(pwd)" + +setup_temp_repo() { + TEMP_DIR=$(python3 -c "import os,tempfile; print(os.path.realpath(tempfile.mkdtemp()))") + git -C "$TEMP_DIR" init -b main >/dev/null 2>&1 + echo "init" > "$TEMP_DIR/README.md" + git -C "$TEMP_DIR" add . && git -C "$TEMP_DIR" commit -m "init" >/dev/null 2>&1 + mkdir -p "$TEMP_DIR/.specify/extensions/worktrees" + cd "$TEMP_DIR" + echo "$TEMP_DIR" +} + +cleanup() { + cd "$ORIG_DIR" + if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + TEMP_DIR="" + fi +} + +assert_file_contains() { + local label="$1" file="$2" needle="$3" + TOTAL=$((TOTAL + 1)) + if grep -qxF "$needle" "$file" 2>/dev/null; then + PASS=$((PASS + 1)) + echo " PASS: $label" + else + FAIL=$((FAIL + 1)) + echo " FAIL: $label" + echo " expected '$needle' in $file" + fi +} + +assert_line_count() { + local label="$1" file="$2" needle="$3" expected="$4" + TOTAL=$((TOTAL + 1)) + local count + count=$(grep -cxF "$needle" "$file" 2>/dev/null || echo "0") + if [[ "$count" -eq "$expected" ]]; then + PASS=$((PASS + 1)) + echo " PASS: $label" + else + FAIL=$((FAIL + 1)) + echo " FAIL: $label (expected $expected occurrences, got $count)" + fi +} + +echo "=== post-install.sh tests ===" +echo "" + +# helper: run post-install inside temp repo so git rev-parse resolves correctly +run_post_install() { + (cd "$TEMP_DIR" && bash "$POST_INSTALL" 2>/dev/null) +} + +# Test 1: adds .worktrees/ to .gitignore when not present +echo "[1] adds .worktrees/ to .gitignore" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +run_post_install +assert_file_contains ".worktrees/ added" "$TEMP_DIR/.gitignore" ".worktrees/" +cleanup; trap - EXIT + +# Test 2: does not duplicate if already present +echo "[2] idempotent — no duplicate entry" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +echo ".worktrees/" > "$TEMP_DIR/.gitignore" +run_post_install +assert_line_count "single entry" "$TEMP_DIR/.gitignore" ".worktrees/" 1 +cleanup; trap - EXIT + +# Test 3: reads custom dotworktrees_dir from config +echo "[3] respects custom dotworktrees_dir from config" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +echo 'dotworktrees_dir: ".wt"' > "$TEMP_DIR/.specify/extensions/worktrees/worktree-config.yml" +run_post_install +assert_file_contains "custom dir in .gitignore" "$TEMP_DIR/.gitignore" ".wt/" +cleanup; trap - EXIT + +# Test 4: works when .gitignore doesn't exist yet +echo "[4] creates .gitignore if absent" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +rm -f "$TEMP_DIR/.gitignore" +run_post_install +assert_file_contains ".gitignore created with entry" "$TEMP_DIR/.gitignore" ".worktrees/" +cleanup; trap - EXIT + +# Test 5: preserves existing .gitignore content +echo "[5] preserves existing .gitignore content" +TEMP_DIR=$(setup_temp_repo) +trap cleanup EXIT +echo "node_modules/" > "$TEMP_DIR/.gitignore" +run_post_install +assert_file_contains "existing entry preserved" "$TEMP_DIR/.gitignore" "node_modules/" +assert_file_contains ".worktrees/ appended" "$TEMP_DIR/.gitignore" ".worktrees/" +cleanup; trap - EXIT + +echo "" +echo "=== Results: $PASS/$TOTAL passed, $FAIL failed ===" + +if [[ "$FAIL" -gt 0 ]]; then + exit 1 +fi diff --git a/worktree-config.yml b/worktree-config.yml index fe96f75..ad729a5 100644 --- a/worktree-config.yml +++ b/worktree-config.yml @@ -2,10 +2,10 @@ # Copy this file to .specify/extensions/worktrees/worktree-config.yml # to override defaults per-repo. -# Layout mode: "sibling" or "nested" -# sibling — worktree at ../-- (default; IDE-friendly, one window per feature) -# nested — worktree at .worktrees// inside the repo (gitignored) -layout: "sibling" +# Layout mode: "nested" or "sibling" +# nested — worktree at .worktrees// inside the repo (default; gitignored, self-contained) +# sibling — worktree at ../-- (IDE-friendly, one window per feature) +layout: "nested" # Auto-create worktree after /speckit.specify without prompting. # Set to false to get a confirmation prompt instead.