02 / 06
wt
移動 / 選択zsh/repo.zsh依存: fzf · wtp
ABOUT
worktree を選んで移動、作成、掃除まで。worktree 運用のハブコマンド。
現在のリポジトリの worktree を fzf で選んで cd する。サブコマンドで作成と掃除も担う。wt create は sotono/<slug> ブランチ(slug 省略時はタイムスタンプ)を、fzf で選んだベースブランチから wtp で作成して cd する。wt clean は親以外の linked worktree を 1 件ずつ確認しながら削除し、--node なら worktree は残して node_modules だけを消す。削除対象の中に自分がいる場合は先に親 worktree へ退避する。
USAGE
wt
wt create [slug]
wt clean [--node]
OPTIONS
- create [slug]
- ベースブランチを fzf で選び、sotono/<slug> の worktree を作成
- clean
- 親以外の worktree を確認付きで削除
- clean --node
- worktree は残し node_modules のみ削除
EXAMPLES
$ wtworktree を fzf で選んで cd
$ wt create fix-loginsotono/fix-login ブランチの worktree を作成
$ wt clean --node各 worktree の node_modules だけ削除
DEMO
DEMO GIF SLOT
src/assets/gifs/wt.gif を追加するとここに表示されます
SOURCE
# y/N を読み、y/yes なら成功を返す内部関数
_wt_confirm() {
local prompt="$1"
local reply
if [[ -n "$prompt" ]]; then
printf '%s [y/N] ' "$prompt"
else
printf '[y/N] '
fi
if ! read -r reply; then
echo
return 1
fi
[[ "$reply" == [yY] || "$reply" == [yY][eE][sS] ]]
}
# cwd が対象 worktree 配下なら親 worktree へ移る内部関数
_wt_leave_worktree_if_inside() {
local target_path="$1"
local main_path="$2"
if [[ "$PWD" == "$target_path" || "$PWD" == "$target_path"/* ]]; then
cd "$main_path" || return 1
echo "作業ディレクトリを親ワークツリーへ移動しました: $main_path"
fi
}
# 親以外の linked worktree を確認付きで削除する。
# --node は削除せず、各 linked worktree 直下の node_modules だけ消す。
# usage: _wt_clean [--node]
_wt_clean() {
local node_only=0
while [[ $# -gt 0 ]]; do
case "$1" in
--node)
node_only=1
shift
;;
-*)
echo "unknown option: $1" >&2
echo "usage: wt clean [--node]" >&2
return 1
;;
*)
echo "usage: wt clean [--node]" >&2
return 1
;;
esac
done
local worktree_choices
worktree_choices=$(_git_worktree_choices .) || return 1
local main_line
main_line=$(printf '%s\n' "$worktree_choices" | sed '/^$/d' | sed -n '1p')
local main_path
main_path=$(printf '%s\n' "$main_line" | cut -f2-)
local linked_choices
linked_choices=$(printf '%s\n' "$worktree_choices" | sed '/^$/d' | sed '1d')
if [[ -z "$linked_choices" ]]; then
echo "追加のワークツリーはありません"
return 1
fi
local branch path
local -a targets=()
if (( node_only )); then
echo "親以外のワークツリーの node_modules:"
while IFS=$'\t' read -r branch path; do
[[ -n "$path" ]] || continue
if [[ -e "$path/node_modules" ]]; then
targets+=("$branch"$'\t'"$path")
printf ' %s\n %s\n' "$branch" "$path/node_modules"
fi
done <<< "$linked_choices"
if (( ${#targets[@]} == 0 )); then
echo "削除する node_modules はありません"
return 0
fi
echo
if ! _wt_confirm "これらの node_modules を削除しますか?"; then
echo "キャンセルしました"
return 1
fi
local removed=0 failed=0
for entry in "${targets[@]}"; do
branch=$(printf '%s\n' "$entry" | cut -f1)
path=$(printf '%s\n' "$entry" | cut -f2-)
if rm -rf -- "$path/node_modules"; then
echo "削除しました: $branch $path/node_modules"
removed=$((removed + 1))
else
echo "削除に失敗しました: $branch $path/node_modules" >&2
failed=$((failed + 1))
fi
done
echo "完了: ${removed} 件削除${failed:+, ${failed} 件失敗}"
(( failed == 0 ))
return
fi
echo "親ワークツリー: $main_path"
echo "削除候補:"
while IFS=$'\t' read -r branch path; do
[[ -n "$path" ]] || continue
printf ' %s\n %s\n' "$branch" "$path"
done <<< "$linked_choices"
echo
local removed=0 skipped=0 failed=0
while IFS=$'\t' read -r branch path; do
[[ -n "$path" ]] || continue
printf 'このワークツリーを削除しますか?\n %s\n %s\n' "$branch" "$path"
if ! _wt_confirm ""; then
echo "スキップしました: $branch"
skipped=$((skipped + 1))
echo
continue
fi
if ! _wt_leave_worktree_if_inside "$path" "$main_path"; then
echo "親ワークツリーへ移動できないためスキップします: $path" >&2
failed=$((failed + 1))
echo
continue
fi
if git worktree remove --force --force -- "$path"; then
echo "削除しました: $branch $path"
removed=$((removed + 1))
else
echo "削除に失敗しました: $branch $path" >&2
failed=$((failed + 1))
fi
echo
done <<< "$linked_choices"
echo "完了: ${removed} 件削除, ${skipped} 件スキップ${failed:+, ${failed} 件失敗}"
(( failed == 0 ))
}
# 現在のリポジトリで新しい worktree を作って cd する
# usage: _wt_create [slug]
# - slug 省略時は sotono/YYYYMMDDHHMMSS のブランチを作成
# - slug 指定時は sotono/<slug> のブランチを作成
# - fzf でベースブランチ(ローカル + リモート、更新順)を選択
_wt_create() {
local slug="${1:-$(date +%Y%m%d%H%M%S)}"
local new_branch="sotono/${slug}"
if git show-ref --verify --quiet "refs/heads/${new_branch}"; then
echo "ブランチが既に存在します: ${new_branch}"
return 1
fi
local base_choice
base_choice=$({
git for-each-ref --sort=-committerdate refs/heads/ \
--format='%(committerdate:relative)%09%(refname:short)%09%(refname:short)'
git for-each-ref --sort=-committerdate refs/remotes/ \
--format='%(committerdate:relative)%09%(refname:short)%09%(refname:short)' |
grep -v '/HEAD$'
} | awk -F '\t' '!seen[$2]++' |
fzf --prompt='base branch> ' \
--header='ベースブランチを選択 (更新順)' \
--with-nth=1,2 --delimiter=$'\t')
if [[ -z "$base_choice" ]]; then
echo "キャンセルしました"
return 1
fi
local base_branch
base_branch=$(printf '%s\n' "$base_choice" | cut -f3-)
local base_commit="$base_branch"
if [[ "$base_branch" == */* ]] && git show-ref --verify --quiet "refs/remotes/${base_branch}"; then
base_commit="$base_branch"
fi
echo "Creating worktree: ${new_branch} (from ${base_branch})"
if ! wtp add -b "${new_branch}" "${base_commit}"; then
echo "wtp add に失敗しました"
return 1
fi
local worktree_path
worktree_path=$(wtp cd "${new_branch}") || {
echo "wtp cd でパスを取得できませんでした: ${new_branch}"
return 1
}
cd "${worktree_path}" || return 1
echo "${worktree_path}" >> "$REPOHIST_FILE"
echo "${worktree_path}"
}
# 現在のリポジトリ内の worktree を fzf で選んで cd する(repo の第2段階と同じ UI)
# usage: wt
# wt create [slug]
# wt clean
# wt clean --node
wt() {
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "このディレクトリはGitリポジトリではありません"
return 1
fi
case "${1:-}" in
create)
shift
_wt_create "$@"
;;
clean)
shift
_wt_clean "$@"
;;
"")
local worktree_choices
worktree_choices=$(_git_worktree_choices .)
local worktree_count
worktree_count=$(printf '%s\n' "$worktree_choices" | sed '/^$/d' | wc -l | tr -d ' ')
if (( worktree_count <= 1 )); then
echo "追加のワークツリーはありません"
return 1
fi
local selected
selected=$(printf '%s\n' "$worktree_choices" | fzf --prompt='worktree> ')
if [[ -n "$selected" ]]; then
cd "$(printf '%s\n' "$selected" | cut -f2-)"
fi
;;
*)
echo "unknown option: $1" >&2
echo "usage: wt" >&2
echo " wt create [slug]" >&2
echo " wt clean [--node]" >&2
return 1
;;
esac
}