Skip to content

Commit 7903bf4

Browse files
authored
Use registered worktree inventory (#178)
* Use registered worktree inventory * Address CodeRabbit review: safe worktree records * Address CI inventory assertion * Address CodeRabbit review: worktree root context * Address CodeRabbit review: escape resolved paths * Address CodeRabbit review: resolve escape roundtrip * Address CodeRabbit review: escape inventory paths
1 parent 32dc340 commit 7903bf4

9 files changed

Lines changed: 594 additions & 135 deletions

File tree

lib/commands/clean.sh

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ _clean_should_skip() {
7272
_clean_merged() {
7373
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:-}"
7474

75+
# base_dir and prefix are kept for the helper contract. Merged cleanup uses
76+
# Git's registry so nested registered worktrees are processed directly.
77+
: "$base_dir" "$prefix"
78+
7579
log_step "Checking for worktrees with merged PRs/MRs..."
7680

7781
local provider
@@ -84,57 +88,77 @@ _clean_merged() {
8488
local removed=0 skipped=0
8589
local main_branch
8690
main_branch=$(current_branch "$repo_root")
87-
88-
for dir in "$base_dir/${prefix}"*; do
89-
[ -d "$dir" ] || continue
90-
91-
local branch
92-
branch=$(current_branch "$dir") || true
93-
local branch_tip
94-
branch_tip=$(git -C "$dir" rev-parse HEAD 2>/dev/null || true)
95-
96-
# Skip main repo branch silently (not counted)
97-
[ "$branch" = "$main_branch" ] && continue
98-
99-
# Check if branch has a merged PR/MR
100-
if check_branch_merged "$provider" "$branch" "$target_ref" "$branch_tip"; then
101-
if _clean_should_skip "$dir" "$branch" "$force" "$active_worktree_path"; then
102-
skipped=$((skipped + 1))
103-
continue
104-
fi
105-
106-
if [ "$dry_run" -eq 1 ]; then
107-
log_info "[dry-run] Would remove: $branch ($dir)"
108-
removed=$((removed + 1))
109-
elif [ "$yes_mode" -eq 1 ] || prompt_yes_no "Remove worktree and delete branch '$branch'?"; then
110-
log_step "Removing worktree: $branch"
111-
112-
if ! run_hooks_in preRemove "$dir" \
113-
REPO_ROOT="$repo_root" \
114-
WORKTREE_PATH="$dir" \
115-
BRANCH="$branch"; then
116-
log_warn "Pre-remove hook failed for $branch, skipping"
117-
skipped=$((skipped + 1))
118-
continue
119-
fi
120-
121-
if remove_worktree "$dir" "$force"; then
122-
git branch -d "$branch" 2>/dev/null || git branch -D "$branch" 2>/dev/null || true
123-
removed=$((removed + 1))
124-
125-
if ! run_hooks postRemove \
126-
REPO_ROOT="$repo_root" \
127-
WORKTREE_PATH="$dir" \
128-
BRANCH="$branch"; then
129-
log_warn "Post-remove hook failed for $branch"
91+
local records
92+
records=$(list_worktree_records "$repo_root")
93+
94+
local is_main="" dir="" branch="" line
95+
while IFS= read -r line; do
96+
case "$line" in
97+
"")
98+
if [ -n "$dir" ] && [ "$is_main" != "1" ]; then
99+
local branch_tip
100+
branch_tip=$(git -C "$dir" rev-parse HEAD 2>/dev/null || true)
101+
102+
# Skip main repo branch silently (not counted)
103+
[ "$branch" = "$main_branch" ] && continue
104+
105+
# Check if branch has a merged PR/MR
106+
if check_branch_merged "$provider" "$branch" "$target_ref" "$branch_tip"; then
107+
if _clean_should_skip "$dir" "$branch" "$force" "$active_worktree_path"; then
108+
skipped=$((skipped + 1))
109+
continue
110+
fi
111+
112+
if [ "$dry_run" -eq 1 ]; then
113+
log_info "[dry-run] Would remove: $branch ($dir)"
114+
removed=$((removed + 1))
115+
elif [ "$yes_mode" -eq 1 ] || prompt_yes_no "Remove worktree and delete branch '$branch'?"; then
116+
log_step "Removing worktree: $branch"
117+
118+
if ! run_hooks_in preRemove "$dir" \
119+
REPO_ROOT="$repo_root" \
120+
WORKTREE_PATH="$dir" \
121+
BRANCH="$branch"; then
122+
log_warn "Pre-remove hook failed for $branch, skipping"
123+
skipped=$((skipped + 1))
124+
continue
125+
fi
126+
127+
if remove_worktree "$dir" "$force"; then
128+
git branch -d "$branch" 2>/dev/null || git branch -D "$branch" 2>/dev/null || true
129+
removed=$((removed + 1))
130+
131+
if ! run_hooks postRemove \
132+
REPO_ROOT="$repo_root" \
133+
WORKTREE_PATH="$dir" \
134+
BRANCH="$branch"; then
135+
log_warn "Post-remove hook failed for $branch"
136+
fi
137+
fi
138+
else
139+
log_warn "Skipped: $branch (user declined)"
140+
skipped=$((skipped + 1))
141+
fi
130142
fi
131143
fi
132-
else
133-
log_warn "Skipped: $branch (user declined)"
134-
skipped=$((skipped + 1))
135-
fi
136-
fi
137-
done
144+
is_main=""
145+
dir=""
146+
branch=""
147+
;;
148+
"is_main "*)
149+
is_main="${line#is_main }"
150+
;;
151+
"path "*)
152+
dir=$(_tsv_unescape_field "${line#path }")
153+
;;
154+
"branch "*)
155+
branch=$(_tsv_unescape_field "${line#branch }")
156+
;;
157+
esac
158+
done <<EOF
159+
$records
160+
161+
EOF
138162

139163
echo ""
140164
if [ "$dry_run" -eq 1 ]; then

lib/commands/copy.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ cmd_copy() {
6262
# Build target list for --all mode
6363
if [ "$all_mode" -eq 1 ]; then
6464
local all_branches
65-
all_branches=$(list_worktree_branches "$base_dir" "$prefix")
65+
all_branches=$(list_worktree_branches "$base_dir" "$prefix" "$repo_root")
6666
if [ -z "$all_branches" ]; then
6767
log_error "No worktrees found"
6868
exit 1

lib/commands/list.sh

Lines changed: 89 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,55 @@ cmd_list() {
99

1010
resolve_repo_context || exit 1
1111

12-
local repo_root="$_ctx_repo_root" base_dir="$_ctx_base_dir" prefix="$_ctx_prefix"
12+
local repo_root="$_ctx_repo_root"
13+
local records
14+
records=$(list_worktree_records "$repo_root")
1315

1416
# Machine-readable output (porcelain)
1517
if [ "$porcelain" -eq 1 ]; then
1618
# Output: path<tab>branch<tab>status
17-
local branch status
18-
branch=$(current_branch "$repo_root")
19-
status=$(worktree_status "$repo_root")
20-
printf "%s\t%s\t%s\n" "$repo_root" "$branch" "$status"
19+
local is_main="" path="" branch="" status="" linked_rows="" line
20+
while IFS= read -r line; do
21+
case "$line" in
22+
"")
23+
[ -z "$path" ] && continue
24+
if [ "$is_main" = "1" ]; then
25+
printf "%s\t%s\t%s\n" "$path" "$branch" "$status"
26+
else
27+
linked_rows="${linked_rows}${path}"$'\t'"${branch}"$'\t'"${status}"$'\n'
28+
fi
29+
is_main=""
30+
path=""
31+
branch=""
32+
status=""
33+
;;
34+
"is_main "*)
35+
is_main="${line#is_main }"
36+
;;
37+
"path "*)
38+
path=$(_tsv_unescape_field "${line#path }")
39+
;;
40+
"branch "*)
41+
branch=$(_tsv_unescape_field "${line#branch }")
42+
;;
43+
"status "*)
44+
status="${line#status }"
45+
;;
46+
esac
47+
done <<EOF
48+
$records
49+
EOF
2150

22-
if [ -d "$base_dir" ]; then
23-
# Find all worktree directories and output: path<tab>branch<tab>status
24-
# Exclude the base directory itself to avoid matching when prefix is empty
25-
find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do
26-
# Skip the base directory itself
27-
[ "$dir" = "$base_dir" ] && continue
28-
local branch status
29-
branch=$(current_branch "$dir")
30-
[ -z "$branch" ] && branch="(detached)"
31-
status=$(worktree_status "$dir")
32-
printf "%s\t%s\t%s\n" "$dir" "$branch" "$status"
33-
done | LC_ALL=C sort -k2,2
51+
if [ -n "$path" ]; then
52+
if [ "$is_main" = "1" ]; then
53+
printf "%s\t%s\t%s\n" "$path" "$branch" "$status"
54+
else
55+
linked_rows="${linked_rows}${path}"$'\t'"${branch}"$'\t'"${status}"$'\n'
56+
fi
57+
fi
58+
59+
if [ -n "$linked_rows" ]; then
60+
printf "%s" "$linked_rows" | LC_ALL=C sort -t "$(printf '\t')" -k2,2 -k1,1
3461
fi
3562
return 0
3663
fi
@@ -41,24 +68,54 @@ cmd_list() {
4168
printf "%-30s %s\n" "BRANCH" "PATH"
4269
printf "%-30s %s\n" "------" "----"
4370

44-
# Always show repo root first
45-
local branch
46-
branch=$(current_branch "$repo_root")
47-
printf "%-30s %s\n" "$branch [main repo]" "$repo_root"
71+
local is_main="" path="" branch="" status="" linked_rows="" line
72+
while IFS= read -r line; do
73+
case "$line" in
74+
"")
75+
[ -z "$path" ] && continue
76+
if [ "$is_main" = "1" ]; then
77+
printf "%-30s %s\n" "$branch [main repo]" "$path"
78+
else
79+
linked_rows="${linked_rows}${branch}"$'\t'"${path}"$'\n'
80+
fi
81+
is_main=""
82+
path=""
83+
branch=""
84+
status=""
85+
;;
86+
"is_main "*)
87+
is_main="${line#is_main }"
88+
;;
89+
"path "*)
90+
path=$(_tsv_unescape_field "${line#path }")
91+
;;
92+
"branch "*)
93+
branch=$(_tsv_unescape_field "${line#branch }")
94+
;;
95+
"status "*)
96+
status="${line#status }"
97+
;;
98+
esac
99+
done <<EOF
100+
$records
101+
EOF
102+
103+
if [ -n "$path" ]; then
104+
if [ "$is_main" = "1" ]; then
105+
printf "%-30s %s\n" "$branch [main repo]" "$path"
106+
else
107+
linked_rows="${linked_rows}${branch}"$'\t'"${path}"$'\n'
108+
fi
109+
fi
48110

49-
# Show worktrees sorted by branch name
50-
if [ -d "$base_dir" ]; then
51-
find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do
52-
# Skip the base directory itself
53-
[ "$dir" = "$base_dir" ] && continue
54-
local branch
55-
branch=$(current_branch "$dir")
56-
[ -z "$branch" ] && branch="(detached)"
57-
printf "%-30s %s\n" "$branch" "$dir"
58-
done | LC_ALL=C sort -k1,1
111+
if [ -n "$linked_rows" ]; then
112+
printf "%s" "$linked_rows" | LC_ALL=C sort -t "$(printf '\t')" -k1,1 -k2,2 | while IFS=$'\t' read -r branch path; do
113+
[ -z "$path" ] && continue
114+
printf "%-30s %s\n" "$branch" "$path"
115+
done
59116
fi
60117

61118
echo ""
62119
echo ""
63120
echo "Tip: Use 'git gtr list --porcelain' for machine-readable output"
64-
}
121+
}

0 commit comments

Comments
 (0)