Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 73 additions & 49 deletions lib/commands/clean.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ _clean_should_skip() {
_clean_merged() {
local repo_root="$1" base_dir="$2" prefix="$3" yes_mode="$4" dry_run="$5" force="${6:-0}" active_worktree_path="${7:-}" target_ref="${8:-}"

# base_dir and prefix are kept for the helper contract. Merged cleanup uses
# Git's registry so nested registered worktrees are processed directly.
: "$base_dir" "$prefix"

log_step "Checking for worktrees with merged PRs/MRs..."

local provider
Expand All @@ -84,57 +88,77 @@ _clean_merged() {
local removed=0 skipped=0
local main_branch
main_branch=$(current_branch "$repo_root")

for dir in "$base_dir/${prefix}"*; do
[ -d "$dir" ] || continue

local branch
branch=$(current_branch "$dir") || true
local branch_tip
branch_tip=$(git -C "$dir" rev-parse HEAD 2>/dev/null || true)

# Skip main repo branch silently (not counted)
[ "$branch" = "$main_branch" ] && continue

# Check if branch has a merged PR/MR
if check_branch_merged "$provider" "$branch" "$target_ref" "$branch_tip"; then
if _clean_should_skip "$dir" "$branch" "$force" "$active_worktree_path"; then
skipped=$((skipped + 1))
continue
fi

if [ "$dry_run" -eq 1 ]; then
log_info "[dry-run] Would remove: $branch ($dir)"
removed=$((removed + 1))
elif [ "$yes_mode" -eq 1 ] || prompt_yes_no "Remove worktree and delete branch '$branch'?"; then
log_step "Removing worktree: $branch"

if ! run_hooks_in preRemove "$dir" \
REPO_ROOT="$repo_root" \
WORKTREE_PATH="$dir" \
BRANCH="$branch"; then
log_warn "Pre-remove hook failed for $branch, skipping"
skipped=$((skipped + 1))
continue
fi

if remove_worktree "$dir" "$force"; then
git branch -d "$branch" 2>/dev/null || git branch -D "$branch" 2>/dev/null || true
removed=$((removed + 1))

if ! run_hooks postRemove \
REPO_ROOT="$repo_root" \
WORKTREE_PATH="$dir" \
BRANCH="$branch"; then
log_warn "Post-remove hook failed for $branch"
local records
records=$(list_worktree_records "$repo_root")

local is_main="" dir="" branch="" line
while IFS= read -r line; do
case "$line" in
"")
if [ -n "$dir" ] && [ "$is_main" != "1" ]; then
local branch_tip
branch_tip=$(git -C "$dir" rev-parse HEAD 2>/dev/null || true)

# Skip main repo branch silently (not counted)
[ "$branch" = "$main_branch" ] && continue

# Check if branch has a merged PR/MR
if check_branch_merged "$provider" "$branch" "$target_ref" "$branch_tip"; then
if _clean_should_skip "$dir" "$branch" "$force" "$active_worktree_path"; then
skipped=$((skipped + 1))
continue
fi

if [ "$dry_run" -eq 1 ]; then
log_info "[dry-run] Would remove: $branch ($dir)"
removed=$((removed + 1))
elif [ "$yes_mode" -eq 1 ] || prompt_yes_no "Remove worktree and delete branch '$branch'?"; then
log_step "Removing worktree: $branch"

if ! run_hooks_in preRemove "$dir" \
REPO_ROOT="$repo_root" \
WORKTREE_PATH="$dir" \
BRANCH="$branch"; then
log_warn "Pre-remove hook failed for $branch, skipping"
skipped=$((skipped + 1))
continue
fi

if remove_worktree "$dir" "$force"; then
git branch -d "$branch" 2>/dev/null || git branch -D "$branch" 2>/dev/null || true
removed=$((removed + 1))

if ! run_hooks postRemove \
REPO_ROOT="$repo_root" \
WORKTREE_PATH="$dir" \
BRANCH="$branch"; then
log_warn "Post-remove hook failed for $branch"
fi
fi
else
log_warn "Skipped: $branch (user declined)"
skipped=$((skipped + 1))
fi
fi
fi
else
log_warn "Skipped: $branch (user declined)"
skipped=$((skipped + 1))
fi
fi
done
is_main=""
dir=""
branch=""
;;
"is_main "*)
is_main="${line#is_main }"
;;
"path "*)
dir=$(_tsv_unescape_field "${line#path }")
;;
"branch "*)
branch=$(_tsv_unescape_field "${line#branch }")
;;
esac
done <<EOF
$records

EOF

echo ""
if [ "$dry_run" -eq 1 ]; then
Expand Down
2 changes: 1 addition & 1 deletion lib/commands/copy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ cmd_copy() {
# Build target list for --all mode
if [ "$all_mode" -eq 1 ]; then
local all_branches
all_branches=$(list_worktree_branches "$base_dir" "$prefix")
all_branches=$(list_worktree_branches "$base_dir" "$prefix" "$repo_root")
if [ -z "$all_branches" ]; then
log_error "No worktrees found"
exit 1
Expand Down
121 changes: 89 additions & 32 deletions lib/commands/list.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,55 @@ cmd_list() {

resolve_repo_context || exit 1

local repo_root="$_ctx_repo_root" base_dir="$_ctx_base_dir" prefix="$_ctx_prefix"
local repo_root="$_ctx_repo_root"
local records
records=$(list_worktree_records "$repo_root")

# Machine-readable output (porcelain)
if [ "$porcelain" -eq 1 ]; then
# Output: path<tab>branch<tab>status
local branch status
branch=$(current_branch "$repo_root")
status=$(worktree_status "$repo_root")
printf "%s\t%s\t%s\n" "$repo_root" "$branch" "$status"
local is_main="" path="" branch="" status="" linked_rows="" line
while IFS= read -r line; do
case "$line" in
"")
[ -z "$path" ] && continue
if [ "$is_main" = "1" ]; then
printf "%s\t%s\t%s\n" "$path" "$branch" "$status"
else
linked_rows="${linked_rows}${path}"$'\t'"${branch}"$'\t'"${status}"$'\n'
fi
is_main=""
path=""
branch=""
status=""
;;
"is_main "*)
is_main="${line#is_main }"
;;
"path "*)
path=$(_tsv_unescape_field "${line#path }")
;;
"branch "*)
branch=$(_tsv_unescape_field "${line#branch }")
;;
"status "*)
status="${line#status }"
;;
esac
done <<EOF
$records
EOF

if [ -d "$base_dir" ]; then
# Find all worktree directories and output: path<tab>branch<tab>status
# Exclude the base directory itself to avoid matching when prefix is empty
find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do
# Skip the base directory itself
[ "$dir" = "$base_dir" ] && continue
local branch status
branch=$(current_branch "$dir")
[ -z "$branch" ] && branch="(detached)"
status=$(worktree_status "$dir")
printf "%s\t%s\t%s\n" "$dir" "$branch" "$status"
done | LC_ALL=C sort -k2,2
if [ -n "$path" ]; then
if [ "$is_main" = "1" ]; then
printf "%s\t%s\t%s\n" "$path" "$branch" "$status"
else
linked_rows="${linked_rows}${path}"$'\t'"${branch}"$'\t'"${status}"$'\n'
fi
fi

if [ -n "$linked_rows" ]; then
printf "%s" "$linked_rows" | LC_ALL=C sort -t "$(printf '\t')" -k2,2 -k1,1
fi
return 0
fi
Expand All @@ -41,24 +68,54 @@ cmd_list() {
printf "%-30s %s\n" "BRANCH" "PATH"
printf "%-30s %s\n" "------" "----"

# Always show repo root first
local branch
branch=$(current_branch "$repo_root")
printf "%-30s %s\n" "$branch [main repo]" "$repo_root"
local is_main="" path="" branch="" status="" linked_rows="" line
while IFS= read -r line; do
case "$line" in
"")
[ -z "$path" ] && continue
if [ "$is_main" = "1" ]; then
printf "%-30s %s\n" "$branch [main repo]" "$path"
else
linked_rows="${linked_rows}${branch}"$'\t'"${path}"$'\n'
fi
is_main=""
path=""
branch=""
status=""
;;
"is_main "*)
is_main="${line#is_main }"
;;
"path "*)
path=$(_tsv_unescape_field "${line#path }")
;;
"branch "*)
branch=$(_tsv_unescape_field "${line#branch }")
;;
"status "*)
status="${line#status }"
;;
esac
done <<EOF
$records
EOF

if [ -n "$path" ]; then
if [ "$is_main" = "1" ]; then
printf "%-30s %s\n" "$branch [main repo]" "$path"
else
linked_rows="${linked_rows}${branch}"$'\t'"${path}"$'\n'
fi
fi

# Show worktrees sorted by branch name
if [ -d "$base_dir" ]; then
find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do
# Skip the base directory itself
[ "$dir" = "$base_dir" ] && continue
local branch
branch=$(current_branch "$dir")
[ -z "$branch" ] && branch="(detached)"
printf "%-30s %s\n" "$branch" "$dir"
done | LC_ALL=C sort -k1,1
if [ -n "$linked_rows" ]; then
printf "%s" "$linked_rows" | LC_ALL=C sort -t "$(printf '\t')" -k1,1 -k2,2 | while IFS=$'\t' read -r branch path; do
[ -z "$path" ] && continue
printf "%-30s %s\n" "$branch" "$path"
done
fi

echo ""
echo ""
echo "Tip: Use 'git gtr list --porcelain' for machine-readable output"
}
}
Loading
Loading