Skip to content

Instantly share code, notes, and snippets.

@claylevering
Forked from dhh/agent-git-trees.sh
Created December 30, 2025 14:16
Show Gist options
  • Select an option

  • Save claylevering/f6bdbb1d01b141c1144458875494c0b4 to your computer and use it in GitHub Desktop.

Select an option

Save claylevering/f6bdbb1d01b141c1144458875494c0b4 to your computer and use it in GitHub Desktop.
#! /usr/bin/env zsh
#
# Worktree helpers (zsh-only)
#
# --- User configuration -------------------------------------------------------
# Override these in your `.zshrc` before sourcing this file.
#
# Example:
# export GIT_WT_HELPER_IDE_CMD="cursor" # e.g. cursor, code, idea, subl, etc.
# export GIT_WT_HELPER_OPEN_IDE_DEFAULT="false" # true/false (default: false)
: "${GIT_WT_HELPER_IDE_CMD:=}"
: "${GIT_WT_HELPER_OPEN_IDE_DEFAULT:=false}"
_git_wt_helper_ga_is_zsh() { [[ -n "${ZSH_VERSION:-}" ]]; }
_git_wt_helper_ga_log() { print -r -- "$*"; }
_git_wt_helper_ga_err() { print -ru2 -- "$*"; }
_git_wt_helper_ga_is_truthy() {
# Treat 1/true/yes/on as true (case-insensitive)
local v="${1:-}"
v="${v:l}"
[[ "$v" == "1" || "$v" == "true" || "$v" == "yes" || "$v" == "on" ]]
}
_git_wt_helper_ga_safe_path() {
local p="/usr/bin:/bin:/usr/sbin:/sbin"
[[ -n "${PATH:-}" ]] && p="$p:$PATH"
print -r -- "$p"
}
_git_wt_helper_ga_find_cmd() {
# Usage: _git_wt_helper_ga_find_cmd <name> [fallback1] [fallback2] ...
local name="$1"
shift
if command -v "$name" >/dev/null 2>&1; then
command -v "$name"
return 0
fi
local candidate
for candidate in "$@"; do
[[ -x "$candidate" ]] && {
print -r -- "$candidate"
return 0
}
done
return 1
}
_git_wt_helper_ga_require_zsh() {
if ! _git_wt_helper_ga_is_zsh; then
_git_wt_helper_ga_err "Error: this function requires zsh. Current shell: ${SHELL:-unknown}"
return 1
fi
return 0
}
_git_wt_helper_ga_require_cmd() {
# Usage: _git_wt_helper_ga_require_cmd <label> <cmd_path>
local label="$1"
local cmd="$2"
if [[ -z "$cmd" ]]; then
_git_wt_helper_ga_err "Error: $label not found."
return 1
fi
return 0
}
# Create a new worktree and branch from within current git directory.
ga() {
_git_wt_helper_ga_require_zsh || return 1
local copy_env=1
local open_ide=0
local branch=""
if _git_wt_helper_ga_is_truthy "${GIT_WT_HELPER_OPEN_IDE_DEFAULT:-false}"; then
open_ide=1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
-n | --no-env)
copy_env=0
shift
;;
-o | --open-ide)
open_ide=1
shift
;;
--no-open-ide)
open_ide=0
shift
;;
-h | --help)
_git_wt_helper_ga_err "Usage: ga [--no-env] [--open-ide|--no-open-ide] <branch>"
_git_wt_helper_ga_err " --no-env: do not copy .env into the new worktree (default: copy if present)"
_git_wt_helper_ga_err " -o, --open-ide: open your IDE after creating the worktree"
_git_wt_helper_ga_err " --no-open-ide: do not open your IDE (overrides default setting)"
_git_wt_helper_ga_err ""
_git_wt_helper_ga_err "Config:"
_git_wt_helper_ga_err " GIT_WT_HELPER_IDE_CMD: IDE command (e.g. cursor, code). If empty, auto-detect."
_git_wt_helper_ga_err " GIT_WT_HELPER_OPEN_IDE_DEFAULT: true/false (default: false)"
return 0
;;
--)
shift
break
;;
-*)
_git_wt_helper_ga_err "Error: unknown option: $1"
_git_wt_helper_ga_err "Usage: ga [--no-env] [--open-ide|--no-open-ide] <branch>"
return 1
;;
*)
branch="$1"
shift
break
;;
esac
done
if [[ -z "$branch" ]]; then
_git_wt_helper_ga_err "Usage: ga [--no-env] [--open-ide|--no-open-ide] <branch>"
return 1
fi
[[ -x /bin/sh ]] || {
_git_wt_helper_ga_err "Error: /bin/sh not found or not executable."
return 1
}
local safe_path
safe_path="$(_git_wt_helper_ga_safe_path)"
local git_cmd
git_cmd="$(_git_wt_helper_ga_find_cmd git /usr/bin/git /usr/local/bin/git)" || true
_git_wt_helper_ga_require_cmd "git" "$git_cmd" || return 1
local mise_cmd
mise_cmd="$(_git_wt_helper_ga_find_cmd mise "$HOME/.local/bin/mise" /opt/homebrew/bin/mise /usr/local/bin/mise)" || true
if [[ -z "$mise_cmd" ]]; then
_git_wt_helper_ga_err "Error: mise not found."
_git_wt_helper_ga_err "Install: https://mise.jdx.dev/getting-started.html"
return 1
fi
if ! "$git_cmd" rev-parse --git-dir >/dev/null 2>&1; then
_git_wt_helper_ga_err "Error: not in a git repository."
return 1
fi
local base
base="$(basename "$PWD")"
local worktree_path="../${base}-${branch}"
if [[ -d "$worktree_path" ]]; then
_git_wt_helper_ga_err "Error: worktree path already exists: $worktree_path"
return 1
fi
_git_wt_helper_ga_log "Creating worktree '$branch' at '$worktree_path'..."
if ! /usr/bin/env PATH="$safe_path" "$git_cmd" worktree add -b "$branch" "$worktree_path"; then
_git_wt_helper_ga_err "Error: failed to create worktree."
return 1
fi
if [[ "$copy_env" -eq 1 ]]; then
local repo_root
repo_root="$("$git_cmd" rev-parse --show-toplevel 2>/dev/null)"
if [[ -n "$repo_root" && -f "$repo_root/.env" ]]; then
if [[ -e "$worktree_path/.env" ]]; then
_git_wt_helper_ga_log ".env already exists in worktree; skipping copy."
else
_git_wt_helper_ga_log "Copying .env into worktree..."
cp -p "$repo_root/.env" "$worktree_path/.env" 2>/dev/null || _git_wt_helper_ga_err "Warning: failed to copy .env (continuing)."
fi
fi
fi
_git_wt_helper_ga_log "Trusting mise in worktree..."
"$mise_cmd" trust "$worktree_path" >/dev/null 2>&1 || _git_wt_helper_ga_err "Warning: mise trust failed (continuing)."
if cd "$worktree_path"; then
_git_wt_helper_ga_log "Now in: $(pwd)"
else
_git_wt_helper_ga_err "Error: failed to change directory to worktree."
return 1
fi
if [[ "$open_ide" -eq 1 ]]; then
local ide_setting ide_cmd
ide_setting="${GIT_WT_HELPER_IDE_CMD:-}"
# Resolve IDE command.
if [[ -n "$ide_setting" ]]; then
# Allow absolute paths too.
if [[ "$ide_setting" == */* ]]; then
[[ -x "$ide_setting" ]] && ide_cmd="$ide_setting"
else
ide_cmd="$(command -v "$ide_setting" 2>/dev/null)"
fi
else
# Auto-detect common IDE CLI commands.
local candidate
for candidate in cursor code idea subl; do
ide_cmd="$(command -v "$candidate" 2>/dev/null)" && break
done
fi
if [[ -n "$ide_cmd" ]]; then
_git_wt_helper_ga_log "Opening IDE..."
"$ide_cmd" . >/dev/null 2>&1 & disown
else
_git_wt_helper_ga_err "Warning: IDE command not found. Set GIT_WT_HELPER_IDE_CMD (e.g. 'cursor') to enable --open-ide."
fi
fi
}
# Remove worktree and branch from within active worktree directory.
gd() {
_git_wt_helper_ga_require_zsh || return 1
local safe_path
safe_path="$(_git_wt_helper_ga_safe_path)"
local git_cmd
git_cmd="$(_git_wt_helper_ga_find_cmd git /usr/bin/git /usr/local/bin/git)" || true
_git_wt_helper_ga_require_cmd "git" "$git_cmd" || return 1
if ! "$git_cmd" rev-parse --git-dir >/dev/null 2>&1; then
_git_wt_helper_ga_err "Error: not in a git repository."
return 1
fi
local cwd
cwd="$(pwd)"
local branch
branch="$("$git_cmd" rev-parse --abbrev-ref HEAD 2>/dev/null)"
[[ -n "$branch" ]] || {
_git_wt_helper_ga_err "Error: could not determine current branch."
return 1
}
# Worktrees have a .git file (not a directory)
if [[ ! -f "$cwd/.git" ]]; then
_git_wt_helper_ga_err "Error: current directory doesn't appear to be a git worktree."
return 1
fi
# Move to a safe directory BEFORE removing the worktree, so shell hooks (mise/fnm/etc)
# don't run in a deleted directory.
local common_git_dir repo_root
common_git_dir="$("$git_cmd" rev-parse --git-common-dir 2>/dev/null)"
if [[ -n "$common_git_dir" ]]; then
# zsh: :A makes path absolute (resolves relative paths)
common_git_dir="${common_git_dir:A}"
repo_root="$(dirname "$common_git_dir")"
fi
if [[ -z "$repo_root" || ! -d "$repo_root" ]]; then
# Fallback: parent directory of the worktree
repo_root="$(dirname "$cwd")"
fi
if [[ -d "$repo_root" ]]; then
cd "$repo_root" || true
fi
if ! command -v gum >/dev/null 2>&1; then
_git_wt_helper_ga_err "Error: gum not found."
_git_wt_helper_ga_err "Install: https://github.com/charmbracelet/gum?tab=readme-ov-file#installation"
return 1
fi
_git_wt_helper_ga_log "About to remove worktree '$cwd' and delete branch '$branch'."
if ! gum confirm "Remove worktree and branch?"; then
_git_wt_helper_ga_log "Cancelled."
return 0
fi
# Remove worktree (use safe PATH for any internal env/sh usage)
if ! /usr/bin/env PATH="$safe_path" "$git_cmd" worktree remove "$cwd" --force; then
_git_wt_helper_ga_err "Error: failed to remove worktree."
return 1
fi
# Delete branch after worktree removal
if ! /usr/bin/env PATH="$safe_path" "$git_cmd" branch -D "$branch" >/dev/null 2>&1; then
_git_wt_helper_ga_err "Warning: failed to delete branch '$branch'. It may be checked out in another worktree."
_git_wt_helper_ga_err "Tip: run 'git worktree list' to find where it's still in use."
return 1
fi
_git_wt_helper_ga_log "Removed worktree and deleted branch '$branch'."
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment