Created
March 27, 2026 20:36
-
-
Save lloyd/bcfee65caae6a461a26376fa65eaac7f to your computer and use it in GitHub Desktop.
Shell helpers for managing git worktrees + Claude Code sessions
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
| # Worktree + Claude management | |
| # Depends on: kraysee (from kraysee.sh) | |
| # ── helpers ─────────────────────────────────────────────────────── | |
| # Detect the default branch for a repo (develop > main > master) | |
| _wt_default_branch() { | |
| local root="${1:-.}" | |
| local branch | |
| branch=$(git -C "$root" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||') | |
| if [ -z "$branch" ]; then | |
| for b in develop main master; do | |
| if git -C "$root" show-ref --verify --quiet "refs/remotes/origin/$b"; then | |
| branch="$b" | |
| break | |
| fi | |
| done | |
| fi | |
| echo "$branch" | |
| } | |
| # ── wtnew ───────────────────────────────────────────────────────── | |
| # wtnew <name> — create a worktree sibling, new branch, and launch claude | |
| wtnew() { | |
| if [ -z "$1" ]; then | |
| echo "Usage: wtnew <name>" | |
| return 1 | |
| fi | |
| local name="$1" | |
| local repo_root | |
| repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || { | |
| echo "Error: not in a git repository"; return 1 | |
| } | |
| local default_branch | |
| default_branch=$(_wt_default_branch "$repo_root") | |
| if [ -z "$default_branch" ]; then | |
| echo "Error: could not determine default branch" | |
| return 1 | |
| fi | |
| local wt_path="${repo_root}.${name}" | |
| if [ -d "$wt_path" ]; then | |
| echo "Error: already exists: $wt_path" | |
| return 1 | |
| fi | |
| echo "Fetching latest..." | |
| git fetch origin "$default_branch" --quiet | |
| echo "Creating worktree at $wt_path (branch '$name' from 'origin/$default_branch')" | |
| git worktree add -b "$name" "$wt_path" "origin/$default_branch" || return 1 | |
| cd "$wt_path" || return 1 | |
| echo "Now in $wt_path on branch $name" | |
| kraysee | |
| } | |
| # ── wtwut ───────────────────────────────────────────────────────── | |
| # wtwut — show all worktrees with git status details | |
| wtwut() { | |
| git rev-parse --show-toplevel &>/dev/null || { | |
| echo "Error: not in a git repository"; return 1 | |
| } | |
| local main_root | |
| main_root=$(git worktree list --porcelain | head -1 | sed 's/worktree //') | |
| local default_branch | |
| default_branch=$(_wt_default_branch "$main_root") | |
| echo "Worktrees for $(basename "$main_root") (default branch: $default_branch):" | |
| echo "" | |
| local wt_path="" branch="" | |
| while IFS= read -r line; do | |
| case "$line" in | |
| "worktree "*) | |
| wt_path="${line#worktree }" | |
| branch="" | |
| ;; | |
| "branch "*) | |
| branch="${line#branch refs/heads/}" | |
| ;; | |
| "") | |
| if [ -n "$wt_path" ]; then | |
| _wtwut_print "$wt_path" "$branch" "$default_branch" "$main_root" | |
| fi | |
| wt_path="" | |
| branch="" | |
| ;; | |
| esac | |
| done < <(git worktree list --porcelain; echo "") | |
| } | |
| _wtwut_print() { | |
| local wt_path="$1" branch="$2" default_branch="$3" main_root="$4" | |
| local changes status_parts=() | |
| # Uncommitted changes | |
| changes=$(git -C "$wt_path" status --porcelain 2>/dev/null | wc -l | tr -d ' ') | |
| if [ "$changes" -gt 0 ]; then | |
| status_parts+=("\033[1;33m${changes} uncommitted\033[0m") | |
| else | |
| status_parts+=("\033[0;32mclean\033[0m") | |
| fi | |
| # Ahead/behind default branch | |
| if [ -n "$default_branch" ] && [ "$branch" != "$default_branch" ]; then | |
| local ab | |
| ab=$(git -C "$wt_path" rev-list --left-right --count "origin/$default_branch...$branch" 2>/dev/null) | |
| if [ -n "$ab" ]; then | |
| local behind_n ahead_n | |
| behind_n=$(echo "$ab" | awk '{print $1}') | |
| ahead_n=$(echo "$ab" | awk '{print $2}') | |
| if [ "$ahead_n" -eq 0 ] 2>/dev/null && [ "$behind_n" -eq 0 ] 2>/dev/null; then | |
| status_parts+=("up to date") | |
| elif [ "$ahead_n" -eq 0 ] 2>/dev/null; then | |
| status_parts+=("\033[0;36mfully merged, ${behind_n} behind\033[0m") | |
| else | |
| local parts="" | |
| [ "$ahead_n" -gt 0 ] 2>/dev/null && parts="${ahead_n} ahead" | |
| [ "$behind_n" -gt 0 ] 2>/dev/null && parts="${parts:+$parts, }${behind_n} behind" | |
| status_parts+=("\033[0;35m${parts}\033[0m") | |
| fi | |
| fi | |
| fi | |
| # Mark if this is the current directory | |
| local marker=" " | |
| if [ "$wt_path" = "$(git rev-parse --show-toplevel 2>/dev/null)" ]; then | |
| marker="*" | |
| fi | |
| local status_str="" | |
| for i in "${!status_parts[@]}"; do | |
| [ "$i" -gt 0 ] && status_str+=", " | |
| status_str+="${status_parts[$i]}" | |
| done | |
| printf " %s %-40s \033[0;34m%-20s\033[0m %b\n" "$marker" "$wt_path" "[$branch]" "$status_str" | |
| # Show compact file changes for dirty worktrees | |
| if [ "$changes" -gt 0 ]; then | |
| git -C "$wt_path" diff --stat=70 --color=always 2>/dev/null | sed '$d' | head -8 | sed 's/^/ /' | |
| git -C "$wt_path" diff --cached --stat=70 --color=always 2>/dev/null | sed '$d' | head -4 | sed 's/^/ /' | |
| local _ut_n | |
| _ut_n=$(git -C "$wt_path" ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ') | |
| [ "$_ut_n" -gt 0 ] && printf " \033[0;32m+%s untracked\033[0m\n" "$_ut_n" | |
| fi | |
| } | |
| # ── wtbye ───────────────────────────────────────────────────────── | |
| # wtbye [name] — remove a worktree (current one if no name given) | |
| wtbye() { | |
| git rev-parse --show-toplevel &>/dev/null || { | |
| echo "Error: not in a git repository"; return 1 | |
| } | |
| local main_root | |
| main_root=$(git worktree list --porcelain | head -1 | sed 's/worktree //') | |
| local wt_path="" branch="" name="" | |
| if [ -n "$1" ]; then | |
| name="$1" | |
| wt_path="${main_root}.${name}" | |
| else | |
| wt_path=$(git rev-parse --show-toplevel 2>/dev/null) | |
| if [ "$wt_path" = "$main_root" ]; then | |
| echo "Error: you're in the main worktree. Provide a name: wtbye <name>" | |
| return 1 | |
| fi | |
| fi | |
| # Verify it's a known worktree | |
| if ! git worktree list --porcelain | grep -q "^worktree ${wt_path}$"; then | |
| echo "Error: no worktree at $wt_path" | |
| return 1 | |
| fi | |
| # Find the branch for this worktree | |
| local in_target=0 | |
| while IFS= read -r line; do | |
| if [ "$line" = "worktree $wt_path" ]; then | |
| in_target=1 | |
| elif [[ "$line" == "worktree "* ]] && [ "$in_target" -eq 1 ]; then | |
| break | |
| elif [ "$in_target" -eq 1 ] && [[ "$line" == "branch "* ]]; then | |
| branch="${line#branch refs/heads/}" | |
| break | |
| fi | |
| done < <(git worktree list --porcelain) | |
| # If we're inside the worktree being removed, move out | |
| if [[ "$(pwd)" == "$wt_path"* ]]; then | |
| echo "Moving to main worktree: $main_root" | |
| cd "$main_root" || return 1 | |
| fi | |
| echo "Removing worktree: $wt_path" | |
| if ! git worktree remove "$wt_path" 2>/dev/null; then | |
| echo "" | |
| printf " \033[1;33mWorktree has uncommitted changes:\033[0m\n" | |
| git -C "$wt_path" diff --stat=80 --color=always 2>/dev/null | sed 's/^/ /' | |
| git -C "$wt_path" diff --cached --stat=80 --color=always 2>/dev/null | sed 's/^/ /' | |
| local _untracked | |
| _untracked=$(git -C "$wt_path" ls-files --others --exclude-standard 2>/dev/null) | |
| if [ -n "$_untracked" ]; then | |
| echo "$_untracked" | head -5 | while IFS= read -r _f; do | |
| printf " \033[0;32m%s\033[0m (new)\n" "$_f" | |
| done | |
| local _ut_count | |
| _ut_count=$(echo "$_untracked" | wc -l | tr -d ' ') | |
| [ "$_ut_count" -gt 5 ] && echo " ... and $((_ut_count - 5)) more untracked" | |
| fi | |
| echo "" | |
| read -rp "Force remove? [y/N] " answer < /dev/tty | |
| if [[ "$answer" =~ ^[Yy]$ ]]; then | |
| git worktree remove --force "$wt_path" || return 1 | |
| else | |
| echo "Aborted." | |
| return 1 | |
| fi | |
| fi | |
| if [ -n "$branch" ]; then | |
| if git branch -d "$branch" 2>/dev/null; then | |
| echo "Deleted branch: $branch" | |
| else | |
| read -rp "Branch '$branch' not fully merged. Force delete? [y/N] " answer < /dev/tty | |
| if [[ "$answer" =~ ^[Yy]$ ]]; then | |
| git branch -D "$branch" | |
| echo "Force-deleted branch: $branch" | |
| else | |
| echo "Kept branch: $branch" | |
| fi | |
| fi | |
| fi | |
| echo "Done." | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment