diff --git a/CHANGELOG.md b/CHANGELOG.md index 468fb7d..6bc6908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.5.0] - 2026-02-25 + +### Added +- `git wt checkout [name]` — check out an existing local or remote branch into a worktree +- Auto-detects remote tracking branches (e.g., `git wt checkout fix/bug-42` finds `origin/fix/bug-42`) +- Explicit remote refs supported (e.g., `git wt checkout origin/fix/bug-42`) +- `co` alias for `checkout` (like `git checkout` → `git co`) +- `wtco` shell alias — checkout + cd in one step +- `--copy-env` and `--copy-ai` flags supported for checkout + ## [1.4.0] - 2026-02-25 ### Added diff --git a/README.md b/README.md index 638d5cf..3d35ee3 100644 --- a/README.md +++ b/README.md @@ -112,11 +112,33 @@ git wt adopt ../my-hotfix # or by path After `adopt`, the worktree is managed by git-wt like any other. +### Checking out existing branches + +Use `checkout` when the branch already exists (locally or on a remote): + +```bash +# Local branch → worktree +git wt checkout feature/login + +# Remote branch → creates local tracking branch → worktree +git wt checkout origin/fix/bug-42 + +# Branch on remote, auto-detected (no prefix needed) +git wt checkout fix/bug-42 + +# Custom worktree name +git wt checkout feature/login my-login-fix +``` + +Worktree names are derived from the branch name (slashes become dashes, remote prefix stripped). +Use `git wt new` when you need a **new** branch. + ## Commands | Command | Description | |---------|-------------| | `git wt new [name]` | Create a new worktree. Auto-generates a name if omitted. | +| `git wt checkout [name]` | Check out an existing branch into a worktree. | | `git wt list` | List all worktrees for the current repo (managed + external). | | `git wt list-all` | List managed worktrees across **all** repos. | | `git wt adopt [name]` | Adopt an external worktree into git-wt management. | @@ -140,6 +162,13 @@ Options for `git wt new`: | `--copy-env` | Copy `.env*` files from the repo root into the worktree | | `--copy-ai` | Copy AI agent configs and save sessions on rm | +Options for `git wt checkout`: + +| Flag | Description | +|------|-------------| +| `--copy-env` | Copy `.env*` files from the repo root into the worktree | +| `--copy-ai` | Copy AI agent configs and save sessions on rm | + Global flags: | Flag | Description | @@ -288,6 +317,7 @@ source /path/to/git-wt/aliases/git-wt.sh | `wtcd ` | `cd $(git wt path )` | cd into a worktree | | `wto` | `cd $(git wt origin)` | cd into the origin (main) repo | | `wtn [name]` | `git wt new` + `cd` | Create worktree and cd into it | +| `wtco ` | `git wt checkout` + `cd` | Checkout existing branch and cd into it | | `wtls` | `git wt list` | List worktrees | | `wtla` | `git wt list-all` | List all worktrees | | `wtrm ` | `git wt rm` | Remove a worktree | diff --git a/aliases/git-wt.sh b/aliases/git-wt.sh index 6974f0b..0123112 100644 --- a/aliases/git-wt.sh +++ b/aliases/git-wt.sh @@ -36,6 +36,23 @@ wtn() { [[ -n "${wt_path:-}" ]] && cd "$wt_path" || return 1 } +# Check out an existing branch into a worktree and cd into it +# wtco feature/login +# wtco origin/fix/bug-42 +# wtco feature/login my-name +wtco() { + local output line wt_path + output=$(git wt checkout "$@") || return 1 + echo "$output" + while IFS= read -r line; do + if [[ "$line" == *"Path:"* ]]; then + read -r wt_path <<< "${line##*Path:}" + break + fi + done <<< "$output" + [[ -n "${wt_path:-}" ]] && cd "$wt_path" || return 1 +} + # List worktrees (current repo) alias wtls='git wt list' @@ -59,11 +76,13 @@ if [[ -n "${ZSH_VERSION:-}" ]]; then # zsh: reuse git-wt completions for worktree-name arguments _wtcd() { compadd -- $(git wt _names 2>/dev/null); } _wtn() { _arguments '*:branch:_git_branch_names'; } + _wtco() { compadd -- $(git branch --all --format='%(refname:short)' 2>/dev/null); } _wtrm() { compadd -- $(git wt _names 2>/dev/null); } _wtopen() { compadd -- $(git wt _names 2>/dev/null); } _wtpath() { compadd -- $(git wt _names 2>/dev/null); } compdef _wtcd wtcd + compdef _wtco wtco compdef _wtrm wtrm compdef _wtopen wtopen compdef _wtpath wtpath @@ -74,7 +93,13 @@ elif [[ -n "${BASH_VERSION:-}" ]]; then COMPREPLY=( $(compgen -W "$(git wt _names 2>/dev/null)" -- "$cur") ) } + _wt_branch_complete() { + local cur="${COMP_WORDS[COMP_CWORD]}" + COMPREPLY=( $(compgen -W "$(git branch --all --format='%(refname:short)' 2>/dev/null)" -- "$cur") ) + } + complete -F _wt_alias_complete wtcd + complete -F _wt_branch_complete wtco complete -F _wt_alias_complete wtrm complete -F _wt_alias_complete wtopen complete -F _wt_alias_complete wtpath diff --git a/bin/git-wt b/bin/git-wt index 409ed29..ffceee3 100755 --- a/bin/git-wt +++ b/bin/git-wt @@ -8,7 +8,7 @@ # shellcheck disable=SC2059 # intentional: ANSI color codes in printf format strings set -euo pipefail -GIT_WT_VERSION="1.3.0" +GIT_WT_VERSION="1.5.0" # --- Config --- GIT_WT_HOME="${GIT_WT_HOME:-${HOME}/.git-wt}" @@ -92,6 +92,68 @@ current_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "HEAD" } +# Convert branch name to worktree directory name. +# Strips remote prefix, replaces slashes with dashes. +# origin/fix/bug-42 → fix-bug-42 +# feature/login → feature-login +branch_to_wt_name() { + local branch="$1" + local remote + for remote in $(git remote 2>/dev/null); do + if [[ "$branch" == "${remote}/"* ]]; then + branch="${branch#"${remote}/"}" + break + fi + done + echo "${branch//\//-}" +} + +# Resolve a branch for checkout. Returns type:local_branch:remote_ref +# Types: local (exists in refs/heads), remote (explicit origin/... ref), auto (found on one remote) +resolve_checkout_branch() { + local input="$1" + + # Case 1: Explicit remote ref (e.g., origin/fix/bug-42) + local remote + for remote in $(git remote 2>/dev/null); do + if [[ "$input" == "${remote}/"* ]]; then + local local_branch="${input#"${remote}/"}" + if ! git show-ref --verify --quiet "refs/remotes/${input}"; then + die "remote branch '${input}' not found" + fi + # If local tracking branch already exists, use it directly + if git show-ref --verify --quiet "refs/heads/${local_branch}"; then + echo "local:${local_branch}:" + else + echo "remote:${local_branch}:${input}" + fi + return 0 + fi + done + + # Case 2: Local branch exists + if git show-ref --verify --quiet "refs/heads/${input}"; then + echo "local:${input}:" + return 0 + fi + + # Case 3: Search on all remotes + local matches=() + local ref + while IFS= read -r ref; do + [[ -n "$ref" ]] && matches+=("$ref") + done < <(git for-each-ref --format='%(refname:short)' "refs/remotes/*/${input}") + + if [[ ${#matches[@]} -eq 1 ]]; then + echo "auto:${input}:${matches[0]}" + return 0 + elif [[ ${#matches[@]} -gt 1 ]]; then + die "branch '${input}' exists on multiple remotes: ${matches[*]}\nSpecify the remote explicitly, e.g., origin/${input}" + fi + + die "branch '${input}' not found (checked local branches and remotes)" +} + # Resolve a worktree name or path to its absolute path. # Checks: managed name → absolute path → external basename match. resolve_wt_path() { @@ -314,6 +376,91 @@ cmd_new() { printf " ${DIM}cd %s${RESET}\n" "${wt_path}" } +cmd_checkout() { + local branch="" + local name="" + local positional=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --copy-env) WT_COPY_ENV=true; shift ;; + --copy-ai) WT_COPY_AI=true; shift ;; + -*) die "unknown option: $1" ;; + *) positional+=("$1"); shift ;; + esac + done + + branch="${positional[0]:-}" + if [[ -z "$branch" ]]; then + die "usage: git wt checkout [name]" + fi + name="${positional[1]:-$(branch_to_wt_name "$branch")}" + + local wt_root + wt_root=$(resolve_wt_dir) + local wt_path="${wt_root}/${name}" + + if [[ -d "$wt_path" ]]; then + die "worktree '${name}' already exists at ${wt_path}" + fi + + # Resolve branch: local, remote-explicit, or auto-detect + local resolution + resolution=$(resolve_checkout_branch "$branch") + local res_type="${resolution%%:*}" + local rest="${resolution#*:}" + local local_branch="${rest%%:*}" + local remote_ref="${rest#*:}" + + mkdir -p "$wt_root" + + case "$res_type" in + local) + info "Checking out branch '${local_branch}' into worktree '${name}'..." + git worktree add "$wt_path" "$local_branch" + ;; + remote) + info "Checking out '${remote_ref}' into worktree '${name}' (tracking branch '${local_branch}')..." + git worktree add --track -b "$local_branch" "$wt_path" "$remote_ref" + ;; + auto) + info "Checking out branch '${local_branch}' into worktree '${name}' (from ${remote_ref})..." + git worktree add "$wt_path" "$local_branch" + ;; + esac + + # Copy env files if requested + if $WT_COPY_ENV; then + local repo_root + repo_root=$(get_repo_root) + local copied=0 + for envfile in "${repo_root}"/.env*; do + if [[ -f "$envfile" ]]; then + cp "$envfile" "$wt_path/" + $WT_QUIET || printf " Copied %s\n" "$(basename "$envfile")" + copied=$((copied + 1)) + fi + done + if [[ $copied -eq 0 ]]; then + warn "no .env* files found to copy" + fi + fi + + # Copy AI agent configs if requested + if [[ "$WT_COPY_AI" == "true" ]]; then + local ai_origin + ai_origin=$(get_repo_root) + _ai_copy_all "$ai_origin" "$wt_path" + fi + + echo "" + printf "${BOLD}Worktree ready:${RESET}\n" + printf " Path: %s\n" "${wt_path}" + printf " Branch: %s\n" "${local_branch}" + echo "" + printf " ${DIM}cd %s${RESET}\n" "${wt_path}" +} + cmd_list() { local wt_root wt_root=$(resolve_wt_dir) @@ -611,6 +758,7 @@ Worktrees are stored in ${DIM}${GIT_WT_HOME}///${RESET} ${BOLD}Usage:${RESET} git wt new [name] Create a new worktree (auto-names if omitted) + git wt checkout [name] Check out existing branch into a worktree git wt list List worktrees for current repo git wt list-all List managed worktrees across all repos git wt adopt [name] Adopt an external worktree into git-wt @@ -627,6 +775,10 @@ ${BOLD}Options for new:${RESET} --copy-env Copy .env* files into new worktree --copy-ai Copy AI agent configs and save sessions on rm +${BOLD}Options for checkout:${RESET} + --copy-env Copy .env* files into worktree + --copy-ai Copy AI agent configs and save sessions on rm + ${BOLD}Environment:${RESET} GIT_WT_HOME Worktrees root (default: ~/.git-wt) GIT_WT_PREFIX Default branch prefix (default: wt) @@ -641,6 +793,9 @@ ${BOLD}Examples:${RESET} git wt new -b main hotfix ${DIM}# fork from main${RESET} git wt new --copy-env experiment ${DIM}# also copies .env files${RESET} git wt new --copy-ai feature ${DIM}# copies AI configs, saves sessions on rm${RESET} + git wt checkout feature/login ${DIM}# existing branch → worktree${RESET} + git wt checkout origin/fix/bug ${DIM}# remote → tracking branch → worktree${RESET} + git wt checkout fix/api my-fix ${DIM}# custom worktree name${RESET} cd \$(git wt path my-feature) ${DIM}# jump into worktree${RESET} git wt open my-feature ${DIM}# open in Cursor/VS Code${RESET} git wt adopt ../my-hotfix ${DIM}# adopt external worktree${RESET} @@ -664,6 +819,7 @@ shift || true case "$command" in new) cmd_new "$@" ;; + checkout|co) cmd_checkout "$@" ;; list|ls) cmd_list "$@" ;; list-all) cmd_list_all "$@" ;; rm|remove) cmd_rm "$@" ;; diff --git a/completions/_git-wt b/completions/_git-wt index cf34775..e4e2a4f 100644 --- a/completions/_git-wt +++ b/completions/_git-wt @@ -5,6 +5,7 @@ _git-wt() { local -a subcommands subcommands=( 'new:Create a new worktree' + 'checkout:Check out existing branch into a worktree' 'list:List worktrees for current repo' 'list-all:List managed worktrees across all repos' 'adopt:Adopt an external worktree into git-wt' @@ -37,6 +38,13 @@ _git-wt() { '--copy-ai[Copy AI agent configs and save sessions on rm]' \ ':name:' ;; + checkout|co) + _arguments \ + '--copy-env[Copy .env files into worktree]' \ + '--copy-ai[Copy AI agent configs and save sessions on rm]' \ + ':branch:__git_branch_names_all' \ + ':name:' + ;; adopt) _arguments \ ':path:_directories' \ @@ -62,4 +70,10 @@ __git_branch_names() { _describe 'branch' branches } +__git_branch_names_all() { + local -a branches + branches=("${(@f)$(git branch --all --format='%(refname:short)' 2>/dev/null)}") + _describe 'branch' branches +} + _git-wt "$@" diff --git a/completions/git-wt.bash b/completions/git-wt.bash index afb4130..c3343c0 100644 --- a/completions/git-wt.bash +++ b/completions/git-wt.bash @@ -12,7 +12,7 @@ _git_wt() { for ((i = 1; i < COMP_CWORD; i++)); do case "${COMP_WORDS[i]}" in -q|--quiet) continue ;; - new|list|list-all|ls|rm|remove|path|cd|origin|open|adopt|clean|help|version|--version|-v) + new|checkout|co|list|list-all|ls|rm|remove|path|cd|origin|open|adopt|clean|help|version|--version|-v) subcmd="${COMP_WORDS[i]}" break ;; @@ -24,7 +24,7 @@ _git_wt() { if [[ "$cur" == -* ]]; then COMPREPLY=($(compgen -W "-q --quiet --help --version" -- "$cur")) else - COMPREPLY=($(compgen -W "new list list-all adopt rm path origin open clean help version" -- "$cur")) + COMPREPLY=($(compgen -W "new checkout list list-all adopt rm path origin open clean help version" -- "$cur")) fi return fi @@ -60,6 +60,16 @@ _git_wt() { ;; esac ;; + checkout|co) + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "--copy-env --copy-ai" -- "$cur")) + else + # Complete with local + remote branch names + local branches + branches=$(git branch --all --format='%(refname:short)' 2>/dev/null) + COMPREPLY=($(compgen -W "$branches" -- "$cur")) + fi + ;; esac } diff --git a/skills/git-wt/SKILL.md b/skills/git-wt/SKILL.md index 7c84dc3..028fb8b 100644 --- a/skills/git-wt/SKILL.md +++ b/skills/git-wt/SKILL.md @@ -30,6 +30,15 @@ git wt new --copy-ai my-feature # Copy AI configs, save sessions on rm git wt new --no-branch scratch # Detached HEAD (no branch created) ``` +### Check out an existing branch +```bash +git wt checkout feature/login # Local branch → worktree +git wt checkout origin/fix/bug-42 # Remote → local tracking branch → worktree +git wt checkout fix/bug-42 # Auto-detects from remote +git wt checkout feature/login my-fix # Custom worktree name +git wt checkout --copy-env feature/api # Also copy .env files +``` + ### Navigate to a worktree ```bash cd $(git wt path ) # Jump into worktree @@ -74,6 +83,7 @@ git wt clean # Remove all managed worktrees for current - **`rm`**: only works on managed worktrees — removes directory and deletes its branch - **`adopt`**: moves an external worktree under `~/.git-wt/` so git-wt fully manages it - **External worktrees**: `list`, `path`, `open` work with worktrees created outside git-wt +- **`checkout`**: checks out an existing branch (local or remote) into a managed worktree — does NOT create new branches - **`--copy-env`**: copies all `.env*` files from repo root (critical for dev servers) - **`--copy-ai`**: copies AI configs on create, archives sessions + syncs settings on rm - **`origin`**: prints main repo path — works from any worktree or main repo itself @@ -102,6 +112,11 @@ Use `git wt new --copy-ai` when: - You want approved commands (`.claude/settings.local.json`) available immediately - You want Claude sessions archived (not lost) when the worktree is removed +Use `git wt checkout` when: +- You need to work on an existing branch in an isolated worktree +- You want to check out a remote branch without manually creating a tracking branch +- You're reviewing someone else's PR branch in isolation + Use `git wt adopt` when: - A worktree was created with `git worktree add` and you want git-wt to manage it - You want the worktree moved to `~/.git-wt/` for consistent management @@ -115,6 +130,7 @@ Optional `aliases/git-wt.sh` provides shorter commands. Source it in `.bashrc`/` | `wtcd ` | `cd` into a worktree | | `wto` | `cd` to the origin (main) repo | | `wtn [name]` | Create worktree + `cd` into it | +| `wtco ` | Checkout existing branch + `cd` into it | | `wtls` / `wtla` | List / list-all | | `wtrm` / `wtopen` / `wtclean` / `wtpath` | Shorthand for corresponding commands |