-
Notifications
You must be signed in to change notification settings - Fork 84
Expand file tree
/
Copy pathclean.sh
More file actions
242 lines (204 loc) · 7.52 KB
/
clean.sh
File metadata and controls
242 lines (204 loc) · 7.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#!/usr/bin/env bash
# Clean command (remove prunable worktrees)
# Detect hosting provider with error messaging.
# Prints provider name on success; returns 1 on failure.
_clean_detect_provider() {
local provider
provider=$(detect_provider) || true
if [ -n "$provider" ]; then
printf "%s" "$provider"
return 0
fi
local remote_url
remote_url=$(git remote get-url origin 2>/dev/null || true)
if [ -z "$remote_url" ]; then
log_error "No remote URL configured for 'origin'"
else
# Sanitize URL to avoid leaking embedded credentials (e.g., https://token@host/...)
local safe_url="${remote_url%%@*}"
if [ "$safe_url" != "$remote_url" ]; then
safe_url="<redacted>@${remote_url#*@}"
fi
log_error "Could not detect hosting provider from remote URL: $safe_url"
log_info "Set manually: git gtr config set gtr.provider github (or gitlab)"
fi
return 1
}
# Check if a worktree should be skipped during merged cleanup.
# Returns 0 if should skip, 1 if should process.
# Usage: _clean_should_skip <dir> <branch> [force] [active_worktree_path]
_clean_should_skip() {
local dir="$1" branch="$2" force="${3:-0}" active_worktree_path="${4:-}"
local dir_canonical="$dir"
local active_worktree_canonical="$active_worktree_path"
if [ -n "$active_worktree_path" ]; then
dir_canonical=$(canonicalize_path "$dir" || printf "%s" "$dir")
active_worktree_canonical=$(canonicalize_path "$active_worktree_path" || printf "%s" "$active_worktree_path")
fi
if [ -n "$active_worktree_path" ] && [ "$dir_canonical" = "$active_worktree_canonical" ]; then
log_warn "Skipping $branch (current active worktree)"
return 0
fi
if [ -z "$branch" ] || [ "$branch" = "(detached)" ]; then
log_warn "Skipping $dir (detached HEAD)"
return 0
fi
if [ "$force" -eq 0 ]; then
if ! git -C "$dir" diff --quiet 2>/dev/null || \
! git -C "$dir" diff --cached --quiet 2>/dev/null; then
log_warn "Skipping $branch (has uncommitted changes)"
return 0
fi
if [ -n "$(git -C "$dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then
log_warn "Skipping $branch (has untracked files)"
return 0
fi
fi
return 1
}
# Remove worktrees whose PRs/MRs are merged (handles squash merges)
# Usage: _clean_merged repo_root base_dir prefix yes_mode dry_run [force] [active_worktree_path] [target_ref]
_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
provider=$(_clean_detect_provider) || exit 1
ensure_provider_cli "$provider" || exit 1
log_step "Fetching from origin..."
git fetch origin --prune 2>/dev/null || log_warn "Could not fetch from origin"
local removed=0 skipped=0
local main_branch
main_branch=$(current_branch "$repo_root")
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
is_main=""
dir=""
branch=""
;;
"is_main "*)
is_main="${line#is_main }"
;;
"path "*)
dir="${line#path }"
;;
"branch "*)
branch="${line#branch }"
;;
esac
done <<EOF
$records
EOF
echo ""
if [ "$dry_run" -eq 1 ]; then
log_info "Dry run complete. Would remove: $removed, Skipped: $skipped"
else
log_info "Merged cleanup complete. Removed: $removed, Skipped: $skipped"
fi
}
# shellcheck disable=SC2154 # _arg_* set by parse_args, _ctx_* set by resolve_*
cmd_clean() {
local _spec
_spec="--merged
--to: value
--yes|-y
--dry-run|-n
--force|-f"
parse_args "$_spec" "$@"
local merged_mode="${_arg_merged:-0}"
local target_ref="${_arg_to:-}"
local yes_mode="${_arg_yes:-0}"
local dry_run="${_arg_dry_run:-0}"
local force="${_arg_force:-0}"
local active_worktree_path=""
if [ -n "$target_ref" ] && [ "$merged_mode" -ne 1 ]; then
log_error "--to can only be used with --merged"
return 1
fi
log_step "Cleaning up stale worktrees..."
# Run git worktree prune
if git worktree prune 2>/dev/null; then
log_info "Pruned stale worktree administrative files"
fi
resolve_repo_context || exit 1
local repo_root="$_ctx_repo_root" base_dir="$_ctx_base_dir" prefix="$_ctx_prefix"
active_worktree_path=$(git rev-parse --show-toplevel 2>/dev/null || true)
if [ -n "$active_worktree_path" ]; then
active_worktree_path=$(canonicalize_path "$active_worktree_path" || printf "%s" "$active_worktree_path")
fi
if [ ! -d "$base_dir" ]; then
log_info "No worktrees directory to clean"
return 0
fi
# Find and remove empty directories
local cleaned=0
local empty_dirs
empty_dirs=$(find "$base_dir" -maxdepth 1 -type d -empty 2>/dev/null | grep -Fxv "$base_dir" || true)
if [ -n "$empty_dirs" ]; then
while IFS= read -r dir; do
if [ -n "$dir" ]; then
if rmdir "$dir" 2>/dev/null; then
cleaned=$((cleaned + 1))
log_info "Removed empty directory: $(basename "$dir")"
fi
fi
done <<EOF
$empty_dirs
EOF
fi
if [ "$cleaned" -gt 0 ]; then
log_info "Cleanup complete ($cleaned director$([ "$cleaned" -eq 1 ] && echo 'y' || echo 'ies') removed)"
else
log_info "Cleanup complete (no empty directories found)"
fi
# --merged mode: remove worktrees with merged PRs/MRs (handles squash merges)
if [ "$merged_mode" -eq 1 ]; then
_clean_merged "$repo_root" "$base_dir" "$prefix" "$yes_mode" "$dry_run" "$force" "$active_worktree_path" "$target_ref"
fi
}