mirror of
https://github.com/Xevion/dotfiles.git
synced 2026-01-31 02:24:11 -06:00
feat: add git worktree management utilities for Fish and Bash
Add comprehensive worktree tooling with FZF integration: - wtb: create branch worktree with gitignored file cloning - wtcd/wtr: interactive picker and multi-remove with FZF - wtf/wth: feature/hotfix branch shortcuts - wts/wtl: status overview and listing - Automatic .worktrees/ organization and .gitignore management
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
function _wt_clone_ignored --description "Clone gitignored files from source to destination worktree"
|
||||||
|
set -l src_path $argv[1]
|
||||||
|
set -l dst_path $argv[2]
|
||||||
|
|
||||||
|
# Find gitignored files (excluding .git and .worktrees)
|
||||||
|
set -l ignored_files (git -C "$src_path" ls-files --others --ignored --exclude-standard 2>/dev/null | \
|
||||||
|
grep -v '^\.git' | grep -v '^\.worktrees')
|
||||||
|
|
||||||
|
if test -z "$ignored_files"
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Found gitignored files to copy..."
|
||||||
|
|
||||||
|
# Measure size with timeout
|
||||||
|
set -l size_bytes 0
|
||||||
|
set -l size_human "unknown"
|
||||||
|
|
||||||
|
set -l size_output (timeout 10s du -sb "$src_path" --exclude='.git' --exclude='.worktrees' 2>/dev/null | head -n1)
|
||||||
|
if test $status -eq 0 -a -n "$size_output"
|
||||||
|
set size_bytes (echo "$size_output" | awk '{print $1}')
|
||||||
|
set size_human (numfmt --to=iec --suffix=B "$size_bytes" 2>/dev/null; or echo "$size_bytes bytes")
|
||||||
|
end
|
||||||
|
|
||||||
|
# 100MB threshold
|
||||||
|
set -l threshold (math "100 * 1024 * 1024")
|
||||||
|
|
||||||
|
if test "$size_bytes" -gt "$threshold" -o "$size_human" = "unknown"
|
||||||
|
echo "Gitignored files size: $size_human"
|
||||||
|
read -P "Copy these files to new worktree? [y/N] " -n 1 confirm
|
||||||
|
echo ""
|
||||||
|
if not string match -qi 'y' "$confirm"
|
||||||
|
echo "Skipping gitignored file copy"
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
echo "Copying gitignored files..."
|
||||||
|
for file in $ignored_files
|
||||||
|
set -l src "$src_path/$file"
|
||||||
|
set -l dst "$dst_path/$file"
|
||||||
|
if test -e "$src"
|
||||||
|
mkdir -p (dirname "$dst")
|
||||||
|
cp -r "$src" "$dst" 2>/dev/null
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
echo "Done copying gitignored files"
|
||||||
|
end
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
function wtb --description "Add git worktree with new branch"
|
||||||
|
if test (count $argv) -lt 1
|
||||||
|
echo "Usage: wtb <branch-name> [base-ref]" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l root (git rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
if test -z "$root"
|
||||||
|
echo "Not in a git repository" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l branch $argv[1]
|
||||||
|
|
||||||
|
# Get base ref: argument, or current branch tip
|
||||||
|
set -l base
|
||||||
|
if test (count $argv) -ge 2
|
||||||
|
set base $argv[2]
|
||||||
|
else
|
||||||
|
# Get current branch name (not HEAD if detached)
|
||||||
|
set base (git symbolic-ref --short HEAD 2>/dev/null)
|
||||||
|
if test -z "$base"
|
||||||
|
set base (git rev-parse HEAD)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sanitize branch name for directory (replace / with -)
|
||||||
|
set -l dir_name (string replace --all '/' '-' "$branch")
|
||||||
|
set -l wt_path "$root/.worktrees/$dir_name"
|
||||||
|
|
||||||
|
# Create .worktrees directory if needed
|
||||||
|
mkdir -p "$root/.worktrees"
|
||||||
|
|
||||||
|
# Add to .gitignore if not already there
|
||||||
|
if not grep -q '^\.worktrees/?$' "$root/.gitignore" 2>/dev/null
|
||||||
|
echo ".worktrees/" >> "$root/.gitignore"
|
||||||
|
echo "Added .worktrees/ to .gitignore"
|
||||||
|
end
|
||||||
|
|
||||||
|
echo "Creating worktree at $wt_path"
|
||||||
|
echo " Branch: $branch"
|
||||||
|
echo " Base: $base"
|
||||||
|
|
||||||
|
if git worktree add -b "$branch" "$wt_path" "$base"
|
||||||
|
echo ""
|
||||||
|
echo "Worktree created: $wt_path"
|
||||||
|
|
||||||
|
# Clone gitignored files from current worktree
|
||||||
|
set -l current_wt (pwd)
|
||||||
|
_wt_clone_ignored "$current_wt" "$wt_path"
|
||||||
|
|
||||||
|
cd "$wt_path"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
function wtcd --description "FZF-based worktree picker - cd into selected"
|
||||||
|
set -l root (git rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
if test -z "$root"
|
||||||
|
echo "Not in a git repository" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l selected (git worktree list | fzf --height=40% --reverse \
|
||||||
|
--header="Select worktree" \
|
||||||
|
--preview="git -C {1} log --oneline -5 2>/dev/null; echo ''; git -C {1} status --short 2>/dev/null")
|
||||||
|
|
||||||
|
if test -n "$selected"
|
||||||
|
set -l wt_path (echo "$selected" | awk '{print $1}')
|
||||||
|
cd "$wt_path"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
function wtf --description "Create feature branch worktree"
|
||||||
|
if test (count $argv) -lt 1
|
||||||
|
echo "Usage: wtf <feature-name> [base-ref]" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l name $argv[1]
|
||||||
|
set -l base $argv[2]
|
||||||
|
|
||||||
|
wtb "feature/$name" $base
|
||||||
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
function wtfeature --description "Create feature branch worktree (alias for wtf)"
|
||||||
|
wtf $argv
|
||||||
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
function wtfix --description "Create hotfix branch worktree (alias for wth)"
|
||||||
|
wth $argv
|
||||||
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
function wth --description "Create hotfix branch worktree"
|
||||||
|
if test (count $argv) -lt 1
|
||||||
|
echo "Usage: wth <hotfix-name> [base-ref]" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l name $argv[1]
|
||||||
|
set -l base $argv[2]
|
||||||
|
|
||||||
|
wtb "hotfix/$name" $base
|
||||||
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
function wtl --description "List all git worktrees"
|
||||||
|
git worktree list $argv
|
||||||
|
end
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
function wtr --description "FZF-based git worktree remover (multi-select)"
|
||||||
|
set -l root (git rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
if test -z "$root"
|
||||||
|
echo "Not in a git repository" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get the main worktree path to exclude it
|
||||||
|
set -l main_wt (git worktree list --porcelain | grep '^worktree ' | head -n1 | string replace 'worktree ' '')
|
||||||
|
|
||||||
|
# Select worktrees with fzf (excluding main)
|
||||||
|
set -l selected (git worktree list | grep -v "^$main_wt " | fzf --multi --height=40% --reverse \
|
||||||
|
--header="Select worktrees to remove (Tab to multi-select)" \
|
||||||
|
--preview="git -C {1} log --oneline -5 2>/dev/null || echo 'No commits'")
|
||||||
|
|
||||||
|
if test -z "$selected"
|
||||||
|
echo "No worktrees selected"
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
echo "Will remove the following worktrees:"
|
||||||
|
for line in $selected
|
||||||
|
set -l wt_path (echo "$line" | awk '{print $1}')
|
||||||
|
echo " - $wt_path"
|
||||||
|
end
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -P "Confirm removal? [y/N] " -n 1 confirm
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if string match -qi 'y' "$confirm"
|
||||||
|
for line in $selected
|
||||||
|
set -l wt_path (echo "$line" | awk '{print $1}')
|
||||||
|
echo "Removing $wt_path..."
|
||||||
|
git worktree remove "$wt_path"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
echo "Cancelled"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
function wts --description "Show git status for all worktrees"
|
||||||
|
set -l root (git rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
if test -z "$root"
|
||||||
|
echo "Not in a git repository" >&2
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
for wt_path in (git worktree list --porcelain | grep '^worktree ' | string replace 'worktree ' '')
|
||||||
|
set -l branch (git -C "$wt_path" symbolic-ref --short HEAD 2>/dev/null; or echo "detached")
|
||||||
|
|
||||||
|
# Print header with box drawing
|
||||||
|
set_color --bold blue
|
||||||
|
echo "╭─────────────────────────────────────────────────────────────╮"
|
||||||
|
echo -n "│ "
|
||||||
|
set_color --bold yellow
|
||||||
|
echo -n "$wt_path"
|
||||||
|
set_color normal
|
||||||
|
echo ""
|
||||||
|
set_color --bold blue
|
||||||
|
echo -n "│ "
|
||||||
|
set_color cyan
|
||||||
|
echo "[$branch]"
|
||||||
|
set_color --bold blue
|
||||||
|
echo "╰─────────────────────────────────────────────────────────────╯"
|
||||||
|
set_color normal
|
||||||
|
|
||||||
|
# Run git status
|
||||||
|
git -C "$wt_path" status --short
|
||||||
|
echo ""
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -164,6 +164,245 @@ alias gsts='git stash save'
|
|||||||
# Git log find by commit message
|
# Git log find by commit message
|
||||||
function glf() { git log --all --grep="$1"; }
|
function glf() { git log --all --grep="$1"; }
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Git Worktree Functions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Helper: Sanitize branch name for directory (feature/foo -> feature-foo)
|
||||||
|
_wt_sanitize() {
|
||||||
|
echo "$1" | tr '/' '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Get the current branch's tip (not HEAD if detached)
|
||||||
|
_wt_get_base() {
|
||||||
|
local branch
|
||||||
|
branch=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||||
|
if [[ -n "$branch" ]]; then
|
||||||
|
echo "$branch"
|
||||||
|
else
|
||||||
|
# Detached HEAD - try to find which branch we're on
|
||||||
|
git rev-parse HEAD
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Get git repo root
|
||||||
|
_wt_root() {
|
||||||
|
git rev-parse --show-toplevel 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: Clone gitignored files from source to destination worktree
|
||||||
|
_wt_clone_ignored() {
|
||||||
|
local src_path="$1"
|
||||||
|
local dst_path="$2"
|
||||||
|
|
||||||
|
# Find gitignored files (excluding .git and .worktrees)
|
||||||
|
local ignored_files
|
||||||
|
ignored_files=$(git -C "$src_path" ls-files --others --ignored --exclude-standard 2>/dev/null | \
|
||||||
|
grep -v "^\.git" | grep -v "^\.worktrees")
|
||||||
|
|
||||||
|
if [[ -z "$ignored_files" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Found gitignored files to copy..."
|
||||||
|
|
||||||
|
# Measure size with timeout
|
||||||
|
local size_output
|
||||||
|
local size_bytes=0
|
||||||
|
local size_human="unknown"
|
||||||
|
|
||||||
|
if size_output=$(timeout 10s du -sb "$src_path" --exclude='.git' --exclude='.worktrees' 2>/dev/null | head -n1); then
|
||||||
|
size_bytes=$(echo "$size_output" | awk '{print $1}')
|
||||||
|
size_human=$(numfmt --to=iec --suffix=B "$size_bytes" 2>/dev/null || echo "$size_bytes bytes")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local threshold=$((100 * 1024 * 1024)) # 100MB
|
||||||
|
|
||||||
|
if [[ "$size_bytes" -gt "$threshold" ]] || [[ "$size_human" == "unknown" ]]; then
|
||||||
|
echo "Gitignored files size: $size_human"
|
||||||
|
read -p "Copy these files to new worktree? [y/N] " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Skipping gitignored file copy"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Copying gitignored files..."
|
||||||
|
echo "$ignored_files" | while read -r file; do
|
||||||
|
local src="$src_path/$file"
|
||||||
|
local dst="$dst_path/$file"
|
||||||
|
if [[ -e "$src" ]]; then
|
||||||
|
mkdir -p "$(dirname "$dst")"
|
||||||
|
cp -r "$src" "$dst" 2>/dev/null
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done copying gitignored files"
|
||||||
|
}
|
||||||
|
|
||||||
|
# wtl - List all worktrees
|
||||||
|
function wtl() {
|
||||||
|
git worktree list "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# wts - Show git status for all worktrees
|
||||||
|
function wts() {
|
||||||
|
local root
|
||||||
|
root=$(_wt_root)
|
||||||
|
if [[ -z "$root" ]]; then
|
||||||
|
echo "Not in a git repository" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git worktree list --porcelain | grep '^worktree ' | cut -d' ' -f2- | while read -r wt_path; do
|
||||||
|
local branch
|
||||||
|
branch=$(git -C "$wt_path" symbolic-ref --short HEAD 2>/dev/null || echo "detached")
|
||||||
|
|
||||||
|
# Print header
|
||||||
|
echo -e "\033[1;34m╭─────────────────────────────────────────────────────────────╮\033[0m"
|
||||||
|
echo -e "\033[1;34m│\033[0m \033[1;33m$wt_path\033[0m"
|
||||||
|
echo -e "\033[1;34m│\033[0m \033[0;36m[$branch]\033[0m"
|
||||||
|
echo -e "\033[1;34m╰─────────────────────────────────────────────────────────────╯\033[0m"
|
||||||
|
|
||||||
|
# Run git status
|
||||||
|
git -C "$wt_path" status --short
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# wtr - FZF-based worktree remover (multi-select)
|
||||||
|
function wtr() {
|
||||||
|
local root
|
||||||
|
root=$(_wt_root)
|
||||||
|
if [[ -z "$root" ]]; then
|
||||||
|
echo "Not in a git repository" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get worktrees excluding the main one
|
||||||
|
local main_wt
|
||||||
|
main_wt=$(git worktree list --porcelain | grep '^worktree ' | head -n1 | cut -d' ' -f2-)
|
||||||
|
|
||||||
|
local selected
|
||||||
|
selected=$(git worktree list | grep -v "^$main_wt " | fzf --multi --height=40% --reverse \
|
||||||
|
--header="Select worktrees to remove (Tab to multi-select)" \
|
||||||
|
--preview="git -C {1} log --oneline -5 2>/dev/null || echo 'No commits'")
|
||||||
|
|
||||||
|
if [[ -z "$selected" ]]; then
|
||||||
|
echo "No worktrees selected"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Will remove the following worktrees:"
|
||||||
|
echo "$selected" | awk '{print " - " $1}'
|
||||||
|
echo ""
|
||||||
|
read -p "Confirm removal? [y/N] " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "$selected" | while read -r line; do
|
||||||
|
local wt_path
|
||||||
|
wt_path=$(echo "$line" | awk '{print $1}')
|
||||||
|
echo "Removing $wt_path..."
|
||||||
|
git worktree remove "$wt_path"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "Cancelled"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# wtb - Add worktree with new branch
|
||||||
|
# Usage: wtb <branch-name> [base-ref]
|
||||||
|
function wtb() {
|
||||||
|
if [[ -z "$1" ]]; then
|
||||||
|
echo "Usage: wtb <branch-name> [base-ref]" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local root
|
||||||
|
root=$(_wt_root)
|
||||||
|
if [[ -z "$root" ]]; then
|
||||||
|
echo "Not in a git repository" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local branch="$1"
|
||||||
|
local base="${2:-$(_wt_get_base)}"
|
||||||
|
local dir_name
|
||||||
|
dir_name=$(_wt_sanitize "$branch")
|
||||||
|
local wt_path="$root/.worktrees/$dir_name"
|
||||||
|
|
||||||
|
# Create .worktrees directory if needed
|
||||||
|
mkdir -p "$root/.worktrees"
|
||||||
|
|
||||||
|
# Add to .gitignore if not already there
|
||||||
|
if ! grep -q "^\.worktrees/?$" "$root/.gitignore" 2>/dev/null; then
|
||||||
|
echo ".worktrees/" >> "$root/.gitignore"
|
||||||
|
echo "Added .worktrees/ to .gitignore"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating worktree at $wt_path"
|
||||||
|
echo " Branch: $branch"
|
||||||
|
echo " Base: $base"
|
||||||
|
|
||||||
|
if git worktree add -b "$branch" "$wt_path" "$base"; then
|
||||||
|
echo ""
|
||||||
|
echo "Worktree created: $wt_path"
|
||||||
|
|
||||||
|
# Clone gitignored files from current worktree
|
||||||
|
local current_wt
|
||||||
|
current_wt=$(pwd)
|
||||||
|
_wt_clone_ignored "$current_wt" "$wt_path"
|
||||||
|
|
||||||
|
cd "$wt_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# wtf / wtfeature - Create feature branch worktree
|
||||||
|
# Usage: wtf <name> [base-ref]
|
||||||
|
function wtf() {
|
||||||
|
if [[ -z "$1" ]]; then
|
||||||
|
echo "Usage: wtf <feature-name> [base-ref]" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
wtb "feature/$1" "${2:-}"
|
||||||
|
}
|
||||||
|
function wtfeature() { wtf "$@"; }
|
||||||
|
|
||||||
|
# wth / wtfix - Create hotfix branch worktree
|
||||||
|
# Usage: wth <name> [base-ref]
|
||||||
|
function wth() {
|
||||||
|
if [[ -z "$1" ]]; then
|
||||||
|
echo "Usage: wth <hotfix-name> [base-ref]" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
wtb "hotfix/$1" "${2:-}"
|
||||||
|
}
|
||||||
|
function wtfix() { wth "$@"; }
|
||||||
|
|
||||||
|
# wtcd - FZF-based worktree picker - cd into selected
|
||||||
|
function wtcd() {
|
||||||
|
local root
|
||||||
|
root=$(_wt_root)
|
||||||
|
if [[ -z "$root" ]]; then
|
||||||
|
echo "Not in a git repository" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local selected
|
||||||
|
selected=$(git worktree list | fzf --height=40% --reverse \
|
||||||
|
--header="Select worktree" \
|
||||||
|
--preview="git -C {1} log --oneline -5 2>/dev/null; echo ''; git -C {1} status --short 2>/dev/null")
|
||||||
|
|
||||||
|
if [[ -n "$selected" ]]; then
|
||||||
|
local wt_path
|
||||||
|
wt_path=$(echo "$selected" | awk '{print $1}')
|
||||||
|
cd "$wt_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# fzf abbreviation/alias search
|
# fzf abbreviation/alias search
|
||||||
if command -v fzf-abbr-search.ts &> /dev/null && command -v fzf &> /dev/null; then
|
if command -v fzf-abbr-search.ts &> /dev/null && command -v fzf &> /dev/null; then
|
||||||
fzf_search_abbr() {
|
fzf_search_abbr() {
|
||||||
|
|||||||
Reference in New Issue
Block a user