Add worktree find <glob> to find in all worktrees

This commit is contained in:
2026-09-07 14:58:08 +01:00
parent 9aba1adce8
commit 538f90083b
2 changed files with 184 additions and 32 deletions

View File

@@ -36,6 +36,7 @@ _worktree() {
local commands; commands=(
'add:Create a worktree for a branch'
'cd:Change directory to a worktree by branch'
'find:Find a path in every worktree'
'list:List managed worktrees'
'ls:List managed worktrees'
'remove:Remove a worktree by branch'
@@ -53,6 +54,11 @@ _worktree() {
(cd)
_arguments '1:: :__worktree_all_branches'
;;
(find)
_arguments \
'(-i --ignore-case)'{-i,--ignore-case}'[Match the glob case insensitively]' \
'1:: :_files'
;;
(rm|remove)
_arguments \
'(-f --force)'{-f,--force}'[Force removal even with modified or untracked files]' \

View File

@@ -1,9 +1,92 @@
# Emit one "path<TAB>branch<TAB>head" row per worktree, main clone first,
# branch empty when the worktree is detached.
__worktree_list() {
git worktree list --porcelain | awk '
/^worktree /{ p = substr($0, 10); b = ""; h = "" }
/^HEAD /{ h = substr($0, 6) }
/^branch refs\/heads\//{ b = $0; sub(/^branch refs\/heads\//, "", b) }
/^$/{ if (p != "") { print p "\t" b "\t" h; p = "" } }
END{ if (p != "") print p "\t" b "\t" h }
'
}
# Fill the caller's paths, names, dirs and mismatched arrays with an entry per
# worktree. names is the checked out branch, or its short hash when detached.
# dirs is @<suffix> for a worktree directory of the form <main clone>@<suffix>,
# and the directory name otherwise. mismatched is 1 when that suffix and the
# branch differ. With -m, only worktrees with such a suffix are returned.
__worktree_labels() {
local managed_only=0
[[ "$1" == "-m" ]] && managed_only=1
local -a rows
rows=("${(@f)$(__worktree_list)}")
local main=${rows[1]%%$'\t'*}
local prefix="${main:h}/${main:t}@"
local row p b h name suffix dir mm
for row in "${rows[@]}"; do
[[ -z "$row" ]] && continue
p=${row%%$'\t'*}
b=${${row#*$'\t'}%%$'\t'*}
h=${row##*$'\t'}
if [[ -n "$b" ]]; then
name=$b
else
name="(detached ${h[1,7]})"
fi
if [[ "${p[1,${#prefix}]}" == "$prefix" ]]; then
suffix=${p[${#prefix}+1,-1]}
dir="@$suffix"
if [[ "$b" == "$suffix" ]]; then
mm=0
else
mm=1
fi
elif (( managed_only )); then
continue
else
dir=${p:t}
mm=0
fi
paths+=("$p")
names+=("$name")
dirs+=("$dir")
mismatched+=($mm)
done
}
__worktree_find_in() {
local dir=$1 pat=$2 icase=$3
if (( $+commands[fd] )); then
local -a fd_args=(
--base-directory "$dir" --hidden --no-ignore --exclude .git --glob
)
if (( icase )); then
fd_args+=(--ignore-case)
else
fd_args+=(--case-sensitive)
fi
if [[ "$pat" == */* ]]; then
fd "${fd_args[@]}" --full-path "**/$pat"
else
fd "${fd_args[@]}" "$pat"
fi
else
local ic=
(( icase )) && ic=i
local -a expr=(-${ic}name "$pat")
[[ "$pat" == */* ]] && expr=(-${ic}path "*/$pat")
(cd "$dir" && find . -name .git -prune -o "${expr[@]}" -print)
fi
}
worktree() {
if [[ "$1" == "" ]]; then
echo "usage: worktree {add,cd,list,remove,sync} <branch> [<branch>...]"
echo "usage: worktree {add,cd,list,remove,sync} <branch> [<branch>...]
worktree find [-i] <glob>"
return 1
elif [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
echo "usage: worktree {add,cd,list,remove,sync} <branch> [<branch>...]
worktree find [-i] <glob>
Manage git worktrees by branch name."
return 0
@@ -17,7 +100,7 @@ Manage git worktrees by branch name."
return 1
fi
if [[ "$cmd" != "cd" ]] && \
if [[ "$cmd" != "cd" ]] && [[ "$cmd" != "find" ]] && \
[[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" == "true" ]] && \
[[ "$(git rev-parse --absolute-git-dir)" != "$(git rev-parse --show-toplevel)/.git" ]]
then
@@ -64,42 +147,102 @@ Manage git worktrees by branch name."
cd "$wt_path"
;;
list|ls)
local root=$(git rev-parse --show-toplevel)
local prefix="${root:h}/${root:t}@"
local -a names dirs mismatched
local maxw=0
local row p b h name suffix
for row in "${(@f)$(git worktree list --porcelain | awk '
/^worktree /{ p = substr($0, 10); b = ""; h = "" }
/^HEAD /{ h = substr($0, 6) }
/^branch refs\/heads\//{ b = $0; sub(/^branch refs\/heads\//, "", b) }
/^$/{ if (p != "") { print p "\t" b "\t" h; p = "" } }
END{ if (p != "") print p "\t" b "\t" h }
')}"; do
[[ -z "$row" ]] && continue
p=${row%%$'\t'*}
b=${${row#*$'\t'}%%$'\t'*}
h=${row##*$'\t'}
[[ "${p[1,${#prefix}]}" == "$prefix" ]] || continue
suffix=${p[${#prefix}+1,-1]}
if [[ -n "$b" ]]; then
name=$b
else
name="(detached ${h[1,7]})"
find)
local icase=0
local -a globs
local arg
for arg in "${@:2}"; do
case "$arg" in
-i|--ignore-case) icase=1 ;;
*) globs+=("$arg") ;;
esac
done
if (( ${#globs} == 0 )); then
print -P "%F{red}error:%f missing <glob> argument"
return 1
fi
names+=("$name")
dirs+=("@$suffix")
if [[ "$b" == "$suffix" ]]; then
mismatched+=(0)
else
mismatched+=(1)
if (( ${#globs} > 1 )); then
print -P "%F{red}error:%f unexpected argument: ${globs[2]}"
return 1
fi
(( ${#name} > maxw )) && maxw=${#name}
local pattern=${globs[1]}
# Matching is on the tail of a path, so leading and trailing slashes
# carry no meaning.
local pat=$pattern
while [[ "$pat" == /* ]]; do pat=${pat#/}; done
while [[ "$pat" == */ ]]; do pat=${pat%/}; done
if [[ -z "$pat" ]]; then
print -P "%F{red}error:%f invalid <glob> argument: $pattern"
return 1
fi
local -a paths names dirs mismatched
__worktree_labels
# Search every worktree at once, each into its own file so that the
# results don't interleave.
local REPORTTIME=-1
local tmp
tmp=$(mktemp -d "${TMPDIR:-/tmp}/worktree-find.XXXXXX") || return 1
local -a results
local i
{
(
for (( i = 1; i <= ${#paths}; i++ )); do
__worktree_find_in "${paths[i]}" "$pat" "$icase" | sort >"$tmp/$i" &
done
wait
)
for (( i = 1; i <= ${#paths}; i++ )); do
results+=("$(<$tmp/$i)")
done
} always {
rm -rf "$tmp"
}
# Report every worktree with matches under the same heading list prints,
# followed by its matches as absolute paths.
local c_dir c_warn c_off
if [[ -t 1 ]]; then
c_dir=$(print -Pn '%F{8}')
c_warn=$(print -Pn '%F{yellow}')
c_off=$(print -Pn '%f')
fi
local found=0 line root c
for (( i = 1; i <= ${#paths}; i++ )); do
[[ -n "${results[i]}" ]] || continue
(( found )) && print
found=1
root=${paths[i]}
[[ "$root" == "$HOME" || "$root" == "$HOME"/* ]] && root="~${root#$HOME}"
if (( mismatched[i] )); then
c=$c_warn
else
c=$c_dir
fi
print -r -- "${names[i]} ${c}${dirs[i]}${c_off}"
for line in "${(@f)${results[i]}}"; do
print -r -- " $root/${line#./}"
done
done
if (( ! found )); then
print -u2 -- "no matches: $pattern"
return 1
fi
;;
list|ls)
local -a paths names dirs mismatched
__worktree_labels -m
(( ${#names} )) || return 0
local maxw=0 name
for name in "${names[@]}"; do
(( ${#name} > maxw )) && maxw=${#name}
done
local c_dir c_warn c_off
if [[ -t 1 ]]; then
c_dir=$(print -Pn '%F{8}')
@@ -236,3 +379,6 @@ Manage git worktrees by branch name."
;;
esac
}
# A <glob> is matched against every worktree, never the current directory.
alias worktree='noglob worktree'