274 lines
8.0 KiB
Fish
274 lines
8.0 KiB
Fish
set main_checkout ~/dev
|
|
set worktree_root ~/worktrees/dev
|
|
set base_branch main
|
|
set remote origin
|
|
|
|
function __devtree_usage --no-scope-shadowing
|
|
echo "usage: devtree <command> [args]" >&2
|
|
echo "commands:" >&2
|
|
echo " new <branch> create a branch worktree from main and cd into it" >&2
|
|
echo " cd <branch> cd into an existing worktree" >&2
|
|
echo " list list worktrees" >&2
|
|
echo " sync [<branch>] fast-forward main, then rebase branch onto main" >&2
|
|
echo " sync --all fast-forward main, then rebase clean topic worktrees onto main" >&2
|
|
echo " remove <branch> remove a clean worktree, keeping the branch" >&2
|
|
echo " prune [--dry-run] prune worktree metadata and stale merged local branches" >&2
|
|
end
|
|
|
|
function __devtree_path_for_branch --no-scope-shadowing --argument-names branch
|
|
set current_path ""
|
|
for line in (git -C $main_checkout worktree list --porcelain)
|
|
if string match -q 'worktree *' -- $line
|
|
set current_path (string replace 'worktree ' '' -- $line)
|
|
else if test "$line" = "branch refs/heads/$branch"
|
|
echo $current_path
|
|
return 0
|
|
end
|
|
end
|
|
|
|
return 1
|
|
end
|
|
|
|
function __devtree_checked_out_branches --no-scope-shadowing
|
|
for line in (git -C $main_checkout worktree list --porcelain)
|
|
if string match -q 'branch refs/heads/*' -- $line
|
|
string replace 'branch refs/heads/' '' -- $line
|
|
end
|
|
end
|
|
end
|
|
|
|
function __devtree_update_main --no-scope-shadowing
|
|
if not test -d $main_checkout/.git
|
|
echo "devtree: main checkout not found at $main_checkout" >&2
|
|
return 1
|
|
end
|
|
|
|
set dirty_lines (git -C $main_checkout status --porcelain)
|
|
if test (count $dirty_lines) -gt 0
|
|
echo "devtree: refusing to update dirty main checkout: $main_checkout" >&2
|
|
return 1
|
|
end
|
|
|
|
git -C $main_checkout fetch --prune $remote
|
|
or return $status
|
|
|
|
git -C $main_checkout switch $base_branch
|
|
or return $status
|
|
|
|
git -C $main_checkout merge --ff-only $remote/$base_branch
|
|
end
|
|
|
|
function __devtree_sync_branch --no-scope-shadowing --argument-names branch
|
|
if test "$branch" = "$base_branch"
|
|
echo "$branch: already updated"
|
|
return 0
|
|
end
|
|
|
|
set path (__devtree_path_for_branch $branch)
|
|
if test -z "$path"
|
|
echo "devtree: no worktree found for branch '$branch'" >&2
|
|
return 1
|
|
end
|
|
|
|
set dirty_lines (git -C $path status --porcelain)
|
|
if test (count $dirty_lines) -gt 0
|
|
echo "$branch: skipped: dirty ($path)"
|
|
return 0
|
|
end
|
|
|
|
echo "$branch: rebasing onto $base_branch"
|
|
git -C $path rebase $base_branch
|
|
end
|
|
|
|
if test (count $argv) -eq 0
|
|
__devtree_usage
|
|
return 2
|
|
end
|
|
|
|
set command $argv[1]
|
|
set args $argv[2..-1]
|
|
|
|
switch $command
|
|
case new
|
|
if test (count $args) -ne 1
|
|
echo "usage: devtree new <branch>" >&2
|
|
return 2
|
|
end
|
|
|
|
set branch $args[1]
|
|
set path $worktree_root/(string replace -a / __ $branch)
|
|
|
|
if git -C $main_checkout show-ref --verify --quiet refs/heads/$branch
|
|
echo "devtree: branch already exists: $branch" >&2
|
|
return 1
|
|
end
|
|
|
|
if test -e $path
|
|
echo "devtree: worktree path already exists: $path" >&2
|
|
return 1
|
|
end
|
|
|
|
__devtree_update_main
|
|
or return $status
|
|
|
|
mkdir -p $worktree_root
|
|
or return $status
|
|
|
|
git -C $main_checkout worktree add -b $branch $path $base_branch
|
|
or return $status
|
|
|
|
cd $path
|
|
|
|
case cd
|
|
if test (count $args) -ne 1
|
|
echo "usage: devtree cd <branch>" >&2
|
|
return 2
|
|
end
|
|
|
|
set path (__devtree_path_for_branch $args[1])
|
|
if test -z "$path"
|
|
echo "devtree: no worktree found for branch '$args[1]'" >&2
|
|
return 1
|
|
end
|
|
|
|
cd $path
|
|
|
|
case list
|
|
if test (count $args) -ne 0
|
|
echo "usage: devtree list" >&2
|
|
return 2
|
|
end
|
|
|
|
git -C $main_checkout worktree list
|
|
|
|
case sync
|
|
if test (count $args) -gt 1
|
|
echo "usage: devtree sync [<branch>]" >&2
|
|
echo " devtree sync --all" >&2
|
|
return 2
|
|
end
|
|
|
|
__devtree_update_main
|
|
or return $status
|
|
|
|
if test (count $args) -eq 1; and test "$args[1]" = --all
|
|
set current_path ""
|
|
set branches
|
|
for line in (git -C $main_checkout worktree list --porcelain)
|
|
if string match -q 'worktree *' -- $line
|
|
set current_path (string replace 'worktree ' '' -- $line)
|
|
else if string match -q 'branch refs/heads/*' -- $line
|
|
set branch (string replace 'branch refs/heads/' '' -- $line)
|
|
if test "$branch" != "$base_branch"
|
|
set --append branches $branch
|
|
end
|
|
end
|
|
end
|
|
|
|
for branch in $branches
|
|
__devtree_sync_branch $branch
|
|
or return $status
|
|
end
|
|
else
|
|
if test (count $args) -eq 1
|
|
set branch $args[1]
|
|
else
|
|
git rev-parse --show-toplevel >/dev/null 2>&1
|
|
or begin
|
|
echo "devtree: run 'devtree sync' from a git checkout, or pass a branch" >&2
|
|
return 2
|
|
end
|
|
|
|
set branch (git branch --show-current)
|
|
if test -z "$branch"
|
|
echo "devtree: could not determine the current branch" >&2
|
|
return 2
|
|
end
|
|
end
|
|
|
|
__devtree_sync_branch $branch
|
|
end
|
|
|
|
case remove
|
|
if test (count $args) -ne 1
|
|
echo "usage: devtree remove <branch>" >&2
|
|
return 2
|
|
end
|
|
|
|
set branch $args[1]
|
|
if test "$branch" = "$base_branch"
|
|
echo "devtree: refusing to remove $base_branch" >&2
|
|
return 1
|
|
end
|
|
|
|
set path (__devtree_path_for_branch $branch)
|
|
if test -z "$path"
|
|
echo "devtree: no worktree found for branch '$branch'" >&2
|
|
return 1
|
|
end
|
|
|
|
set dirty_lines (git -C $path status --porcelain)
|
|
if test (count $dirty_lines) -gt 0
|
|
echo "devtree: refusing to remove dirty worktree: $path" >&2
|
|
return 1
|
|
end
|
|
|
|
git -C $main_checkout worktree remove $path
|
|
|
|
case prune
|
|
set dry_run false
|
|
if test (count $args) -gt 1
|
|
echo "usage: devtree prune [--dry-run]" >&2
|
|
return 2
|
|
else if test (count $args) -eq 1
|
|
if test "$args[1]" = --dry-run
|
|
set dry_run true
|
|
else
|
|
echo "usage: devtree prune [--dry-run]" >&2
|
|
return 2
|
|
end
|
|
end
|
|
|
|
git -C $main_checkout worktree prune
|
|
or return $status
|
|
|
|
git -C $main_checkout fetch --prune $remote
|
|
or return $status
|
|
|
|
set checked_out (__devtree_checked_out_branches)
|
|
for line in (git -C $main_checkout for-each-ref --format='%(refname:short)|%(upstream:track)' refs/heads)
|
|
set parts (string split '|' -- $line)
|
|
set branch $parts[1]
|
|
set upstream_track $parts[2]
|
|
|
|
if test "$branch" = "$base_branch"
|
|
continue
|
|
end
|
|
|
|
if contains -- $branch $checked_out
|
|
continue
|
|
end
|
|
|
|
if not string match -q '*gone*' -- $upstream_track
|
|
continue
|
|
end
|
|
|
|
if not git -C $main_checkout merge-base --is-ancestor $branch $base_branch
|
|
echo "$branch: skipped: not merged into $base_branch"
|
|
continue
|
|
end
|
|
|
|
if test "$dry_run" = true
|
|
echo "$branch: would delete"
|
|
else
|
|
git -C $main_checkout branch -d $branch
|
|
or return $status
|
|
end
|
|
end
|
|
|
|
case '*'
|
|
echo "devtree: unknown command: $command" >&2
|
|
__devtree_usage
|
|
return 2
|
|
end
|