Created
March 17, 2026 01:34
-
-
Save kevinold/b7df076b728cd1c1dd50a21b68fed171 to your computer and use it in GitHub Desktop.
gh-worktree
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # gh-worktree - Create or delete a git worktree from a GitHub issue | |
| # Requires: gh CLI (authenticated) | |
| set -e | |
| usage() { | |
| echo "Usage: gh-worktree [-t type] [issue_number] [base_branch]" | |
| echo " gh-worktree -d <issue_number>" | |
| echo "" | |
| echo "Options:" | |
| echo " -t, --type TYPE Branch type: feat, fix, chore (default: auto-detect from labels)" | |
| echo " -d, --delete Delete the worktree for the given issue" | |
| echo " --no-claude Don't launch claude after creating worktree" | |
| echo " -h, --help Show this help message" | |
| echo "" | |
| echo "Arguments:" | |
| echo " issue_number GitHub issue number (optional — omit to create temp worktree)" | |
| echo " base_branch Branch to base worktree on (default: staging)" | |
| echo "" | |
| echo "Examples:" | |
| echo " gh-worktree 226 staging # Named worktree from issue #226 off staging" | |
| echo " gh-worktree staging # Temp worktree off staging, name later" | |
| echo " gh-worktree # Temp worktree off staging (default)" | |
| echo "" | |
| echo "Branch naming: {type}-{issue}-{short-description}" | |
| echo " Example: feat-42-add-dark-mode" | |
| } | |
| DELETE=false | |
| BRANCH_TYPE="" | |
| LAUNCH_CLAUDE=true | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -d|--delete) | |
| DELETE=true | |
| shift | |
| ;; | |
| -t|--type) | |
| BRANCH_TYPE="$2" | |
| shift 2 | |
| ;; | |
| --no-claude) | |
| LAUNCH_CLAUDE=false | |
| shift | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| -*) | |
| echo "Unknown option: $1" | |
| usage | |
| exit 1 | |
| ;; | |
| *) | |
| if [ -z "$FIRST_ARG" ]; then | |
| FIRST_ARG="$1" | |
| elif [ -z "$SECOND_ARG" ]; then | |
| SECOND_ARG="$1" | |
| fi | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Determine if first arg is an issue number or a branch name | |
| if [[ "$FIRST_ARG" =~ ^[0-9]+$ ]]; then | |
| ISSUE_NUMBER="$FIRST_ARG" | |
| BASE_BRANCH="${SECOND_ARG:-staging}" | |
| else | |
| # No issue number — first arg is base branch (or empty) | |
| ISSUE_NUMBER="" | |
| BASE_BRANCH="${FIRST_ARG:-staging}" | |
| fi | |
| REPO_ROOT=$(git rev-parse --show-toplevel) | |
| REPO_NAME=$(basename "$REPO_ROOT") | |
| WORKTREE_BASE=$(dirname "$REPO_ROOT") | |
| # Handle delete | |
| if [ "$DELETE" = true ]; then | |
| if [ -z "$ISSUE_NUMBER" ]; then | |
| echo "Delete requires an issue number: gh-worktree -d <issue_number>" | |
| exit 1 | |
| fi | |
| WORKTREE_DIR="${WORKTREE_BASE}/${REPO_NAME}-wt-${ISSUE_NUMBER}" | |
| if [ ! -d "$WORKTREE_DIR" ]; then | |
| echo "Worktree not found: $WORKTREE_DIR" | |
| exit 1 | |
| fi | |
| echo "Removing worktree: $WORKTREE_DIR" | |
| git worktree remove "$WORKTREE_DIR" | |
| echo "Done! Worktree removed." | |
| exit 0 | |
| fi | |
| if [ -n "$ISSUE_NUMBER" ]; then | |
| # Mode 1: Issue known — named worktree | |
| WORKTREE_DIR="${WORKTREE_BASE}/${REPO_NAME}-wt-${ISSUE_NUMBER}" | |
| ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --json title,labels 2>/dev/null) | |
| if [ -z "$ISSUE_JSON" ]; then | |
| echo "Could not fetch issue #$ISSUE_NUMBER. Check that:" | |
| echo " - You are in a GitHub repository" | |
| echo " - The issue number is valid" | |
| echo " - gh is authenticated (run: gh auth status)" | |
| exit 1 | |
| fi | |
| ISSUE_TITLE=$(echo "$ISSUE_JSON" | jq -r '.title') | |
| LABELS=$(echo "$ISSUE_JSON" | jq -r '.labels[].name' 2>/dev/null | tr '[:upper:]' '[:lower:]') | |
| # Auto-detect branch type from labels if not specified | |
| if [ -z "$BRANCH_TYPE" ]; then | |
| if echo "$LABELS" | grep -qiE '^bug$|^bugfix$|^fix$'; then | |
| BRANCH_TYPE="fix" | |
| elif echo "$LABELS" | grep -qiE '^chore$|^maintenance$|^dependencies$'; then | |
| BRANCH_TYPE="chore" | |
| else | |
| BRANCH_TYPE="feat" | |
| fi | |
| fi | |
| # Generate branch name: {type}-{issue}-{short-description} | |
| SLUG=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '-' | sed 's/^-//;s/-$//' | cut -c1-50) | |
| BRANCH_NAME="${BRANCH_TYPE}-${ISSUE_NUMBER}-${SLUG}" | |
| echo "Creating worktree:" | |
| echo " Issue: #$ISSUE_NUMBER - $ISSUE_TITLE" | |
| echo " Branch: $BRANCH_NAME" | |
| echo " Base: $BASE_BRANCH" | |
| echo " Directory: $WORKTREE_DIR" | |
| else | |
| # Mode 2: No issue — temp worktree | |
| SHORT_ID=$(openssl rand -hex 2) | |
| BRANCH_NAME="wt-tmp-${SHORT_ID}" | |
| WORKTREE_DIR="${WORKTREE_BASE}/${REPO_NAME}-wt-tmp-${SHORT_ID}" | |
| echo "Creating temp worktree:" | |
| echo " Branch: $BRANCH_NAME" | |
| echo " Base: $BASE_BRANCH" | |
| echo " Directory: $WORKTREE_DIR" | |
| echo "" | |
| echo " To rename after creating an issue:" | |
| echo " git branch -m <new-branch-name>" | |
| fi | |
| echo "" | |
| git fetch origin "$BASE_BRANCH" | |
| git worktree add "$WORKTREE_DIR" -b "$BRANCH_NAME" "origin/$BASE_BRANCH" | |
| echo "" | |
| if [ "$LAUNCH_CLAUDE" = true ]; then | |
| echo "Launching claude in $WORKTREE_DIR..." | |
| cd "$WORKTREE_DIR" && exec claude | |
| else | |
| echo "Done! To start working:" | |
| echo " cd $WORKTREE_DIR" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment