From b773fb5f68835ddf7ba7aa67291ebb766ed8f6d5 Mon Sep 17 00:00:00 2001 From: "Thing-han, Lim" <15379156+potsrevennil@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:09:28 +0800 Subject: [PATCH 1/6] feat(shell): let git checkout/switch check out GitHub PRs via gh With gh installed, a bare `git checkout|switch` (and omz gco/gsw) of #123, a PR URL, or a number that is neither a rev nor a path lands on the PR's exact head commit: stay put if HEAD is already there, else switch to (or cd into the worktree of) a local branch whose tip is that commit, preferring the PR's branch name. Only when no branch carries it does `gh pr checkout` create one, named after the PR's branch unless that name is taken (then pr-, then pr--), so a local branch that has moved ahead of the PR is never reused. Co-Authored-By: Claude Opus 5.5 --- modules/shells/zsh/zshrc | 64 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/modules/shells/zsh/zshrc b/modules/shells/zsh/zshrc index cfbfc02..c6d4bb0 100644 --- a/modules/shells/zsh/zshrc +++ b/modules/shells/zsh/zshrc @@ -60,6 +60,10 @@ alias fman="compgen -c | fzf | xargs man" # git worktree path print the worktree that has checked out # git checkout|switch cd into that worktree instead of failing with # "already used by worktree"; also covers omz gco/gsw +# git checkout|switch with gh: #123, a PR URL, or a bare number that is not a +# rev or path; lands on the PR's exact head commit, in the +# worktree or local branch whose tip it is, else on a new +# branch from `gh pr checkout` # Sets REPLY to the worktree of branch $1; one `git worktree list`, parsed in-shell. _git_worktree_of() { local line wt @@ -72,6 +76,54 @@ _git_worktree_of() { return 1 } +# Sets REPLY to the gh PR selector named by $1, if gh is installed. +_git_pr_of() { + (( $+commands[gh] )) || return 1 + case $1 in + '#'<->) REPLY=${1#\#} ;; + https://*/pull/<->|https://*/pull/<->/*) REPLY=$1 ;; + <->) [[ ! -e $1 ]] && ! command git rev-parse -q --verify "$1^{commit}" >/dev/null || return 1 + REPLY=$1 ;; + *) return 1 ;; + esac +} + +# Land on PR $1's exact head commit: HEAD if already there, else the local branch whose +# tip it is (the PR's branch name first), cd-ing to its worktree if it has +# one; with no such branch, `gh pr checkout` into a new branch that does not clobber one. +_git_pr_checkout() { + local -a pr at + local line name + pr=(${(f)"$(gh pr view $1 --json number,headRefOid,headRefName -q '.number, .headRefOid, .headRefName')"}) + (( $#pr == 3 )) || return 1 + local num=$pr[1] oid=$pr[2] head=$pr[3] + if [[ $(command git rev-parse -q --verify HEAD) == $oid ]]; then + print -u2 "Already on PR #$num ($oid)" + return + fi + for line in ${(f)"$(command git for-each-ref --format='%(objectname) %(refname:short)' refs/heads)"}; do + [[ ${line%% *} == $oid ]] && at+=(${line#* }) + done + if (( $#at )); then + name=${at[(r)$head]:-$at[1]} + _git_jump_to_worktree $name && return + command git switch $name + return + fi + for name in $head pr-$num pr-$num-${oid[1,7]}; do + command git show-ref -q --verify refs/heads/$name || break + done + gh pr checkout $1 --branch $name +} + +# cd into the worktree holding branch $1, unless it is already the current one. +_git_jump_to_worktree() { + _git_worktree_of $1 && [[ -d $REPLY ]] && + [[ $(command git symbolic-ref -q --short HEAD) != $1 ]] || return 1 + print -u2 "Switched to worktree '$REPLY'" + cd $REPLY +} + git() { local REPLY case "$1:$2" in @@ -80,13 +132,13 @@ git() { _git_worktree_of $3 && print -r -- $REPLY return ;; checkout:[^-]*|switch:[^-]*) - # Only a bare `git checkout `; flags, paths and `-` go straight to git. - if (( $# == 2 )) && _git_worktree_of $2 && [[ -d $REPLY ]] && - [[ $(command git symbolic-ref -q --short HEAD) != $2 ]]; then - print -u2 "Switched to worktree '$REPLY'" - cd $REPLY + # Only a bare `git checkout `; flags, paths and `-` go straight to git. + (( $# == 2 )) || { command git "$@"; return } + if _git_pr_of $2; then + _git_pr_checkout $REPLY return - fi ;; + fi + _git_jump_to_worktree $2 && return ;; esac command git "$@" } From 2f5a7a1c98c051e5d3bbcce63b85f5cce5fa3b27 Mon Sep 17 00:00:00 2001 From: "Thing-han, Lim" <15379156+potsrevennil@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:20:43 +0800 Subject: [PATCH 2/6] feat(shell): switch branches in the main checkout by default A bare `git checkout|switch ` from any worktree now switches the main checkout and cds there, instead of jumping to whichever worktree already has the branch; `-H` does it in the current worktree. A linked worktree holding the branch is detached at the same commit to free it, refused if it has uncommitted changes, and reattached if the switch then fails. Commits, tags, paths and other flags still go straight to git. Co-Authored-By: Claude Opus 5.5 --- modules/shells/zsh/zshrc | 113 ++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 30 deletions(-) diff --git a/modules/shells/zsh/zshrc b/modules/shells/zsh/zshrc index c6d4bb0..2335221 100644 --- a/modules/shells/zsh/zshrc +++ b/modules/shells/zsh/zshrc @@ -57,13 +57,17 @@ alias fman="compgen -c | fzf | xargs man" # Worktree-aware git (zsh only: git has no hook for new `worktree` subcommands or for # changing `checkout`/`switch`, and a git alias cannot cd the shell). -# git worktree path print the worktree that has checked out -# git checkout|switch cd into that worktree instead of failing with -# "already used by worktree"; also covers omz gco/gsw -# git checkout|switch with gh: #123, a PR URL, or a bare number that is not a -# rev or path; lands on the PR's exact head commit, in the -# worktree or local branch whose tip it is, else on a new -# branch from `gh pr checkout` +# git worktree path print the worktree that has checked out +# git checkout|switch switch the main checkout to and cd there, from +# any worktree; also covers omz gco/gsw +# git checkout|switch -H the same, in the current worktree instead +# git checkout|switch [-H] with gh: #123, a PR URL, or a bare number that is not a +# rev or path; lands on the PR's exact head commit, on the +# local branch whose tip it is, else on a new branch from +# `gh pr checkout` +# A linked worktree that already holds the branch is detached (same commit) to free it, +# but only if it has no uncommitted changes. Anything not naming a local or remote branch +# (commits, tags, paths, `-`, other flags) goes straight to git. # Sets REPLY to the worktree of branch $1; one `git worktree list`, parsed in-shell. _git_worktree_of() { local line wt @@ -76,6 +80,18 @@ _git_worktree_of() { return 1 } +# Sets REPLY to the main checkout, or the current worktree if there is none (bare repo, +# submodule). +_git_main_worktree() { + local common + common=$(command git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return + if [[ $common == */.git ]]; then + REPLY=${common:h} + else + REPLY=$(command git rev-parse --show-toplevel) || return + fi +} + # Sets REPLY to the gh PR selector named by $1, if gh is installed. _git_pr_of() { (( $+commands[gh] )) || return 1 @@ -88,57 +104,94 @@ _git_pr_of() { esac } -# Land on PR $1's exact head commit: HEAD if already there, else the local branch whose -# tip it is (the PR's branch name first), cd-ing to its worktree if it has -# one; with no such branch, `gh pr checkout` into a new branch that does not clobber one. +# Whether $1 is a local branch, or a remote one `git switch` would create it from. +_git_is_branch() { + command git show-ref -q --verify refs/heads/$1 2>/dev/null || + [[ -n $(command git for-each-ref --count=1 "refs/remotes/*/$1" 2>/dev/null) ]] +} + +# cd to worktree $1 unless already in it. +_git_enter() { + [[ ${1:A} == ${$(command git rev-parse --show-toplevel 2>/dev/null):A} ]] && return + print -u2 "Switched to worktree '$1'" + cd $1 +} + +# Run `git $2 $3` in worktree $1 and cd there. If another worktree holds branch $3, detach +# it first (refusing when it is dirty), and reattach it if the switch fails. +_git_switch_in() { + local dir=$1 cmd=$2 branch=$3 holder + _git_worktree_of $branch && holder=$REPLY + if [[ -n $holder && ${holder:A} != ${dir:A} ]]; then + if [[ -n $(command git -C $holder status --porcelain 2>/dev/null) ]]; then + print -u2 "'$branch' is checked out in '$holder', which has uncommitted changes; leaving it" + return 1 + fi + command git -C $holder switch -q --detach || return + print -u2 "Detached '$holder' to free '$branch'" + if ! command git -C $dir $cmd $branch; then + command git -C $holder switch -q $branch && print -u2 "Reattached '$holder' to '$branch'" + return 1 + fi + elif [[ -z $holder ]]; then + command git -C $dir $cmd $branch || return + fi + _git_enter $dir +} + +# Land worktree $1 on PR $3's exact head commit, via `git $2`: stay if its HEAD is already +# there, else switch to the local branch whose tip it is (the PR's branch name first); with +# no such branch, `gh pr checkout` into a new branch that does not clobber one. _git_pr_checkout() { + local dir=$1 cmd=$2 local -a pr at local line name - pr=(${(f)"$(gh pr view $1 --json number,headRefOid,headRefName -q '.number, .headRefOid, .headRefName')"}) + pr=(${(f)"$(gh pr view $3 --json number,headRefOid,headRefName -q '.number, .headRefOid, .headRefName')"}) (( $#pr == 3 )) || return 1 local num=$pr[1] oid=$pr[2] head=$pr[3] - if [[ $(command git rev-parse -q --verify HEAD) == $oid ]]; then + if [[ $(command git -C $dir rev-parse -q --verify HEAD) == $oid ]]; then print -u2 "Already on PR #$num ($oid)" + _git_enter $dir return fi for line in ${(f)"$(command git for-each-ref --format='%(objectname) %(refname:short)' refs/heads)"}; do [[ ${line%% *} == $oid ]] && at+=(${line#* }) done if (( $#at )); then - name=${at[(r)$head]:-$at[1]} - _git_jump_to_worktree $name && return - command git switch $name + _git_switch_in $dir $cmd ${at[(r)$head]:-$at[1]} return fi for name in $head pr-$num pr-$num-${oid[1,7]}; do command git show-ref -q --verify refs/heads/$name || break done - gh pr checkout $1 --branch $name -} - -# cd into the worktree holding branch $1, unless it is already the current one. -_git_jump_to_worktree() { - _git_worktree_of $1 && [[ -d $REPLY ]] && - [[ $(command git symbolic-ref -q --short HEAD) != $1 ]] || return 1 - print -u2 "Switched to worktree '$REPLY'" - cd $REPLY + ( builtin cd -q $dir && gh pr checkout $3 --branch $name ) && _git_enter $dir } git() { - local REPLY + local REPLY dir case "$1:$2" in worktree:path) (( $# == 3 )) || { print -u2 'usage: git worktree path '; return 129 } _git_worktree_of $3 && print -r -- $REPLY return ;; + checkout:-H|switch:-H) + (( $# == 3 )) && dir=$(command git rev-parse --show-toplevel 2>/dev/null) || + { command git $1 $@[3,-1]; return } ;& checkout:[^-]*|switch:[^-]*) - # Only a bare `git checkout `; flags, paths and `-` go straight to git. - (( $# == 2 )) || { command git "$@"; return } - if _git_pr_of $2; then - _git_pr_checkout $REPLY + # Only a bare `git checkout [-H] `; anything else goes straight to git. + if [[ -z $dir ]]; then + (( $# == 2 )) && _git_main_worktree || { command git "$@"; return } + dir=$REPLY + fi + if _git_pr_of $@[-1]; then + _git_pr_checkout $dir $1 $REPLY return fi - _git_jump_to_worktree $2 && return ;; + if _git_is_branch $@[-1]; then + _git_switch_in $dir $1 $@[-1] + return + fi ;; esac + [[ $1 == (checkout|switch) && $2 == -H ]] && set -- $1 $@[3,-1] command git "$@" } From ed37fa0806bb0e9c33b35961a6a593571f99d1be Mon Sep 17 00:00:00 2001 From: "Thing-han, Lim" <15379156+potsrevennil@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:10:06 +0800 Subject: [PATCH 3/6] refactor(shell): spell the current-worktree flag `-.`, before or after `git checkout dev -.` (or `-. dev`) switches the current worktree instead of the main checkout. Unlike a bare `.`, which git reads as a pathspec and would restore files with if the wrapper were not loaded, `-.` is rejected by git itself. Co-Authored-By: Claude Opus 5.5 --- modules/shells/zsh/zshrc | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/modules/shells/zsh/zshrc b/modules/shells/zsh/zshrc index 2335221..8c4d33e 100644 --- a/modules/shells/zsh/zshrc +++ b/modules/shells/zsh/zshrc @@ -60,8 +60,9 @@ alias fman="compgen -c | fzf | xargs man" # git worktree path print the worktree that has checked out # git checkout|switch switch the main checkout to and cd there, from # any worktree; also covers omz gco/gsw -# git checkout|switch -H the same, in the current worktree instead -# git checkout|switch [-H] with gh: #123, a PR URL, or a bare number that is not a +# git checkout|switch -. the same, in the current worktree instead (`-.` may +# also come first; git itself rejects it, so it is safe) +# git checkout|switch [-.] with gh: #123, a PR URL, or a bare number that is not a # rev or path; lands on the PR's exact head commit, on the # local branch whose tip it is, else on a new branch from # `gh pr checkout` @@ -168,30 +169,33 @@ _git_pr_checkout() { } git() { - local REPLY dir + local REPLY dir here + if [[ $1 == (checkout|switch) ]] && (( $@[(I)-.] )); then + here=1 + set -- ${@:#-.} + fi case "$1:$2" in worktree:path) (( $# == 3 )) || { print -u2 'usage: git worktree path '; return 129 } _git_worktree_of $3 && print -r -- $REPLY return ;; - checkout:-H|switch:-H) - (( $# == 3 )) && dir=$(command git rev-parse --show-toplevel 2>/dev/null) || - { command git $1 $@[3,-1]; return } ;& checkout:[^-]*|switch:[^-]*) - # Only a bare `git checkout [-H] `; anything else goes straight to git. - if [[ -z $dir ]]; then - (( $# == 2 )) && _git_main_worktree || { command git "$@"; return } + # Only a bare `git checkout [-.]`; anything else goes straight to git. + (( $# == 2 )) || { command git "$@"; return } + if (( here )); then + dir=$(command git rev-parse --show-toplevel 2>/dev/null) + elif _git_main_worktree; then dir=$REPLY fi - if _git_pr_of $@[-1]; then + [[ -n $dir ]] || { command git "$@"; return } + if _git_pr_of $2; then _git_pr_checkout $dir $1 $REPLY return fi - if _git_is_branch $@[-1]; then - _git_switch_in $dir $1 $@[-1] + if _git_is_branch $2; then + _git_switch_in $dir $1 $2 return fi ;; esac - [[ $1 == (checkout|switch) && $2 == -H ]] && set -- $1 $@[3,-1] command git "$@" } From 066666846dc295e3cf05cf8b74f666ba88871152 Mon Sep 17 00:00:00 2001 From: "Thing-han, Lim" <15379156+potsrevennil@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:21:52 +0800 Subject: [PATCH 4/6] fix(shell): keep `#` literal under EXTENDED_GLOB zsh-utils' completion plugin turns on EXTENDED_GLOB, which makes `#` a glob operator, so `nixpkgs#hello` fails with "no matches found" and `gco #47` with "bad pattern". `disable -p '#'` turns off just that operator, keeping the rest of extended globbing; the `(#i)`-style glob flags go with it. Replaces the commented-out `setopt noglob` attempt. Co-Authored-By: Claude Opus 5.5 --- modules/shells/zsh/zshrc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/shells/zsh/zshrc b/modules/shells/zsh/zshrc index 8c4d33e..381b0a7 100644 --- a/modules/shells/zsh/zshrc +++ b/modules/shells/zsh/zshrc @@ -48,8 +48,10 @@ _fzf_comprun() { esac } -# fix catch command not found when executing nix command with "#" -# setopt noglob +# Keep `#` literal (nixpkgs#hello, `gco #47`): zsh-utils' completion plugin turns on +# EXTENDED_GLOB, which makes `#` a glob operator; this keeps the rest (^, ~) but also +# drops the `(#i)`-style glob flags. +disable -p '#' # alias alias ls="ls --color" From 7eeb61a92ab4c964e8f5270ee122788969e6b975 Mon Sep 17 00:00:00 2001 From: "Thing-han, Lim" <15379156+potsrevennil@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:51:13 +0800 Subject: [PATCH 5/6] fix(shell): address review findings on the checkout wrapper - Pass straight to git under Claude Code ($CLAUDECODE): agents' shells snapshot this function and expect plain git in their own worktrees. - Match remote branches exactly (/ for each remote), so a name that only tails a nested remote branch is not sent to the main checkout, where git would read it as a path and restore that file. - Replace `disable -p '#'` with `unsetopt extended_glob`: the former silently broke `##` even in code that sets extendedglob locally. - Ignore untracked files when deciding whether a holder worktree is dirty; detaching at the same commit never touches them. - Checking out the current worktree's own branch stays put instead of detaching it and moving the branch to the main checkout. Co-Authored-By: Claude Opus 5.5 --- modules/shells/zsh/zshrc | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/modules/shells/zsh/zshrc b/modules/shells/zsh/zshrc index 381b0a7..84c3812 100644 --- a/modules/shells/zsh/zshrc +++ b/modules/shells/zsh/zshrc @@ -49,9 +49,9 @@ _fzf_comprun() { } # Keep `#` literal (nixpkgs#hello, `gco #47`): zsh-utils' completion plugin turns on -# EXTENDED_GLOB, which makes `#` a glob operator; this keeps the rest (^, ~) but also -# drops the `(#i)`-style glob flags. -disable -p '#' +# EXTENDED_GLOB globally, which makes `#` a glob operator. Code that needs it (completion, +# p10k, fast-syntax-highlighting) sets it locally, so turning it back off is safe. +unsetopt extended_glob # alias alias ls="ls --color" @@ -69,8 +69,10 @@ alias fman="compgen -c | fzf | xargs man" # local branch whose tip it is, else on a new branch from # `gh pr checkout` # A linked worktree that already holds the branch is detached (same commit) to free it, -# but only if it has no uncommitted changes. Anything not naming a local or remote branch -# (commits, tags, paths, `-`, other flags) goes straight to git. +# but only if it has no uncommitted changes to tracked files; the current worktree's own +# branch stays put. Anything not naming a local or remote branch (commits, tags, paths, +# `-`, other flags) goes straight to git, as does everything under Claude Code +# ($CLAUDECODE), whose agents expect plain git in their worktrees. # Sets REPLY to the worktree of branch $1; one `git worktree list`, parsed in-shell. _git_worktree_of() { local line wt @@ -107,10 +109,14 @@ _git_pr_of() { esac } -# Whether $1 is a local branch, or a remote one `git switch` would create it from. +# Whether $1 is a local branch, or /$1 exists for `git switch` to create it from. _git_is_branch() { - command git show-ref -q --verify refs/heads/$1 2>/dev/null || - [[ -n $(command git for-each-ref --count=1 "refs/remotes/*/$1" 2>/dev/null) ]] + local remote + command git show-ref -q --verify refs/heads/$1 2>/dev/null && return + for remote in ${(f)"$(command git remote 2>/dev/null)"}; do + command git show-ref -q --verify refs/remotes/$remote/$1 && return + done + return 1 } # cd to worktree $1 unless already in it. @@ -121,12 +127,17 @@ _git_enter() { } # Run `git $2 $3` in worktree $1 and cd there. If another worktree holds branch $3, detach -# it first (refusing when it is dirty), and reattach it if the switch fails. +# it first (refusing when tracked files are modified), and reattach it if the switch fails; +# if the current worktree holds it, stay. _git_switch_in() { local dir=$1 cmd=$2 branch=$3 holder _git_worktree_of $branch && holder=$REPLY + if [[ -n $holder && ${holder:A} == ${$(command git rev-parse --show-toplevel 2>/dev/null):A} ]]; then + print -u2 "Already on '$branch'" + return + fi if [[ -n $holder && ${holder:A} != ${dir:A} ]]; then - if [[ -n $(command git -C $holder status --porcelain 2>/dev/null) ]]; then + if [[ -n $(command git -C $holder status --porcelain --untracked-files=no 2>/dev/null) ]]; then print -u2 "'$branch' is checked out in '$holder', which has uncommitted changes; leaving it" return 1 fi @@ -171,6 +182,7 @@ _git_pr_checkout() { } git() { + [[ -n $CLAUDECODE ]] && { command git "$@"; return } local REPLY dir here if [[ $1 == (checkout|switch) ]] && (( $@[(I)-.] )); then here=1 From 6f044fb03525f80e37fc2bc5bdf55bf952c055e8 Mon Sep 17 00:00:00 2001 From: "Thing-han, Lim" <15379156+potsrevennil@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:55:12 +0800 Subject: [PATCH 6/6] fix(shell): address remaining review findings on the checkout wrapper - Error out when the PR's branch name, pr- and pr-- are all taken, instead of letting gh reuse the last one off the PR's commit. - Count `-.` only before `--` (after it, it is a path) and keep empty arguments when stripping it. - Refuse a PR URL whose owner/repo none of the remotes point at. - Say so when gh returns incomplete PR info instead of failing silently. - Read the main checkout and every branch's worktree from one `git worktree list`: an intercepted checkout now runs 3 extra git processes instead of 5-6, and other git commands take a fast path (~4 us) before any locals are set up. Co-Authored-By: Claude Opus 5.5 --- modules/shells/zsh/zshrc | 116 ++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 51 deletions(-) diff --git a/modules/shells/zsh/zshrc b/modules/shells/zsh/zshrc index 84c3812..6fa9c1f 100644 --- a/modules/shells/zsh/zshrc +++ b/modules/shells/zsh/zshrc @@ -73,36 +73,36 @@ alias fman="compgen -c | fzf | xargs man" # branch stays put. Anything not naming a local or remote branch (commits, tags, paths, # `-`, other flags) goes straight to git, as does everything under Claude Code # ($CLAUDECODE), whose agents expect plain git in their worktrees. -# Sets REPLY to the worktree of branch $1; one `git worktree list`, parsed in-shell. -_git_worktree_of() { - local line wt +# Fills the caller's `wt` (branch -> worktree) and `main` (the main checkout, empty in a +# bare repo) from one `git worktree list`, and `top` (the current worktree). +_git_scan() { + local line p first=1 for line in ${(f)"$(command git worktree list --porcelain 2>/dev/null)"}; do case $line in - 'worktree '*) wt=${line#worktree } ;; - "branch refs/heads/$1") REPLY=$wt; return 0 ;; + 'worktree '*) p=${line#worktree }; (( first )) && main=$p; first=0 ;; + bare) [[ $p == $main ]] && main= ;; + 'branch refs/heads/'*) wt[${line#branch refs/heads/}]=$p ;; esac done - return 1 -} - -# Sets REPLY to the main checkout, or the current worktree if there is none (bare repo, -# submodule). -_git_main_worktree() { - local common - common=$(command git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return - if [[ $common == */.git ]]; then - REPLY=${common:h} - else - REPLY=$(command git rev-parse --show-toplevel) || return - fi + top=$(command git rev-parse --show-toplevel 2>/dev/null) + [[ -n $top ]] } -# Sets REPLY to the gh PR selector named by $1, if gh is installed. +# Sets REPLY to the gh PR selector named by $1, if gh is installed. Returns 2 for a PR URL +# of a repo none of this repo's remotes point at. _git_pr_of() { (( $+commands[gh] )) || return 1 + local slug url case $1 in '#'<->) REPLY=${1#\#} ;; - https://*/pull/<->|https://*/pull/<->/*) REPLY=$1 ;; + https://*/pull/<->|https://*/pull/<->/*) + slug=${(L)${${1#https://*/}%%/pull/*}} + for url in ${(f)"$(command git remote -v 2>/dev/null)"}; do + url=${(L)${${url#*$'\t'}% \(*\)}%.git} + [[ $url == *[:/]$slug ]] && { REPLY=$1; return 0 } + done + print -u2 "$1 is not a PR of this repo" + return 2 ;; <->) [[ ! -e $1 ]] && ! command git rev-parse -q --verify "$1^{commit}" >/dev/null || return 1 REPLY=$1 ;; *) return 1 ;; @@ -111,17 +111,18 @@ _git_pr_of() { # Whether $1 is a local branch, or /$1 exists for `git switch` to create it from. _git_is_branch() { - local remote - command git show-ref -q --verify refs/heads/$1 2>/dev/null && return - for remote in ${(f)"$(command git remote 2>/dev/null)"}; do - command git show-ref -q --verify refs/remotes/$remote/$1 && return + local ref + for ref in ${(f)"$(command git for-each-ref --format='%(refname)' refs/heads/$1 "refs/remotes/*/$1" 2>/dev/null)"}; do + [[ $ref == refs/heads/$1 ]] && return + ref=${ref#refs/remotes/} + [[ $ref == ${ref%%/*}/$1 ]] && return done return 1 } # cd to worktree $1 unless already in it. _git_enter() { - [[ ${1:A} == ${$(command git rev-parse --show-toplevel 2>/dev/null):A} ]] && return + [[ ${1:A} == ${top:A} ]] && return print -u2 "Switched to worktree '$1'" cd $1 } @@ -130,9 +131,8 @@ _git_enter() { # it first (refusing when tracked files are modified), and reattach it if the switch fails; # if the current worktree holds it, stay. _git_switch_in() { - local dir=$1 cmd=$2 branch=$3 holder - _git_worktree_of $branch && holder=$REPLY - if [[ -n $holder && ${holder:A} == ${$(command git rev-parse --show-toplevel 2>/dev/null):A} ]]; then + local dir=$1 cmd=$2 branch=$3 holder=$wt[$3] + if [[ -n $holder && ${holder:A} == ${top:A} ]]; then print -u2 "Already on '$branch'" return fi @@ -157,11 +157,14 @@ _git_switch_in() { # there, else switch to the local branch whose tip it is (the PR's branch name first); with # no such branch, `gh pr checkout` into a new branch that does not clobber one. _git_pr_checkout() { - local dir=$1 cmd=$2 + local dir=$1 cmd=$2 out line name local -a pr at - local line name - pr=(${(f)"$(gh pr view $3 --json number,headRefOid,headRefName -q '.number, .headRefOid, .headRefName')"}) - (( $#pr == 3 )) || return 1 + out=$(gh pr view $3 --json number,headRefOid,headRefName -q '.number, .headRefOid, .headRefName') || return + pr=("${(@f)out}") + if (( $#pr != 3 )) || [[ -z $pr[1] || -z $pr[2] || -z $pr[3] ]]; then + print -u2 "gh pr view $3: incomplete PR info (number, head commit, head branch)" + return 1 + fi local num=$pr[1] oid=$pr[2] head=$pr[3] if [[ $(command git -C $dir rev-parse -q --verify HEAD) == $oid ]]; then print -u2 "Already on PR #$num ($oid)" @@ -175,38 +178,49 @@ _git_pr_checkout() { _git_switch_in $dir $cmd ${at[(r)$head]:-$at[1]} return fi - for name in $head pr-$num pr-$num-${oid[1,7]}; do - command git show-ref -q --verify refs/heads/$name || break + for line in $head pr-$num pr-$num-${oid[1,7]}; do + command git show-ref -q --verify refs/heads/$line || { name=$line; break } done + if [[ -z $name ]]; then + print -u2 "PR #$num: branches $head, pr-$num and pr-$num-${oid[1,7]} all exist but none is at $oid" + return 1 + fi ( builtin cd -q $dir && gh pr checkout $3 --branch $name ) && _git_enter $dir } git() { - [[ -n $CLAUDECODE ]] && { command git "$@"; return } - local REPLY dir here - if [[ $1 == (checkout|switch) ]] && (( $@[(I)-.] )); then - here=1 - set -- ${@:#-.} + # Fast path: everything but checkout/switch/worktree, and anything under Claude Code. + [[ -n $CLAUDECODE || $1 != (checkout|switch|worktree) ]] && { command git "$@"; return } + local REPLY dir here top main + local -A wt + if [[ $1 == (checkout|switch) ]]; then + # `-.` counts only before `--`; after it, it is a path. + local -i dd=$@[(i)--] + local -a pre=("${(@)@[1,dd-1]}") + if (( $pre[(I)-.] )); then + here=1 + set -- "${(@)pre:#-.}" "${(@)@[dd,-1]}" + fi fi case "$1:$2" in worktree:path) (( $# == 3 )) || { print -u2 'usage: git worktree path '; return 129 } - _git_worktree_of $3 && print -r -- $REPLY + _git_scan + [[ -n $wt[$3] ]] && print -r -- $wt[$3] return ;; checkout:[^-]*|switch:[^-]*) # Only a bare `git checkout [-.]`; anything else goes straight to git. (( $# == 2 )) || { command git "$@"; return } - if (( here )); then - dir=$(command git rev-parse --show-toplevel 2>/dev/null) - elif _git_main_worktree; then - dir=$REPLY - fi - [[ -n $dir ]] || { command git "$@"; return } - if _git_pr_of $2; then - _git_pr_checkout $dir $1 $REPLY - return - fi - if _git_is_branch $2; then + _git_pr_of $2 + case $? in + 0) _git_scan || { command git "$@"; return } + (( here )) && dir=$top || dir=${main:-$top} + _git_pr_checkout $dir $1 $REPLY + return ;; + 2) return 1 ;; + esac + if _git_is_branch $2 && _git_scan; then + (( here )) && dir=$top || dir=${main:-$top} _git_switch_in $dir $1 $2 return fi ;;