feat: add worktree slash commands for isolated Rust development#364
feat: add worktree slash commands for isolated Rust development#364pszymkowiak merged 1 commit intodevelopfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Claude Code slash-command docs for creating and cleaning git worktrees in the RTK repo (with optional background cargo check), and updates documentation/version references to match the current RTK release.
Changes:
- Add
/worktree+/worktree-statuscommands to create isolated worktrees and track a backgroundcargo check. - Add
/clean-worktrees(automatic) and/clean-worktree(interactive) cleanup commands for merged worktrees. - Update docs to reference RTK
0.27.0and ignore.worktrees/+.vitals/directories.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates “verify version” snippet to 0.27.0. |
| CLAUDE.md | Updates “verify version” snippet to 0.27.0. |
| ARCHITECTURE.md | Updates recorded RTK version to 0.27.0. |
| .gitignore | Adds ignores for .vitals/ and .worktrees/. |
| .claude/commands/worktree.md | New command script/doc to create worktrees and run cargo check (background/foreground). |
| .claude/commands/worktree-status.md | New command script/doc to read background cargo check log and report status. |
| .claude/commands/clean-worktrees.md | New automatic cleanup script/doc for merged worktrees (supports --dry-run). |
| .claude/commands/clean-worktree.md | New interactive cleanup script/doc for merged worktrees. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Parse flags | ||
| RAW_ARGS="$ARGUMENTS" | ||
| BRANCH_NAME="$RAW_ARGS" | ||
| SKIP_CHECK=false | ||
| BLOCKING_CHECK=false | ||
|
|
||
| if [[ "$RAW_ARGS" == *"--fast"* ]]; then | ||
| SKIP_CHECK=true | ||
| BRANCH_NAME="${BRANCH_NAME// --fast/}" | ||
| fi | ||
| if [[ "$RAW_ARGS" == *"--check"* ]]; then | ||
| BLOCKING_CHECK=true | ||
| BRANCH_NAME="${BRANCH_NAME// --check/}" | ||
| fi |
There was a problem hiding this comment.
Flag parsing via substring replacement (${BRANCH_NAME// --fast/} / // --check/) is brittle (order-dependent, fails if flag is first, can leave extra whitespace, and doesn't prevent additional unknown args). Consider parsing args properly (e.g., loop over tokens / getopts) and deriving BRANCH_NAME from the remaining positional arg(s).
| # Parse flags | |
| RAW_ARGS="$ARGUMENTS" | |
| BRANCH_NAME="$RAW_ARGS" | |
| SKIP_CHECK=false | |
| BLOCKING_CHECK=false | |
| if [[ "$RAW_ARGS" == *"--fast"* ]]; then | |
| SKIP_CHECK=true | |
| BRANCH_NAME="${BRANCH_NAME// --fast/}" | |
| fi | |
| if [[ "$RAW_ARGS" == *"--check"* ]]; then | |
| BLOCKING_CHECK=true | |
| BRANCH_NAME="${BRANCH_NAME// --check/}" | |
| fi | |
| # Parse flags and branch name from $ARGUMENTS | |
| BRANCH_NAME="" | |
| SKIP_CHECK=false | |
| BLOCKING_CHECK=false | |
| # Convert $ARGUMENTS into positional parameters for robust parsing | |
| set -- $ARGUMENTS | |
| while [ "$#" -gt 0 ]; do | |
| case "$1" in | |
| --fast) | |
| SKIP_CHECK=true | |
| ;; | |
| --check) | |
| BLOCKING_CHECK=true | |
| ;; | |
| -*) | |
| echo "Unknown flag: $1" | |
| exit 1 | |
| ;; | |
| *) | |
| if [ -n "$BRANCH_NAME" ]; then | |
| echo "Multiple branch names provided: '$BRANCH_NAME' and '$1'" | |
| exit 1 | |
| fi | |
| BRANCH_NAME="$1" | |
| ;; | |
| esac | |
| shift | |
| done | |
| if [ -z "$BRANCH_NAME" ]; then | |
| echo "Usage: /worktree <branch> [--fast|--check]" | |
| exit 1 | |
| fi |
| CURRENT_DIR="$(pwd)" | ||
|
|
||
| while IFS= read -r line; do | ||
| path=$(echo "$line" | awk '{print $1}') | ||
| branch=$(echo "$line" | grep -oE '\[.*\]' | tr -d '[]' || true) | ||
|
|
||
| [ -z "$branch" ] && continue | ||
| [ "$branch" = "master" ] && continue | ||
| [ "$branch" = "main" ] && continue | ||
| [ "$path" = "$CURRENT_DIR" ] && continue | ||
|
|
There was a problem hiding this comment.
CURRENT_DIR="$(pwd)" only protects you if you run the command from the worktree root. If you run it from a subdirectory inside a worktree, $CURRENT_DIR won’t equal the worktree path and the script may remove the worktree you’re currently using. Compare realpaths and treat any worktree path that is a prefix of the current directory as “current” (or use git rev-parse --show-toplevel for the current worktree root).
| [ "$branch" = "main" ] && continue | ||
| [ "$path" = "$CURRENT_DIR" ] && continue | ||
|
|
||
| if git branch --merged master | grep -q "^[* ] ${branch}$" 2>/dev/null; then |
There was a problem hiding this comment.
Branch merge detection uses grep with an unescaped branch name (grep -q "^[* ] ${branch}$"), so branch names containing regex metacharacters (e.g. ., +, []) can cause false matches and lead to deleting the wrong worktree/branch. Use git branch --merged master --format="%(refname:short)" | grep -Fxq "$branch" (or another exact-match approach) instead of regex matching.
| if git branch --merged master | grep -q "^[* ] ${branch}$" 2>/dev/null; then | |
| if git branch --merged master --format="%(refname:short)" | grep -Fxq "$branch" 2>/dev/null; then |
| if git branch --merged master | grep -q "^[* ] ${branch}$" 2>/dev/null; then | ||
| echo " - $branch (at $path) - MERGED" |
There was a problem hiding this comment.
Branch merge detection uses grep with an unescaped branch name (grep -q "^[* ] ${branch}$"), so branch names containing regex metacharacters can cause false positives and list/delete the wrong worktree/branch. Prefer an exact-match check (e.g., git branch --merged master --format="%(refname:short)" | grep -Fxq "$branch").
| while IFS= read -r line; do | ||
| path=$(echo "$line" | awk '{print $1}') | ||
| branch=$(echo "$line" | grep -oE '\[.*\]' | tr -d '[]' || true) | ||
|
|
||
| [ -z "$branch" ] && continue | ||
| [ "$branch" = "master" ] && continue | ||
| [ "$branch" = "main" ] && continue | ||
| [ "$path" = "$CURRENT_DIR" ] && continue | ||
|
|
||
| if git branch --merged master | grep -q "^[* ] ${branch}$" 2>/dev/null; then | ||
| MERGED_COUNT=$((MERGED_COUNT + 1)) | ||
| MERGED_BRANCHES+=("$branch|$path") | ||
| echo " - $branch (merged)" | ||
| fi | ||
| done < <(git worktree list) |
There was a problem hiding this comment.
Parsing git worktree list with awk '{print $1}' will break if a worktree path contains spaces (it will truncate the path), and later git worktree remove / rm -rf could target the wrong directory. Use git worktree list --porcelain (or -z where available) to parse paths safely.
| while IFS= read -r line; do | |
| path=$(echo "$line" | awk '{print $1}') | |
| branch=$(echo "$line" | grep -oE '\[.*\]' | tr -d '[]' || true) | |
| [ -z "$branch" ] && continue | |
| [ "$branch" = "master" ] && continue | |
| [ "$branch" = "main" ] && continue | |
| [ "$path" = "$CURRENT_DIR" ] && continue | |
| if git branch --merged master | grep -q "^[* ] ${branch}$" 2>/dev/null; then | |
| MERGED_COUNT=$((MERGED_COUNT + 1)) | |
| MERGED_BRANCHES+=("$branch|$path") | |
| echo " - $branch (merged)" | |
| fi | |
| done < <(git worktree list) | |
| path="" | |
| branch="" | |
| while IFS= read -r line; do | |
| if [[ "$line" == worktree\ * ]]; then | |
| path="${line#worktree }" | |
| elif [[ "$line" == branch\ * ]]; then | |
| ref="${line#branch }" | |
| branch="${ref#refs/heads/}" | |
| elif [[ -z "$line" ]]; then | |
| # End of a worktree record; process collected path and branch | |
| if [ -n "$branch" ]; then | |
| [ "$branch" = "master" ] && { path=""; branch=""; continue; } | |
| [ "$branch" = "main" ] && { path=""; branch=""; continue; } | |
| [ "$path" = "$CURRENT_DIR" ] && { path=""; branch=""; continue; } | |
| if git branch --merged master | grep -q "^[* ] ${branch}$" 2>/dev/null; then | |
| MERGED_COUNT=$((MERGED_COUNT + 1)) | |
| MERGED_BRANCHES+=("$branch|$path") | |
| echo " - $branch (merged)" | |
| fi | |
| fi | |
| # Reset for next record | |
| path="" | |
| branch="" | |
| fi | |
| done < <(git worktree list --porcelain) |
| # Parse flags | ||
| RAW_ARGS="$ARGUMENTS" | ||
| BRANCH_NAME="$RAW_ARGS" |
There was a problem hiding this comment.
With set -u, reading $ARGUMENTS will abort the script if that variable is unset in the caller environment. Use a default (RAW_ARGS="${ARGUMENTS:-}") and add an explicit check that the branch argument is present (and not just flags) before proceeding.
| #!/bin/bash | ||
| set -euo pipefail | ||
|
|
||
| BRANCH_NAME="$ARGUMENTS" |
There was a problem hiding this comment.
With set -u, BRANCH_NAME="$ARGUMENTS" will fail if ARGUMENTS is unset. Use BRANCH_NAME="${ARGUMENTS:-}" and validate it's non-empty (and not just whitespace) before building the log file path.
| BRANCH_NAME="$ARGUMENTS" | |
| BRANCH_NAME="${ARGUMENTS:-}" | |
| # Ensure branch name is provided and not just whitespace before using it in a path | |
| if [[ -z "${BRANCH_NAME//[[:space:]]/}" ]]; then | |
| echo "Error: branch name must be provided." | |
| echo "Usage: /worktree-status <branch-name>" | |
| exit 1 | |
| fi |
| LOG_CONTENT=$(head -n 500 "$LOG_FILE") | ||
|
|
||
| if echo "$LOG_CONTENT" | grep -q "^PASSED"; then | ||
| TIMESTAMP=$(echo "$LOG_CONTENT" | grep "^PASSED" | sed 's/PASSED at //') | ||
| echo "cargo check passed" |
There was a problem hiding this comment.
LOG_CONTENT=$(head -n 500 ...) can miss the PASSED/FAILED sentinel lines because those are appended at the end of the log, and cargo check output can easily exceed 500 lines. Prefer checking tail (or scanning the whole file) for the completion marker and timestamp so completed checks aren’t reported as “still running/unknown”.
| CURRENT_DIR="$(pwd)" | ||
|
|
||
| while IFS= read -r line; do | ||
| path=$(echo "$line" | awk '{print $1}') | ||
| branch=$(echo "$line" | grep -oE '\[.*\]' | tr -d '[]' || true) | ||
| [ -z "$branch" ] && continue | ||
| [ "$branch" = "master" ] && continue | ||
| [ "$branch" = "main" ] && continue | ||
| [ "$path" = "$CURRENT_DIR" ] && continue | ||
|
|
There was a problem hiding this comment.
CURRENT_DIR="$(pwd)" only matches the worktree root. If the script is executed from a subdirectory inside a worktree, $CURRENT_DIR won’t equal the worktree path and the script can end up offering to delete the worktree you’re currently in. Consider comparing realpaths with prefix checks, or compute the current worktree root via git rev-parse --show-toplevel and exclude that worktree path.
| while IFS= read -r line; do | ||
| path=$(echo "$line" | awk '{print $1}') | ||
| branch=$(echo "$line" | grep -oE '\[.*\]' | tr -d '[]' || true) | ||
| [ -z "$branch" ] && continue |
There was a problem hiding this comment.
Parsing git worktree list with awk '{print $1}' will truncate paths containing spaces, and that can lead to deleting the wrong directory when calling git worktree remove / rm -rf. Prefer git worktree list --porcelain (or another machine-readable format) and parse the worktree <path> lines instead.
|
Hi! Thanks for the contribution! Since March 6, all PRs should target the Could you update the base branch? Click Edit at the top right of this PR and change it from Thanks! |
Adds 4 Claude Code slash commands for git worktree management, adapted from MethodeAristote project (Rust/cargo instead of pnpm/TypeScript). Commands: - /worktree <branch>: creates worktree + background cargo check - --fast: skip cargo check (instant) - --check: blocking cargo check before ready - /worktree-status <branch>: check background cargo check result - /clean-worktrees: auto-remove all merged worktrees (--dry-run support) - /clean-worktree: interactive cleanup with confirmation Also adds .worktrees/ to .gitignore (required by /worktree command). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
685fc4a to
0abcc02
Compare
Adds 4 Claude Code slash commands for git worktree management, adapted from MethodeAristote project (Rust/cargo instead of pnpm/TypeScript). Commands: - /worktree <branch>: creates worktree + background cargo check - --fast: skip cargo check (instant) - --check: blocking cargo check before ready - /worktree-status <branch>: check background cargo check result - /clean-worktrees: auto-remove all merged worktrees (--dry-run support) - /clean-worktree: interactive cleanup with confirmation Also adds .worktrees/ to .gitignore (required by /worktree command). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Ports 4 worktree management slash commands from MethodeAristote project, adapted for RTK (Rust/cargo instead of pnpm/TypeScript).
New commands:
/worktree <branch>cargo check/worktree-status <branch>cargo checkresult/clean-worktrees/clean-worktreeKey adaptations from source project:
pnpm tsc --noEmit→cargo check(background by default)developbranch →masterbranch for merge detectiontimeout(usesgtimeoutfallback)--fast(skip check) and--check(blocking check)Also fixes: version refs in README, CLAUDE.md, ARCHITECTURE.md (0.26.0 → 0.27.0) — pre-push hook was blocking any push on master.
Test plan
/worktree feature/test-cmd— creates.worktrees/feature-test-cmd, startscargo checkin background/worktree-status feature/test-cmd— shows check result/worktree feature/test-fast --fast— instant, no cargo check/clean-worktrees --dry-run— preview mode, no deletions/clean-worktree— interactive, confirms before deleting🤖 Generated with Claude Code