Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [1.5.0] - 2026-02-25

### Added
- `git wt checkout <branch> [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
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <branch> [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\|path> [name]` | Adopt an external worktree into git-wt management. |
Expand All @@ -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 |
Expand Down Expand Up @@ -288,6 +317,7 @@ source /path/to/git-wt/aliases/git-wt.sh
| `wtcd <name>` | `cd $(git wt path <name>)` | 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 <branch>` | `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 <name>` | `git wt rm` | Remove a worktree |
Expand Down
25 changes: 25 additions & 0 deletions aliases/git-wt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
Expand All @@ -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
Expand Down
158 changes: 157 additions & 1 deletion bin/git-wt
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 <branch> [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)
Expand Down Expand Up @@ -611,6 +758,7 @@ Worktrees are stored in ${DIM}${GIT_WT_HOME}/<repo>/<name>/${RESET}

${BOLD}Usage:${RESET}
git wt new [name] Create a new worktree (auto-names if omitted)
git wt checkout <branch> [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|path> [name] Adopt an external worktree into git-wt
Expand All @@ -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)
Expand All @@ -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}
Expand All @@ -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 "$@" ;;
Expand Down
14 changes: 14 additions & 0 deletions completions/_git-wt
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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' \
Expand All @@ -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 "$@"
14 changes: 12 additions & 2 deletions completions/git-wt.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
Loading