Skip to content

Commit 1f9a0f7

Browse files
committed
Address CodeRabbit review: safe worktree records
1 parent c88943f commit 1f9a0f7

4 files changed

Lines changed: 235 additions & 80 deletions

File tree

lib/commands/clean.sh

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -91,57 +91,73 @@ _clean_merged() {
9191
local records
9292
records=$(list_worktree_records "$repo_root")
9393

94-
local is_main dir branch _status
95-
while IFS=$'\t' read -r is_main dir branch _status; do
96-
[ -z "$dir" ] && continue
97-
[ "$is_main" = "1" ] && continue
98-
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"
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
136142
fi
137143
fi
138-
else
139-
log_warn "Skipped: $branch (user declined)"
140-
skipped=$((skipped + 1))
141-
fi
142-
fi
144+
is_main=""
145+
dir=""
146+
branch=""
147+
;;
148+
"is_main "*)
149+
is_main="${line#is_main }"
150+
;;
151+
"path "*)
152+
dir="${line#path }"
153+
;;
154+
"branch "*)
155+
branch="${line#branch }"
156+
;;
157+
esac
143158
done <<EOF
144159
$records
160+
145161
EOF
146162

147163
echo ""

lib/commands/list.sh

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,45 @@ cmd_list() {
1616
# Machine-readable output (porcelain)
1717
if [ "$porcelain" -eq 1 ]; then
1818
# Output: path<tab>branch<tab>status
19-
local is_main path branch status linked_rows=""
20-
while IFS=$'\t' read -r is_main path branch status; do
21-
[ -z "$path" ] && continue
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="${line#path }"
39+
;;
40+
"branch "*)
41+
branch="${line#branch }"
42+
;;
43+
"status "*)
44+
status="${line#status }"
45+
;;
46+
esac
47+
done <<EOF
48+
$records
49+
EOF
50+
51+
if [ -n "$path" ]; then
2252
if [ "$is_main" = "1" ]; then
2353
printf "%s\t%s\t%s\n" "$path" "$branch" "$status"
2454
else
2555
linked_rows="${linked_rows}${path}"$'\t'"${branch}"$'\t'"${status}"$'\n'
2656
fi
27-
done <<EOF
28-
$records
29-
EOF
57+
fi
3058

3159
if [ -n "$linked_rows" ]; then
3260
printf "%s" "$linked_rows" | LC_ALL=C sort -t "$(printf '\t')" -k2,2 -k1,1
@@ -40,17 +68,45 @@ EOF
4068
printf "%-30s %s\n" "BRANCH" "PATH"
4169
printf "%-30s %s\n" "------" "----"
4270

43-
local is_main path branch status linked_rows=""
44-
while IFS=$'\t' read -r is_main path branch status; do
45-
[ -z "$path" ] && continue
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="${line#path }"
91+
;;
92+
"branch "*)
93+
branch="${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
46104
if [ "$is_main" = "1" ]; then
47105
printf "%-30s %s\n" "$branch [main repo]" "$path"
48106
else
49107
linked_rows="${linked_rows}${branch}"$'\t'"${path}"$'\n'
50108
fi
51-
done <<EOF
52-
$records
53-
EOF
109+
fi
54110

55111
if [ -n "$linked_rows" ]; then
56112
printf "%s" "$linked_rows" | LC_ALL=C sort -t "$(printf '\t')" -k1,1 -k2,2 | while IFS=$'\t' read -r branch path; do

lib/core.sh

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,15 @@ _emit_worktree_record() {
219219
[ -z "$branch" ] && branch="(detached)"
220220
status=$(_worktree_record_status "$wt_detached" "$wt_locked" "$wt_prunable")
221221

222-
printf "%s\t%s\t%s\t%s\n" "$is_main" "$wt_path" "$branch" "$status"
222+
printf "is_main %s\n" "$is_main"
223+
printf "path %s\n" "$wt_path"
224+
printf "branch %s\n" "$branch"
225+
printf "status %s\n\n" "$status"
223226
}
224227

225228
# List registered git worktrees for a repository.
226229
# Usage: list_worktree_records repo_root
227-
# Output: is_main<TAB>path<TAB>branch<TAB>status
230+
# Output: blank-line-delimited records with is_main/path/branch/status fields
228231
list_worktree_records() {
229232
local repo_root="$1"
230233
local repo_root_canonical
@@ -293,17 +296,47 @@ worktree_status() {
293296
local repo_root
294297
repo_root=$(_resolve_main_repo_root) || return 1
295298

296-
local is_main path branch record_status
297-
while IFS=$'\t' read -r is_main path branch record_status; do
298-
if [ "$path" = "$target_path" ] || [ "$path" = "$target_path_canonical" ]; then
299-
found=1
300-
status="$record_status"
301-
break
302-
fi
299+
local is_main="" path="" branch="" record_status="" line path_canonical
300+
while IFS= read -r line; do
301+
case "$line" in
302+
"")
303+
[ -z "$path" ] && continue
304+
path_canonical=$(canonicalize_path "$path" || printf "%s" "$path")
305+
if [ "$path" = "$target_path" ] || [ "$path_canonical" = "$target_path_canonical" ]; then
306+
found=1
307+
status="$record_status"
308+
break
309+
fi
310+
is_main=""
311+
path=""
312+
branch=""
313+
record_status=""
314+
;;
315+
"is_main "*)
316+
is_main="${line#is_main }"
317+
;;
318+
"path "*)
319+
path="${line#path }"
320+
;;
321+
"branch "*)
322+
branch="${line#branch }"
323+
;;
324+
"status "*)
325+
record_status="${line#status }"
326+
;;
327+
esac
303328
done <<EOF
304329
$(list_worktree_records "$repo_root")
305330
EOF
306331

332+
if [ "$found" -eq 0 ] && [ -n "$path" ]; then
333+
path_canonical=$(canonicalize_path "$path" || printf "%s" "$path")
334+
if [ "$path" = "$target_path" ] || [ "$path_canonical" = "$target_path_canonical" ]; then
335+
found=1
336+
status="$record_status"
337+
fi
338+
fi
339+
307340
# If worktree not found in git's list
308341
if [ "$found" -eq 0 ]; then
309342
status="missing"
@@ -361,16 +394,37 @@ resolve_target() {
361394
fi
362395

363396
# Last resort: ask git for all worktrees (catches non-gtr-managed worktrees)
364-
local is_main wt_path wt_branch _wt_status
365-
while IFS=$'\t' read -r is_main wt_path wt_branch _wt_status; do
366-
if [ "$wt_branch" = "$identifier" ]; then
367-
printf "%s\t%s\t%s\n" "$is_main" "$wt_path" "$wt_branch"
368-
return 0
369-
fi
397+
local is_main="" wt_path="" wt_branch="" line
398+
while IFS= read -r line; do
399+
case "$line" in
400+
"")
401+
if [ "$wt_branch" = "$identifier" ]; then
402+
printf "%s\t%s\t%s\n" "$is_main" "$wt_path" "$wt_branch"
403+
return 0
404+
fi
405+
is_main=""
406+
wt_path=""
407+
wt_branch=""
408+
;;
409+
"is_main "*)
410+
is_main="${line#is_main }"
411+
;;
412+
"path "*)
413+
wt_path="${line#path }"
414+
;;
415+
"branch "*)
416+
wt_branch="${line#branch }"
417+
;;
418+
esac
370419
done <<EOF
371420
$(list_worktree_records "$repo_root")
372421
EOF
373422

423+
if [ "$wt_branch" = "$identifier" ]; then
424+
printf "%s\t%s\t%s\n" "$is_main" "$wt_path" "$wt_branch"
425+
return 0
426+
fi
427+
374428
log_error "Worktree not found for branch: $identifier"
375429
return 1
376430
}
@@ -622,13 +676,32 @@ list_worktree_branches() {
622676
local records
623677
records=$(list_worktree_records "$repo_root")
624678

625-
local is_main path branch status
626-
while IFS=$'\t' read -r is_main path branch status; do
627-
[ "$is_main" = "1" ] && continue
628-
[ -z "$branch" ] && continue
629-
[ "$branch" = "(detached)" ] && continue
630-
printf "%s\n" "$branch"
679+
local is_main="" path="" branch="" line
680+
while IFS= read -r line; do
681+
case "$line" in
682+
"")
683+
if [ "$is_main" != "1" ] && [ -n "$branch" ] && [ "$branch" != "(detached)" ]; then
684+
printf "%s\n" "$branch"
685+
fi
686+
is_main=""
687+
path=""
688+
branch=""
689+
;;
690+
"is_main "*)
691+
is_main="${line#is_main }"
692+
;;
693+
"path "*)
694+
path="${line#path }"
695+
;;
696+
"branch "*)
697+
branch="${line#branch }"
698+
;;
699+
esac
631700
done <<EOF
632701
$records
633702
EOF
703+
704+
if [ "$is_main" != "1" ] && [ -n "$branch" ] && [ "$branch" != "(detached)" ]; then
705+
printf "%s\n" "$branch"
706+
fi
634707
}

tests/core_resolve_target.bats

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ teardown() {
124124
git -C "$TEST_REPO" worktree unlock "$TEST_WORKTREES_DIR/records-locked"
125125
}
126126

127+
@test "worktree_status handles registered path containing tab" {
128+
local tab_path="$TEST_WORKTREES_DIR/with"$'\t'"tab"
129+
git -C "$TEST_REPO" worktree add "$tab_path" -b tab-path --quiet
130+
131+
local status
132+
status=$(worktree_status "$tab_path")
133+
134+
[ "$status" = "ok" ]
135+
}
136+
127137
# ── discover_repo_root from worktree ──────────────────────────────────────────
128138

129139
@test "discover_repo_root returns main repo root when called from a worktree" {

0 commit comments

Comments
 (0)