diff --git a/.gitignore b/.gitignore index 4e35d43fb5..e89206b677 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ pip-wheel-metadata # for running AWS Lambda tests using AWS SAM sam.template.yaml + +.worktrees/ diff --git a/Makefile b/Makefile index fb5900e5ea..c8a8170947 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ help: @echo @echo "make apidocs: Build the API documentation" @echo "make aws-lambda-layer: Build AWS Lambda layer directory for serverless integration" + @echo "make worktree-create NAME=: Create a worktree with a new feature branch and virtual environment" + @echo "make worktree-delete NAME=: Remove a worktree (prompts to delete branch)" + @echo "make worktree-list: List all active worktrees" @echo @echo "Also make sure to read ./CONTRIBUTING.md" @echo @@ -33,3 +36,19 @@ aws-lambda-layer: dist $(VENV_PATH)/bin/pip install -r requirements-aws-lambda-layer.txt $(VENV_PATH)/bin/python -m scripts.build_aws_lambda_layer .PHONY: aws-lambda-layer + +worktree-create: + @test -n "$(NAME)" || (echo "Error: NAME is required. Usage: make worktree-create NAME=" && false) + @echo "$(NAME)" | grep -qE '^[a-zA-Z0-9_/-]+$$' || (echo "Error: NAME contains invalid characters" && false) + ./scripts/worktree-create.sh "$(NAME)" +.PHONY: worktree-create + +worktree-delete: + @test -n "$(NAME)" || (echo "Error: NAME is required. Usage: make worktree-delete NAME=" && false) + @echo "$(NAME)" | grep -qE '^[a-zA-Z0-9_/-]+$$' || (echo "Error: NAME contains invalid characters" && false) + ./scripts/worktree-delete.sh "$(NAME)" +.PHONY: worktree-delete + +worktree-list: + @git worktree list +.PHONY: worktree-list diff --git a/scripts/worktree-create.sh b/scripts/worktree-create.sh new file mode 100755 index 0000000000..8ce9c5bee0 --- /dev/null +++ b/scripts/worktree-create.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +POSITIONAL_ARGS=("$@") + +# For simplicity, we use the same value for both the worktree name and branch name. +WORKTREE_NAME="${POSITIONAL_ARGS[0]}" +BRANCH_NAME="${POSITIONAL_ARGS[0]}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +WORKTREE_DIR="$REPO_ROOT/.worktrees/$WORKTREE_NAME" + +if [[ -d "$WORKTREE_DIR" ]]; then + echo "Error: worktree directory already exists: $WORKTREE_DIR" >&2 + exit 1 +fi + +if git -C "$REPO_ROOT" branch --list "$BRANCH_NAME" | grep -q .; then + echo "Error: branch '$BRANCH_NAME' already exists. Delete it first or choose a different name." >&2 + exit 1 +fi + +echo "Creating worktree '$WORKTREE_NAME' on branch '$BRANCH_NAME'..." +git -C "$REPO_ROOT" worktree add "$WORKTREE_DIR" -b "$BRANCH_NAME" + +if command -v uv &>/dev/null; then + echo "Setting up virtual environment with uv..." + uv venv "$WORKTREE_DIR/.venv" +else + echo "uv not found — falling back to python -m venv..." + python -m venv "$WORKTREE_DIR/.venv" +fi + +echo "" +echo "Worktree ready!" +echo " Path: $WORKTREE_DIR" +echo " Branch: $BRANCH_NAME" +echo "" +echo "To start working:" +echo " cd $WORKTREE_DIR" +echo " source .venv/bin/activate" diff --git a/scripts/worktree-delete.sh b/scripts/worktree-delete.sh new file mode 100755 index 0000000000..7c86724698 --- /dev/null +++ b/scripts/worktree-delete.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +for WORKTREE_NAME in "$@"; do + WORKTREE_DIR="$REPO_ROOT/.worktrees/$WORKTREE_NAME" + + if [[ ! -d "$WORKTREE_DIR" ]]; then + echo "Warning: worktree directory not found: $WORKTREE_DIR — skipping" >&2 + continue + fi + + # Capture branch name before removal + BRANCH_NAME="$(git -C "$WORKTREE_DIR" branch --show-current 2>/dev/null || true)" + + echo "Removing worktree '$WORKTREE_NAME'..." + git -C "$REPO_ROOT" worktree remove "$WORKTREE_DIR" + echo " Removed: $WORKTREE_DIR" + + if [[ -n "$BRANCH_NAME" ]]; then + if [[ -t 0 ]]; then + read -r -p " Delete branch '$BRANCH_NAME'? [y/N] " REPLY + echo + else + REPLY="n" + fi + if [[ "$REPLY" == "y" || "$REPLY" == "Y" ]]; then + git -C "$REPO_ROOT" branch -d "$BRANCH_NAME" + echo " Deleted branch: $BRANCH_NAME" + fi + fi + + echo " Done." +done