feat: add interactive worktree add command (gwa)

Add `gwa` command for interactively creating git worktrees with
fzf-powered branch selection.

Usage:
- `gwa`: select a branch to create a worktree for
- `gwa <new-branch>`: create a new branch from a selected base branch
- 2+ args: fall back to `git worktree add` directly

New worktrees are created under `$FORGIT_WORKTREE_ADD_DIR` (defaults to
`<repo-root>/.wt/`) and the shell automatically cd's into the result.
This commit is contained in:
Wenxuan Zhang 2026-03-12 10:01:45 +08:00 committed by Wenxuan
parent e20071e29b
commit 7f9ea3a4b1
7 changed files with 78 additions and 3 deletions

View file

@ -71,6 +71,7 @@ It's **lightweight** and **easy to use**.
| `grw` | Interactive `git commit --fixup=reword && git rebase -i --autosquash` selector |
| `gclean` | Interactive `git clean` selector |
| `gwt` | Interactive `git worktree` selector |
| `gwa` | Interactive `git worktree add` selector |
| `gwd` | Interactive `git worktree remove` selector |
📥 Installation
@ -203,6 +204,7 @@ forgit_fixup=gfu
forgit_squash=gsq
forgit_reword=grw
forgit_worktree=gwt
forgit_worktree_add=gwa
forgit_worktree_delete=gwd
```
@ -280,6 +282,7 @@ Each forgit command can be customized with dedicated environment variables for g
| `gsq` | `FORGIT_SQUASH_GIT_OPTS` | `FORGIT_SQUASH_FZF_OPTS` |
| `grw` | `FORGIT_REWORD_GIT_OPTS` | `FORGIT_REWORD_FZF_OPTS` |
| `gwt` | | `FORGIT_WORKTREE_FZF_OPTS` |
| `gwa` | `FORGIT_WORKTREE_ADD_BRANCH_GIT_OPTS` | `FORGIT_WORKTREE_ADD_FZF_OPTS` |
| `gwd` | `FORGIT_WORKTREE_DELETE_GIT_OPTS` | `FORGIT_WORKTREE_DELETE_FZF_OPTS` |
### Pagers
@ -350,6 +353,7 @@ export FORGIT_LOG_FZF_OPTS='
| `FORGIT_FULLSCREEN_CONTEXT` | lines of diff context in full-screen mode | 10 |
| `FORGIT_DIR_VIEW` | command used to preview directories | `tree` if available, otherwise `find` |
| `FORGIT_CLEAN_LIST_FILES_OPTS` | arguments passed to `git ls-files` together with `--others` to determine which files are shown when invoking `forgit clean` | |
| `FORGIT_WORKTREE_ADD_DIR` | directory where new worktrees are created | `<repo-root>/.wt` |
⌨ Keybindings
---------------

View file

@ -1262,6 +1262,11 @@ _forgit_paths_list() {
find "$path" -name "*$ext" -print |sed -e "s#$ext\$##" -e 's#.*/##' -e '/^$/d' | sort -fu
}
# Get the root path of the main worktree (not the current worktree)
_forgit_main_worktree_root() {
git worktree list --porcelain | sed -n 's/^worktree //p;q'
}
# Parse git worktree list --porcelain output and format it for display
# Output format: [XY] /path/to/worktree (branch) 3 hours ago
# X: '*' = current worktree, ' ' = other
@ -1410,6 +1415,51 @@ _forgit_worktree_delete() {
done
}
_forgit_worktree_add() {
_forgit_inside_git_repo || return 1
# Default to main worktree root, not current worktree's root
local wt_dir="${FORGIT_WORKTREE_ADD_DIR:-$(_forgit_main_worktree_root)/.wt}"
# Two or more arguments: pass through to git directly
if [[ $# -ge 2 ]]; then
git worktree add "$@"
return $?
fi
local new_branch="${1:-}"
local opts branch
_forgit_worktree_add_branch_git_opts=()
_forgit_parse_array _forgit_worktree_add_branch_git_opts "$FORGIT_WORKTREE_ADD_BRANCH_GIT_OPTS"
local header_opt=""
[[ -z "$new_branch" ]] && header_opt="--header-lines=1"
opts="
$FORGIT_FZF_DEFAULT_OPTS
+s +m --tiebreak=index $header_opt
--preview=\"$FORGIT branch_preview {}\"
$FORGIT_WORKTREE_ADD_FZF_OPTS
"
branch=$(_forgit_branch_list "${_forgit_worktree_add_branch_git_opts[@]:---all}" |
FZF_DEFAULT_OPTS="$opts" fzf | _forgit_extract_branch_name)
[[ -z "$branch" ]] && return 1
# Strip remotes/<remote>/ prefix for remote branches
local local_branch="${branch#remotes/*/}"
if [[ -n "$new_branch" ]]; then
# gwa <new-branch>: create new branch from selected base
local target="$wt_dir/$new_branch"
git worktree add -b "$new_branch" "$target" "$branch" >&2 && echo "$target"
else
# gwa: create worktree for selected branch
local target="$wt_dir/$local_branch"
git worktree add "$target" "$branch" >&2 && echo "$target"
fi
}
check_prequisites() {
local installed_fzf_version
local higher_fzf_version
@ -1484,6 +1534,7 @@ PUBLIC_COMMANDS=(
"stash_show"
"stash_push"
"worktree"
"worktree_add"
"worktree_delete"
)

View file

@ -92,6 +92,7 @@ _git-forgit() {
'stash_push:git stash push selector'
'switch_branch:git switch branch selector'
'worktree:git worktree browser'
'worktree_add:git worktree add selector'
'worktree_delete:git worktree remove selector'
)
_describe -t commands 'git forgit' subcommands

View file

@ -92,6 +92,7 @@ _git_forgit()
stash_push
switch_branch
worktree
worktree_add
worktree_delete
"

View file

@ -8,7 +8,7 @@
function __fish_forgit_needs_subcommand
for subcmd in add blame branch_delete checkout_branch checkout_commit checkout_file checkout_tag \
cherry_pick cherry_pick_from_branch clean diff fixup ignore log reflog rebase reset_head \
revert_commit reword squash stash_show stash_push switch_branch worktree worktree_delete
revert_commit reword squash stash_show stash_push switch_branch worktree worktree_add worktree_delete
if contains -- $subcmd (commandline -opc)
return 1
end
@ -51,6 +51,7 @@ complete -c git-forgit -n __fish_forgit_needs_subcommand -a stash_show -d 'git s
complete -c git-forgit -n __fish_forgit_needs_subcommand -a stash_push -d 'git stash push selector'
complete -c git-forgit -n __fish_forgit_needs_subcommand -a switch_branch -d 'git switch branch selector'
complete -c git-forgit -n __fish_forgit_needs_subcommand -a worktree -d 'git worktree browser'
complete -c git-forgit -n __fish_forgit_needs_subcommand -a worktree_add -d 'git worktree add selector'
complete -c git-forgit -n __fish_forgit_needs_subcommand -a worktree_delete -d 'git worktree remove selector'
complete -c git-forgit -n '__fish_seen_subcommand_from add' -a "(complete -C 'git add ')"

View file

@ -36,7 +36,14 @@ function forgit::worktree
return $status
end
set -l tree (git-forgit worktree)
test -n "$tree"; and cd "$tree"
test -d "$tree"; and cd "$tree"
end
function forgit::worktree::add
set -l tree (git-forgit worktree_add $argv)
or return $status
test -d "$tree"; or return 0
cd "$tree"; or return 1
end
# register abbreviations
@ -66,5 +73,6 @@ if test -z "$FORGIT_NO_ALIASES"
abbr -a -- (string collect $forgit_blame; or string collect "gbl") git-forgit blame
abbr -a -- (string collect $forgit_checkout_tag; or string collect "gct") git-forgit checkout_tag
abbr -a -- (string collect $forgit_worktree; or string collect "gwt") forgit::worktree
abbr -a -- (string collect $forgit_worktree_add; or string collect "gwa") forgit::worktree::add
abbr -a -- (string collect $forgit_worktree_delete; or string collect "gwd") git-forgit worktree_delete
end

View file

@ -164,7 +164,14 @@ forgit::worktree() {
if [[ $# -ne 0 ]]; then "$FORGIT" worktree "$@"; return $?; fi
local tree
tree=$("$FORGIT" worktree) || return $?
[[ -n "$tree" ]] && builtin cd "$tree" || return 1
[[ -d "$tree" ]] && builtin cd "$tree" || return 1
}
forgit::worktree::add() {
local tree
tree=$("$FORGIT" worktree_add "$@") || return $?
[[ -d "$tree" ]] || return 0
builtin cd "$tree" || return 1
}
forgit::worktree::delete() {
@ -200,6 +207,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then
builtin export forgit_reword="${forgit_reword:-grw}"
builtin export forgit_blame="${forgit_blame:-gbl}"
builtin export forgit_worktree="${forgit_worktree:-gwt}"
builtin export forgit_worktree_add="${forgit_worktree_add:-gwa}"
builtin export forgit_worktree_delete="${forgit_worktree_delete:-gwd}"
builtin alias "${forgit_add}"='forgit::add'
@ -227,6 +235,7 @@ if [[ -z "$FORGIT_NO_ALIASES" ]]; then
builtin alias "${forgit_reword}"='forgit::reword'
builtin alias "${forgit_blame}"='forgit::blame'
builtin alias "${forgit_worktree}"='forgit::worktree'
builtin alias "${forgit_worktree_add}"='forgit::worktree::add'
builtin alias "${forgit_worktree_delete}"='forgit::worktree::delete'
fi