gitbash 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/branch.sh +4 -4
- package/commands/cleanstash.sh +4 -4
- package/commands/cleanup.sh +89 -46
- package/commands/commits.sh +4 -3
- package/commands/stale.sh +12 -7
- package/commands/stashes.sh +4 -4
- package/commands/status.sh +4 -3
- package/commands/switch.sh +10 -5
- package/commands/unstash.sh +4 -4
- package/package.json +1 -1
- package/commands/stale.zsh +0 -403
package/commands/branch.sh
CHANGED
|
@@ -147,15 +147,15 @@ EOF
|
|
|
147
147
|
# 3. Run fzf menu
|
|
148
148
|
# -----------------------------
|
|
149
149
|
local selection
|
|
150
|
-
selection=$(
|
|
151
|
-
| fzf --prompt="Branch action > " \
|
|
150
|
+
selection=$(fzf --prompt="Branch action > " \
|
|
152
151
|
-i \
|
|
153
152
|
--reverse \
|
|
154
153
|
--border \
|
|
155
154
|
--header="What would you like to do?" \
|
|
156
155
|
--no-multi \
|
|
157
|
-
--bind=enter:accept
|
|
158
|
-
|
|
156
|
+
--bind=enter:accept \
|
|
157
|
+
<<< "$choices"
|
|
158
|
+
) </dev/tty || true
|
|
159
159
|
|
|
160
160
|
# ESC or Ctrl-C
|
|
161
161
|
if [[ -z "$selection" ]]; then
|
package/commands/cleanstash.sh
CHANGED
|
@@ -108,8 +108,7 @@ EOF
|
|
|
108
108
|
# 3. Run fzf full-screen picker with preview (multi-select)
|
|
109
109
|
# -----------------------------
|
|
110
110
|
local selection
|
|
111
|
-
selection=$(
|
|
112
|
-
| fzf --prompt="Available stashes > " \
|
|
111
|
+
selection=$(fzf --prompt="Available stashes > " \
|
|
113
112
|
-i \
|
|
114
113
|
--reverse \
|
|
115
114
|
--border \
|
|
@@ -123,8 +122,9 @@ EOF
|
|
|
123
122
|
else
|
|
124
123
|
echo "No preview";
|
|
125
124
|
fi
|
|
126
|
-
'
|
|
127
|
-
|
|
125
|
+
' \
|
|
126
|
+
<<< "$choices"
|
|
127
|
+
) </dev/tty || true
|
|
128
128
|
|
|
129
129
|
# ESC or Ctrl-C
|
|
130
130
|
if [[ -z "$selection" ]]; then
|
package/commands/cleanup.sh
CHANGED
|
@@ -70,7 +70,6 @@ EOF
|
|
|
70
70
|
;;
|
|
71
71
|
esac
|
|
72
72
|
done
|
|
73
|
-
|
|
74
73
|
# -----------------------------
|
|
75
74
|
# 1. Check prerequisites
|
|
76
75
|
# -----------------------------
|
|
@@ -110,7 +109,11 @@ EOF
|
|
|
110
109
|
if [[ "$json_mode" == false ]]; then
|
|
111
110
|
echo "Fetching and pruning remote..."
|
|
112
111
|
fi
|
|
113
|
-
git fetch --prune origin 2>/dev/null
|
|
112
|
+
if ! git fetch --prune origin 2>/dev/null; then
|
|
113
|
+
if [[ "$json_mode" == false ]]; then
|
|
114
|
+
echo "⚠ Fetch failed; continuing with local data." >&2
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
114
117
|
|
|
115
118
|
# -----------------------------
|
|
116
119
|
# 3. Detect base branch
|
|
@@ -141,12 +144,15 @@ EOF
|
|
|
141
144
|
|
|
142
145
|
# Get list of remote branches (without origin/ prefix)
|
|
143
146
|
local remote_branches
|
|
144
|
-
remote_branches=$(git for-each-ref --format='%(refname:short)' refs/remotes/origin | sed 's|^origin/||' | grep -v '^HEAD$')
|
|
147
|
+
remote_branches=$(git for-each-ref --format='%(refname:short)' refs/remotes/origin | sed 's|^origin/||' | grep -v '^HEAD$' || true)
|
|
145
148
|
|
|
146
149
|
# Arrays to hold branches by category (format: timestamp|branch|relative_date|author_email|author_name)
|
|
147
|
-
local merged_branches
|
|
148
|
-
local stale_branches
|
|
149
|
-
local recent_branches
|
|
150
|
+
local -a merged_branches
|
|
151
|
+
local -a stale_branches
|
|
152
|
+
local -a recent_branches
|
|
153
|
+
merged_branches=()
|
|
154
|
+
stale_branches=()
|
|
155
|
+
recent_branches=()
|
|
150
156
|
|
|
151
157
|
# Process each local branch
|
|
152
158
|
while IFS= read -r branch; do
|
|
@@ -186,15 +192,26 @@ EOF
|
|
|
186
192
|
fi
|
|
187
193
|
done < <(git for-each-ref --format='%(refname:short)' refs/heads)
|
|
188
194
|
|
|
189
|
-
# Sort each category by timestamp (most recent first)
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
# Sort each category by timestamp (most recent first), compatible with bash 3.x (no mapfile)
|
|
196
|
+
_sort_desc() {
|
|
197
|
+
local _var="$1"
|
|
198
|
+
shift
|
|
199
|
+
local _out=()
|
|
200
|
+
while IFS= read -r line; do
|
|
201
|
+
_out+=("$line")
|
|
202
|
+
done < <(printf '%s\n' "$@" | sort -t'|' -k1 -rn)
|
|
203
|
+
# shellcheck disable=SC2034 # assigned via eval to caller var
|
|
204
|
+
eval "$_var=(\"\${_out[@]}\")"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if [[ ${merged_branches+set} && ${#merged_branches[@]} -gt 0 ]]; then
|
|
208
|
+
_sort_desc merged_branches "${merged_branches[@]}"
|
|
192
209
|
fi
|
|
193
|
-
if [[ ${#stale_branches[@]} -gt 0 ]]; then
|
|
194
|
-
|
|
210
|
+
if [[ ${stale_branches+set} && ${#stale_branches[@]} -gt 0 ]]; then
|
|
211
|
+
_sort_desc stale_branches "${stale_branches[@]}"
|
|
195
212
|
fi
|
|
196
|
-
if [[ ${#recent_branches[@]} -gt 0 ]]; then
|
|
197
|
-
|
|
213
|
+
if [[ ${recent_branches+set} && ${#recent_branches[@]} -gt 0 ]]; then
|
|
214
|
+
_sort_desc recent_branches "${recent_branches[@]}"
|
|
198
215
|
fi
|
|
199
216
|
|
|
200
217
|
# -----------------------------
|
|
@@ -216,7 +233,18 @@ EOF
|
|
|
216
233
|
}
|
|
217
234
|
|
|
218
235
|
# Process all branches for JSON output
|
|
219
|
-
|
|
236
|
+
local all_entries=()
|
|
237
|
+
if [[ ${merged_branches+set} ]]; then
|
|
238
|
+
all_entries+=("${merged_branches[@]}")
|
|
239
|
+
fi
|
|
240
|
+
if [[ ${stale_branches+set} ]]; then
|
|
241
|
+
all_entries+=("${stale_branches[@]}")
|
|
242
|
+
fi
|
|
243
|
+
if [[ ${recent_branches+set} ]]; then
|
|
244
|
+
all_entries+=("${recent_branches[@]}")
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
for entry in "${all_entries[@]}"; do
|
|
220
248
|
[[ -z "$entry" ]] && continue
|
|
221
249
|
|
|
222
250
|
local ts name rel_date email author
|
|
@@ -255,7 +283,8 @@ EOF
|
|
|
255
283
|
local max_branch_len=60
|
|
256
284
|
|
|
257
285
|
# Add merged branches (pre-selected)
|
|
258
|
-
|
|
286
|
+
if [[ ${merged_branches+set} ]]; then
|
|
287
|
+
for entry in "${merged_branches[@]}"; do
|
|
259
288
|
local branch date display_branch line
|
|
260
289
|
branch=$(echo "$entry" | cut -d'|' -f2)
|
|
261
290
|
date=$(echo "$entry" | cut -d'|' -f3)
|
|
@@ -267,36 +296,41 @@ EOF
|
|
|
267
296
|
branch_list+="$line"$'\n'
|
|
268
297
|
preselect_list+="$line"$'\n'
|
|
269
298
|
((idx++))
|
|
270
|
-
|
|
299
|
+
done
|
|
300
|
+
fi
|
|
271
301
|
|
|
272
302
|
# Add stale branches (pre-selected)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
303
|
+
if [[ ${stale_branches+set} ]]; then
|
|
304
|
+
for entry in "${stale_branches[@]}"; do
|
|
305
|
+
local branch date display_branch line
|
|
306
|
+
branch=$(echo "$entry" | cut -d'|' -f2)
|
|
307
|
+
date=$(echo "$entry" | cut -d'|' -f3)
|
|
308
|
+
display_branch="$branch"
|
|
309
|
+
if [[ ${#display_branch} -gt $max_branch_len ]]; then
|
|
310
|
+
display_branch="${display_branch:0:$((max_branch_len - 3))}..."
|
|
311
|
+
fi
|
|
312
|
+
line=$(printf "[STALE] %-${max_branch_len}s %s\t%s" "$display_branch" "$date" "$branch")
|
|
313
|
+
branch_list+="$line"$'\n'
|
|
314
|
+
preselect_list+="$line"$'\n'
|
|
315
|
+
((idx++))
|
|
316
|
+
done
|
|
317
|
+
fi
|
|
286
318
|
|
|
287
319
|
# Add recent branches (not pre-selected)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
320
|
+
if [[ ${recent_branches+set} ]]; then
|
|
321
|
+
for entry in "${recent_branches[@]}"; do
|
|
322
|
+
local branch date display_branch line
|
|
323
|
+
branch=$(echo "$entry" | cut -d'|' -f2)
|
|
324
|
+
date=$(echo "$entry" | cut -d'|' -f3)
|
|
325
|
+
display_branch="$branch"
|
|
326
|
+
if [[ ${#display_branch} -gt $max_branch_len ]]; then
|
|
327
|
+
display_branch="${display_branch:0:$((max_branch_len - 3))}..."
|
|
328
|
+
fi
|
|
329
|
+
line=$(printf "[RECENT] %-${max_branch_len}s %s\t%s" "$display_branch" "$date" "$branch")
|
|
330
|
+
branch_list+="$line"$'\n'
|
|
331
|
+
((idx++))
|
|
332
|
+
done
|
|
333
|
+
fi
|
|
300
334
|
|
|
301
335
|
if [[ -z "$branch_list" ]]; then
|
|
302
336
|
echo "No local branches to clean up."
|
|
@@ -316,8 +350,16 @@ EOF
|
|
|
316
350
|
# -----------------------------
|
|
317
351
|
# 7. Run fzf picker
|
|
318
352
|
# -----------------------------
|
|
319
|
-
local
|
|
320
|
-
local
|
|
353
|
+
local merged_count=0
|
|
354
|
+
local stale_count=0
|
|
355
|
+
local recent_count=0
|
|
356
|
+
|
|
357
|
+
[[ ${merged_branches+set} ]] && merged_count=${#merged_branches[@]}
|
|
358
|
+
[[ ${stale_branches+set} ]] && stale_count=${#stale_branches[@]}
|
|
359
|
+
[[ ${recent_branches+set} ]] && recent_count=${#recent_branches[@]}
|
|
360
|
+
|
|
361
|
+
local total_count=$((merged_count + stale_count + recent_count))
|
|
362
|
+
local preselect_count=$((merged_count + stale_count))
|
|
321
363
|
|
|
322
364
|
# Build toggle sequence for pre-selection (toggle first N items)
|
|
323
365
|
local toggle_sequence="first"
|
|
@@ -327,7 +369,7 @@ EOF
|
|
|
327
369
|
toggle_sequence+="+first"
|
|
328
370
|
|
|
329
371
|
local selection
|
|
330
|
-
selection=$(
|
|
372
|
+
selection=$(fzf \
|
|
331
373
|
--prompt="Local branches to clean up > " \
|
|
332
374
|
-i \
|
|
333
375
|
--reverse \
|
|
@@ -351,8 +393,9 @@ Found $total_count branches ($preselect_count pre-selected for deletion)" \
|
|
|
351
393
|
fi
|
|
352
394
|
' \
|
|
353
395
|
--preview-window=right:35% \
|
|
354
|
-
--bind "load:$toggle_sequence"
|
|
355
|
-
|
|
396
|
+
--bind "load:$toggle_sequence" \
|
|
397
|
+
< "$branch_file"
|
|
398
|
+
) </dev/tty || true
|
|
356
399
|
|
|
357
400
|
# Cleanup temp files
|
|
358
401
|
rm -f "$branch_file" "$preselect_file"
|
package/commands/commits.sh
CHANGED
|
@@ -111,7 +111,7 @@ EOF
|
|
|
111
111
|
# 4. Run fzf picker
|
|
112
112
|
# -----------------------------
|
|
113
113
|
local selection
|
|
114
|
-
selection=$(
|
|
114
|
+
selection=$(fzf \
|
|
115
115
|
--prompt="Commits on '$current_branch' > " \
|
|
116
116
|
-i \
|
|
117
117
|
--reverse \
|
|
@@ -137,8 +137,9 @@ Showing last $count commits" \
|
|
|
137
137
|
git show --color=always "$hash" 2>/dev/null | head -100;
|
|
138
138
|
fi
|
|
139
139
|
' \
|
|
140
|
-
--preview-window=right:50%
|
|
141
|
-
|
|
140
|
+
--preview-window=right:50% \
|
|
141
|
+
< "$commit_file"
|
|
142
|
+
) </dev/tty || true
|
|
142
143
|
|
|
143
144
|
# Cleanup temp file
|
|
144
145
|
rm -f "$commit_file"
|
package/commands/stale.sh
CHANGED
|
@@ -144,7 +144,11 @@ EOF
|
|
|
144
144
|
if [[ "$json_mode" == false ]]; then
|
|
145
145
|
echo "Fetching latest from remote..."
|
|
146
146
|
fi
|
|
147
|
-
git fetch --prune origin 2>/dev/null
|
|
147
|
+
if ! git fetch --prune origin 2>/dev/null; then
|
|
148
|
+
if [[ "$json_mode" == false ]]; then
|
|
149
|
+
echo "⚠ Fetch failed; continuing with local data." >&2
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
148
152
|
|
|
149
153
|
# -----------------------------
|
|
150
154
|
# 4. Build branch list sorted by date (oldest first)
|
|
@@ -172,7 +176,7 @@ EOF
|
|
|
172
176
|
fi
|
|
173
177
|
# Format: display branch (padded), date, author, TAB, full branch name
|
|
174
178
|
printf "%-${max_branch_length}s %-20s %-25s\t%s\n" "$display_branch" "$date" "$author" "$full_branch"
|
|
175
|
-
done)
|
|
179
|
+
done || true)
|
|
176
180
|
|
|
177
181
|
# Build stale branch list (only branches older than 3 months)
|
|
178
182
|
local stale_branch_list
|
|
@@ -192,7 +196,7 @@ EOF
|
|
|
192
196
|
# Format: display branch (padded), date, author, TAB, full branch name
|
|
193
197
|
printf "%-${max_branch_length}s %-20s %-25s\t%s\n" "$display_branch" "$date" "$author" "$full_branch"
|
|
194
198
|
fi
|
|
195
|
-
done)
|
|
199
|
+
done || true)
|
|
196
200
|
|
|
197
201
|
if [[ -z "$all_branch_list" ]]; then
|
|
198
202
|
if [[ "$json_mode" == true ]]; then
|
|
@@ -246,7 +250,7 @@ EOF
|
|
|
246
250
|
|
|
247
251
|
json_output+="{\"last_change_timestamp\":$timestamp,\"author_email\":\"$author_email\",\"author_name\":\"$author_name\",\"name\":\"$name\",\"last_change_relative\":\"$rel_date\"}"
|
|
248
252
|
fi
|
|
249
|
-
done < <(git for-each-ref --sort=committerdate --format='%(refname:short)|%(committerdate:relative)|%(authorname)|%(authoremail)|%(committerdate:unix)' refs/remotes/origin 2>/dev/null | grep -v 'origin/HEAD' | grep -v '^origin$')
|
|
253
|
+
done < <(git for-each-ref --sort=committerdate --format='%(refname:short)|%(committerdate:relative)|%(authorname)|%(authoremail)|%(committerdate:unix)' refs/remotes/origin 2>/dev/null | grep -v 'origin/HEAD' | grep -v '^origin$' || true)
|
|
250
254
|
} &>/dev/null
|
|
251
255
|
json_output+="]"
|
|
252
256
|
echo "$json_output"
|
|
@@ -314,7 +318,7 @@ HEADER_EOF
|
|
|
314
318
|
|
|
315
319
|
local selection
|
|
316
320
|
selection=$(
|
|
317
|
-
|
|
321
|
+
fzf \
|
|
318
322
|
--prompt="Stale branches (>3 months, oldest first) > " \
|
|
319
323
|
--query="$filter" \
|
|
320
324
|
-i \
|
|
@@ -340,8 +344,9 @@ Showing $stale_count stale branches (older than 3 months)" \
|
|
|
340
344
|
git log --oneline --color=always -n 15 "origin/$branch" 2>/dev/null || echo "No commits found";
|
|
341
345
|
fi
|
|
342
346
|
' \
|
|
343
|
-
--preview-window=right:35%
|
|
344
|
-
|
|
347
|
+
--preview-window=right:35% \
|
|
348
|
+
< "$stale_file"
|
|
349
|
+
) </dev/tty || true
|
|
345
350
|
|
|
346
351
|
# Cleanup temp files
|
|
347
352
|
rm -f "$stale_file" "$all_file" "$state_file" "$toggle_script" "$prompt_script" "$header_script"
|
package/commands/stashes.sh
CHANGED
|
@@ -146,15 +146,15 @@ EOF
|
|
|
146
146
|
# 3. Run fzf menu
|
|
147
147
|
# -----------------------------
|
|
148
148
|
local selection
|
|
149
|
-
selection=$(
|
|
150
|
-
| fzf --prompt="Stash action > " \
|
|
149
|
+
selection=$(fzf --prompt="Stash action > " \
|
|
151
150
|
-i \
|
|
152
151
|
--reverse \
|
|
153
152
|
--border \
|
|
154
153
|
--header="What would you like to do?" \
|
|
155
154
|
--no-multi \
|
|
156
|
-
--bind=enter:accept
|
|
157
|
-
|
|
155
|
+
--bind=enter:accept \
|
|
156
|
+
<<< "$choices"
|
|
157
|
+
) </dev/tty || true
|
|
158
158
|
|
|
159
159
|
# ESC or Ctrl-C
|
|
160
160
|
if [[ -z "$selection" ]]; then
|
package/commands/status.sh
CHANGED
|
@@ -129,7 +129,7 @@ EOF
|
|
|
129
129
|
while true; do
|
|
130
130
|
local selected_output
|
|
131
131
|
selected_output=$(
|
|
132
|
-
|
|
132
|
+
fzf \
|
|
133
133
|
--height=100% \
|
|
134
134
|
-i \
|
|
135
135
|
--reverse \
|
|
@@ -167,8 +167,9 @@ EOF
|
|
|
167
167
|
git diff --color=always "$file" 2>/dev/null | delta 2>/dev/null || git diff --color=always "$file" 2>/dev/null;
|
|
168
168
|
fi
|
|
169
169
|
' \
|
|
170
|
-
--preview-window=right:60%
|
|
171
|
-
|
|
170
|
+
--preview-window=right:60% \
|
|
171
|
+
<<< "$status_output"
|
|
172
|
+
) </dev/tty || true
|
|
172
173
|
|
|
173
174
|
# Parse the key pressed and the selected files
|
|
174
175
|
local key_pressed
|
package/commands/switch.sh
CHANGED
|
@@ -105,10 +105,11 @@ EOF
|
|
|
105
105
|
if ! echo "$local_branches" | grep -qx "$local_name"; then
|
|
106
106
|
echo "$remote"
|
|
107
107
|
fi
|
|
108
|
-
done)
|
|
108
|
+
done || true)
|
|
109
109
|
|
|
110
110
|
local selected_branch
|
|
111
|
-
|
|
111
|
+
local branch_list
|
|
112
|
+
branch_list=$(
|
|
112
113
|
{
|
|
113
114
|
# List local branches
|
|
114
115
|
echo "$local_branches" | sed 's/^/local: /'
|
|
@@ -118,7 +119,10 @@ EOF
|
|
|
118
119
|
# List filtered remote branches
|
|
119
120
|
echo "$remote_branches" | sed 's/^/remote: /'
|
|
120
121
|
fi
|
|
121
|
-
}
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
selected_branch=$(
|
|
125
|
+
fzf \
|
|
122
126
|
--height=40% \
|
|
123
127
|
--reverse \
|
|
124
128
|
--border \
|
|
@@ -127,8 +131,9 @@ EOF
|
|
|
127
131
|
-i \
|
|
128
132
|
--preview="branch=\$(echo {} | sed 's/^[^:]*: //'); if [[ \"\$branch\" == *───* ]]; then echo 'Spacer - not selectable'; else git log --color=always -n 1 --format='%C(bold cyan)Author:%C(reset) %an%n%C(bold cyan)Date:%C(reset) %ar (%ad)%n%C(bold cyan)Message:%C(reset) %s%n' --date=format:'%Y-%m-%d %H:%M' \"\$branch\" 2>/dev/null && echo && git log --oneline --color=always -n 10 \"\$branch\" 2>/dev/null; fi" \
|
|
129
133
|
--preview-window=right:50% \
|
|
130
|
-
--header="Current: $current_branch"
|
|
131
|
-
|
|
134
|
+
--header="Current: $current_branch" \
|
|
135
|
+
<<< "$branch_list"
|
|
136
|
+
) </dev/tty || true
|
|
132
137
|
|
|
133
138
|
# -----------------------------
|
|
134
139
|
# 4. Handle selection
|
package/commands/unstash.sh
CHANGED
|
@@ -111,8 +111,7 @@ EOF
|
|
|
111
111
|
# 3. Run fzf full-screen picker with preview
|
|
112
112
|
# -----------------------------
|
|
113
113
|
local selection
|
|
114
|
-
selection=$(
|
|
115
|
-
| fzf --prompt="Available stashes > " \
|
|
114
|
+
selection=$(fzf --prompt="Available stashes > " \
|
|
116
115
|
-i \
|
|
117
116
|
--reverse \
|
|
118
117
|
--border \
|
|
@@ -126,8 +125,9 @@ EOF
|
|
|
126
125
|
else
|
|
127
126
|
echo "No preview";
|
|
128
127
|
fi
|
|
129
|
-
'
|
|
130
|
-
|
|
128
|
+
' \
|
|
129
|
+
<<< "$choices"
|
|
130
|
+
) </dev/tty || true
|
|
131
131
|
|
|
132
132
|
# ESC or Ctrl-C
|
|
133
133
|
if [[ -z "$selection" ]]; then
|
package/package.json
CHANGED
package/commands/stale.zsh
DELETED
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
stale() {
|
|
2
|
-
# -----------------------------
|
|
3
|
-
# 0. Check for help flag and parse options
|
|
4
|
-
# -----------------------------
|
|
5
|
-
local json_mode=false
|
|
6
|
-
local my_mode=false
|
|
7
|
-
local filter_args=()
|
|
8
|
-
|
|
9
|
-
while [[ $# -gt 0 ]]; do
|
|
10
|
-
case "$1" in
|
|
11
|
-
-h|--help)
|
|
12
|
-
cat << 'EOF'
|
|
13
|
-
Usage: stale [OPTIONS] [FILTER...]
|
|
14
|
-
|
|
15
|
-
Show a list of remote branches ordered by last modification date (oldest first).
|
|
16
|
-
Useful for identifying stale branches that may need cleanup.
|
|
17
|
-
|
|
18
|
-
Arguments:
|
|
19
|
-
FILTER... Optional search filter words to pre-fill fzf (joined with spaces)
|
|
20
|
-
|
|
21
|
-
Options:
|
|
22
|
-
-h, --help Show this help message
|
|
23
|
-
-m, --my Pre-fill filter with your git username (from git config user.name)
|
|
24
|
-
--json Output branch data as JSON (non-interactive)
|
|
25
|
-
|
|
26
|
-
Features:
|
|
27
|
-
- Lists all remote branches sorted by last commit date (oldest first)
|
|
28
|
-
- By default, only shows branches older than 3 months (truly stale)
|
|
29
|
-
- Shows branch name, relative date, and author
|
|
30
|
-
- Interactive selection with fzf
|
|
31
|
-
- Preview shows recent commits on the selected branch
|
|
32
|
-
- Delete selected branches directly from the list
|
|
33
|
-
|
|
34
|
-
Navigation:
|
|
35
|
-
↑/↓ or j/k Navigate through branches
|
|
36
|
-
TAB Select/deselect branch (multi-select)
|
|
37
|
-
Ctrl-A Toggle showing all branches (including recent ones)
|
|
38
|
-
Enter Delete selected branch(es)
|
|
39
|
-
ESC/Ctrl-C Exit without action
|
|
40
|
-
|
|
41
|
-
Examples:
|
|
42
|
-
$ stale
|
|
43
|
-
Stale branches (oldest first) >
|
|
44
|
-
> feature/very-old 6 months ago Bob Wilson
|
|
45
|
-
feature/another-old-one 4 months ago Jane Smith
|
|
46
|
-
feature/old-feature 3 months ago John Doe
|
|
47
|
-
✖ Abort
|
|
48
|
-
|
|
49
|
-
# Press Ctrl-A to show all branches including recent ones
|
|
50
|
-
# Select branches with TAB, press Enter to delete
|
|
51
|
-
|
|
52
|
-
$ stale --my
|
|
53
|
-
# Pre-fills fzf filter with your git username to show only your branches
|
|
54
|
-
|
|
55
|
-
$ stale Product refactoring
|
|
56
|
-
# Pre-fills fzf filter with "Product refactoring"
|
|
57
|
-
|
|
58
|
-
$ stale --json
|
|
59
|
-
# Output JSON for scripting
|
|
60
|
-
|
|
61
|
-
Output format:
|
|
62
|
-
<branch-name> <relative-date> <author>
|
|
63
|
-
|
|
64
|
-
Requirements:
|
|
65
|
-
- Must be in a git repository
|
|
66
|
-
- fzf (fuzzy finder) - will prompt to install if not found (not required for --json)
|
|
67
|
-
|
|
68
|
-
EOF
|
|
69
|
-
return 0
|
|
70
|
-
;;
|
|
71
|
-
--json)
|
|
72
|
-
json_mode=true
|
|
73
|
-
shift
|
|
74
|
-
;;
|
|
75
|
-
-m|--my)
|
|
76
|
-
my_mode=true
|
|
77
|
-
shift
|
|
78
|
-
;;
|
|
79
|
-
*)
|
|
80
|
-
filter_args+=("$1")
|
|
81
|
-
shift
|
|
82
|
-
;;
|
|
83
|
-
esac
|
|
84
|
-
done
|
|
85
|
-
|
|
86
|
-
# -----------------------------
|
|
87
|
-
# 1. Check prerequisites
|
|
88
|
-
# -----------------------------
|
|
89
|
-
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
90
|
-
if [[ "$json_mode" == true ]]; then
|
|
91
|
-
echo "[]"
|
|
92
|
-
else
|
|
93
|
-
echo "Not inside a git repository."
|
|
94
|
-
fi
|
|
95
|
-
return 1
|
|
96
|
-
fi
|
|
97
|
-
|
|
98
|
-
if [[ "$json_mode" == false ]]; then
|
|
99
|
-
if ! command -v fzf >/dev/null 2>&1; then
|
|
100
|
-
echo "fzf is not installed."
|
|
101
|
-
|
|
102
|
-
read -rp "Install fzf with Homebrew? (y/N): " ans
|
|
103
|
-
case "$ans" in
|
|
104
|
-
[yY][eE][sS]|[yY])
|
|
105
|
-
echo "Installing fzf with brew..."
|
|
106
|
-
if ! command -v brew >/dev/null 2>&1; then
|
|
107
|
-
echo "Homebrew is not installed. Aborting."
|
|
108
|
-
return 1
|
|
109
|
-
fi
|
|
110
|
-
brew install fzf || { echo "fzf install failed. Aborting."; return 1; }
|
|
111
|
-
if [[ -f "$(brew --prefix)/opt/fzf/install" ]]; then
|
|
112
|
-
echo "Running fzf install script..."
|
|
113
|
-
yes | "$(brew --prefix)/opt/fzf/install"
|
|
114
|
-
fi
|
|
115
|
-
;;
|
|
116
|
-
*)
|
|
117
|
-
echo "Aborted (skipped fzf installation)."
|
|
118
|
-
return 1
|
|
119
|
-
;;
|
|
120
|
-
esac
|
|
121
|
-
fi
|
|
122
|
-
fi
|
|
123
|
-
|
|
124
|
-
# -----------------------------
|
|
125
|
-
# 2. Handle filter (--my flag or arbitrary words)
|
|
126
|
-
# -----------------------------
|
|
127
|
-
local filter=""
|
|
128
|
-
if [[ "$my_mode" == true ]]; then
|
|
129
|
-
filter=$(git config user.name)
|
|
130
|
-
if [[ -z "$filter" && "$json_mode" == false ]]; then
|
|
131
|
-
echo "Warning: git config user.name is not set."
|
|
132
|
-
fi
|
|
133
|
-
elif [[ ${#filter_args[@]} -gt 0 ]]; then
|
|
134
|
-
filter="${filter_args[*]}"
|
|
135
|
-
fi
|
|
136
|
-
|
|
137
|
-
# -----------------------------
|
|
138
|
-
# 3. Fetch latest from remote
|
|
139
|
-
# -----------------------------
|
|
140
|
-
if [[ "$json_mode" == false ]]; then
|
|
141
|
-
echo "Fetching latest from remote..."
|
|
142
|
-
fi
|
|
143
|
-
git fetch --prune origin 2>/dev/null
|
|
144
|
-
|
|
145
|
-
# -----------------------------
|
|
146
|
-
# 4. Build branch list sorted by date (oldest first)
|
|
147
|
-
# -----------------------------
|
|
148
|
-
local abort_label="✖ Abort"
|
|
149
|
-
local max_branch_length=65
|
|
150
|
-
|
|
151
|
-
# Calculate date 3 months ago
|
|
152
|
-
local three_months_ago
|
|
153
|
-
three_months_ago=$(date -v-3m +%s 2>/dev/null || date -d "3 months ago" +%s 2>/dev/null)
|
|
154
|
-
|
|
155
|
-
# Build full branch list (all branches)
|
|
156
|
-
# Format: display_branch | date | author | full_branch (tab-separated, last field is full branch for operations)
|
|
157
|
-
local all_branch_list
|
|
158
|
-
all_branch_list=$(git for-each-ref --sort=committerdate --format='%(refname:short)|%(committerdate:relative)|%(authorname)|%(committerdate:unix)' refs/remotes/origin | \
|
|
159
|
-
grep -v 'origin/HEAD' | \
|
|
160
|
-
grep -v '^origin$' | \
|
|
161
|
-
while IFS='|' read -r branch date author timestamp; do
|
|
162
|
-
# Remove origin/ prefix for display
|
|
163
|
-
local full_branch="${branch#origin/}"
|
|
164
|
-
local display_branch="$full_branch"
|
|
165
|
-
# Truncate branch name if too long
|
|
166
|
-
if [[ ${#display_branch} -gt $max_branch_length ]]; then
|
|
167
|
-
display_branch="${display_branch:0:$((max_branch_length - 3))}..."
|
|
168
|
-
fi
|
|
169
|
-
# Format: display branch (padded), date, author, TAB, full branch name
|
|
170
|
-
printf "%-${max_branch_length}s %-20s %-25s\t%s\n" "$display_branch" "$date" "$author" "$full_branch"
|
|
171
|
-
done)
|
|
172
|
-
|
|
173
|
-
# Build stale branch list (only branches older than 3 months)
|
|
174
|
-
local stale_branch_list
|
|
175
|
-
stale_branch_list=$(git for-each-ref --sort=committerdate --format='%(refname:short)|%(committerdate:relative)|%(authorname)|%(committerdate:unix)' refs/remotes/origin | \
|
|
176
|
-
grep -v 'origin/HEAD' | \
|
|
177
|
-
grep -v '^origin$' | \
|
|
178
|
-
while IFS='|' read -r branch date author timestamp; do
|
|
179
|
-
# Only include if older than 3 months
|
|
180
|
-
if [[ "$timestamp" -lt "$three_months_ago" ]]; then
|
|
181
|
-
# Remove origin/ prefix for display
|
|
182
|
-
local full_branch="${branch#origin/}"
|
|
183
|
-
local display_branch="$full_branch"
|
|
184
|
-
# Truncate branch name if too long
|
|
185
|
-
if [[ ${#display_branch} -gt $max_branch_length ]]; then
|
|
186
|
-
display_branch="${display_branch:0:$((max_branch_length - 3))}..."
|
|
187
|
-
fi
|
|
188
|
-
# Format: display branch (padded), date, author, TAB, full branch name
|
|
189
|
-
printf "%-${max_branch_length}s %-20s %-25s\t%s\n" "$display_branch" "$date" "$author" "$full_branch"
|
|
190
|
-
fi
|
|
191
|
-
done)
|
|
192
|
-
|
|
193
|
-
if [[ -z "$all_branch_list" ]]; then
|
|
194
|
-
if [[ "$json_mode" == true ]]; then
|
|
195
|
-
echo "[]"
|
|
196
|
-
else
|
|
197
|
-
echo "No remote branches found."
|
|
198
|
-
fi
|
|
199
|
-
return 0
|
|
200
|
-
fi
|
|
201
|
-
|
|
202
|
-
# -----------------------------
|
|
203
|
-
# 5. JSON mode output
|
|
204
|
-
# -----------------------------
|
|
205
|
-
if [[ "$json_mode" == true ]]; then
|
|
206
|
-
local json_output="["
|
|
207
|
-
local first=true
|
|
208
|
-
|
|
209
|
-
# Helper function to escape JSON strings
|
|
210
|
-
_json_escape() {
|
|
211
|
-
local str="$1"
|
|
212
|
-
str="${str//\\/\\\\}"
|
|
213
|
-
str="${str//\"/\\\"}"
|
|
214
|
-
str="${str//$'\n'/\\n}"
|
|
215
|
-
str="${str//$'\r'/\\r}"
|
|
216
|
-
str="${str//$'\t'/\\t}"
|
|
217
|
-
echo "$str"
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
# Build JSON from stale branches
|
|
221
|
-
{
|
|
222
|
-
while IFS='|' read -r branch rel_date author_name author_email timestamp; do
|
|
223
|
-
[[ -z "$branch" ]] && continue
|
|
224
|
-
# Only include if older than 3 months
|
|
225
|
-
if [[ "$timestamp" -lt "$three_months_ago" ]]; then
|
|
226
|
-
local name="${branch#origin/}"
|
|
227
|
-
# Remove angle brackets from email
|
|
228
|
-
author_email="${author_email#<}"
|
|
229
|
-
author_email="${author_email%>}"
|
|
230
|
-
|
|
231
|
-
# Escape values for JSON
|
|
232
|
-
name=$(_json_escape "$name")
|
|
233
|
-
rel_date=$(_json_escape "$rel_date")
|
|
234
|
-
author_email=$(_json_escape "$author_email")
|
|
235
|
-
author_name=$(_json_escape "$author_name")
|
|
236
|
-
|
|
237
|
-
if [[ "$first" == true ]]; then
|
|
238
|
-
first=false
|
|
239
|
-
else
|
|
240
|
-
json_output+=","
|
|
241
|
-
fi
|
|
242
|
-
|
|
243
|
-
json_output+="{\"last_change_timestamp\":$timestamp,\"author_email\":\"$author_email\",\"author_name\":\"$author_name\",\"name\":\"$name\",\"last_change_relative\":\"$rel_date\"}"
|
|
244
|
-
fi
|
|
245
|
-
done < <(git for-each-ref --sort=committerdate --format='%(refname:short)|%(committerdate:relative)|%(authorname)|%(authoremail)|%(committerdate:unix)' refs/remotes/origin 2>/dev/null | grep -v 'origin/HEAD' | grep -v '^origin$')
|
|
246
|
-
} &>/dev/null
|
|
247
|
-
json_output+="]"
|
|
248
|
-
echo "$json_output"
|
|
249
|
-
return 0
|
|
250
|
-
fi
|
|
251
|
-
|
|
252
|
-
# Create temp files for fzf reload
|
|
253
|
-
local stale_file=$(mktemp)
|
|
254
|
-
local all_file=$(mktemp)
|
|
255
|
-
local state_file=$(mktemp)
|
|
256
|
-
echo "$stale_branch_list" > "$stale_file"
|
|
257
|
-
echo "$abort_label" >> "$stale_file"
|
|
258
|
-
echo "$all_branch_list" > "$all_file"
|
|
259
|
-
echo "$abort_label" >> "$all_file"
|
|
260
|
-
echo "stale" > "$state_file"
|
|
261
|
-
|
|
262
|
-
# -----------------------------
|
|
263
|
-
# 6. Run fzf picker with preview
|
|
264
|
-
# -----------------------------
|
|
265
|
-
local stale_count=$(echo "$stale_branch_list" | grep -c . || echo "0")
|
|
266
|
-
local all_count=$(echo "$all_branch_list" | grep -c . || echo "0")
|
|
267
|
-
|
|
268
|
-
# Create toggle script
|
|
269
|
-
local toggle_script=$(mktemp)
|
|
270
|
-
cat > "$toggle_script" << TOGGLE_EOF
|
|
271
|
-
#!/bin/bash
|
|
272
|
-
state=\$(cat "$state_file")
|
|
273
|
-
if [[ "\$state" == "stale" ]]; then
|
|
274
|
-
echo "all" > "$state_file"
|
|
275
|
-
cat "$all_file"
|
|
276
|
-
else
|
|
277
|
-
echo "stale" > "$state_file"
|
|
278
|
-
cat "$stale_file"
|
|
279
|
-
fi
|
|
280
|
-
TOGGLE_EOF
|
|
281
|
-
chmod +x "$toggle_script"
|
|
282
|
-
|
|
283
|
-
# Create prompt script
|
|
284
|
-
local prompt_script=$(mktemp)
|
|
285
|
-
cat > "$prompt_script" << PROMPT_EOF
|
|
286
|
-
#!/bin/bash
|
|
287
|
-
state=\$(cat "$state_file")
|
|
288
|
-
if [[ "\$state" == "stale" ]]; then
|
|
289
|
-
echo "Stale branches (>3 months, oldest first) > "
|
|
290
|
-
else
|
|
291
|
-
echo "All branches (oldest first) > "
|
|
292
|
-
fi
|
|
293
|
-
PROMPT_EOF
|
|
294
|
-
chmod +x "$prompt_script"
|
|
295
|
-
|
|
296
|
-
# Create header script
|
|
297
|
-
local header_script=$(mktemp)
|
|
298
|
-
cat > "$header_script" << HEADER_EOF
|
|
299
|
-
#!/bin/bash
|
|
300
|
-
state=\$(cat "$state_file")
|
|
301
|
-
if [[ "\$state" == "stale" ]]; then
|
|
302
|
-
echo "[TAB] select | [Ctrl-A] toggle all/stale | [Enter] delete | [ESC] exit
|
|
303
|
-
Showing $stale_count stale branches (older than 3 months)"
|
|
304
|
-
else
|
|
305
|
-
echo "[TAB] select | [Ctrl-A] toggle all/stale | [Enter] delete | [ESC] exit
|
|
306
|
-
Showing all $all_count branches"
|
|
307
|
-
fi
|
|
308
|
-
HEADER_EOF
|
|
309
|
-
chmod +x "$header_script"
|
|
310
|
-
|
|
311
|
-
local selection
|
|
312
|
-
selection=$(
|
|
313
|
-
cat "$stale_file" | fzf \
|
|
314
|
-
--prompt="Stale branches (>3 months, oldest first) > " \
|
|
315
|
-
--query="$filter" \
|
|
316
|
-
-i \
|
|
317
|
-
--reverse \
|
|
318
|
-
--border \
|
|
319
|
-
--header="[TAB] select | [Ctrl-A] toggle all/stale | [Enter] delete | [ESC] exit
|
|
320
|
-
Showing $stale_count stale branches (older than 3 months)" \
|
|
321
|
-
--multi \
|
|
322
|
-
--delimiter=$'\t' \
|
|
323
|
-
--with-nth=1 \
|
|
324
|
-
--bind=enter:accept \
|
|
325
|
-
--bind="ctrl-a:reload($toggle_script)+transform-prompt($prompt_script)+transform-header($header_script)" \
|
|
326
|
-
--preview='
|
|
327
|
-
line={}
|
|
328
|
-
if [[ "$line" == "✖ Abort" ]]; then
|
|
329
|
-
echo "Exit without action";
|
|
330
|
-
else
|
|
331
|
-
# Extract full branch name (after tab)
|
|
332
|
-
branch=$(echo "$line" | cut -f2)
|
|
333
|
-
echo "Branch: origin/$branch";
|
|
334
|
-
echo "";
|
|
335
|
-
echo "Recent commits:";
|
|
336
|
-
git log --oneline --color=always -n 15 "origin/$branch" 2>/dev/null || echo "No commits found";
|
|
337
|
-
fi
|
|
338
|
-
' \
|
|
339
|
-
--preview-window=right:35%
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
# Cleanup temp files
|
|
343
|
-
rm -f "$stale_file" "$all_file" "$state_file" "$toggle_script" "$prompt_script" "$header_script"
|
|
344
|
-
|
|
345
|
-
# ESC or Ctrl-C
|
|
346
|
-
if [[ -z "$selection" ]]; then
|
|
347
|
-
echo "Exited."
|
|
348
|
-
return 0
|
|
349
|
-
fi
|
|
350
|
-
|
|
351
|
-
# Abort option
|
|
352
|
-
if echo "$selection" | grep -q "^$abort_label"; then
|
|
353
|
-
echo "Aborted."
|
|
354
|
-
return 0
|
|
355
|
-
fi
|
|
356
|
-
|
|
357
|
-
# -----------------------------
|
|
358
|
-
# 7. Extract branch names and confirm deletion
|
|
359
|
-
# -----------------------------
|
|
360
|
-
local branches_to_delete=()
|
|
361
|
-
while IFS= read -r line; do
|
|
362
|
-
# Skip abort option
|
|
363
|
-
if [[ "$line" == "✖ Abort" ]]; then
|
|
364
|
-
continue
|
|
365
|
-
fi
|
|
366
|
-
# Extract full branch name (after tab)
|
|
367
|
-
local branch_name
|
|
368
|
-
branch_name=$(echo "$line" | cut -f2)
|
|
369
|
-
if [[ -n "$branch_name" ]]; then
|
|
370
|
-
branches_to_delete+=("$branch_name")
|
|
371
|
-
fi
|
|
372
|
-
done <<< "$selection"
|
|
373
|
-
|
|
374
|
-
if [[ ${#branches_to_delete[@]} -eq 0 ]]; then
|
|
375
|
-
echo "No branches selected."
|
|
376
|
-
return 0
|
|
377
|
-
fi
|
|
378
|
-
|
|
379
|
-
echo ""
|
|
380
|
-
echo "Selected branches to delete from remote:"
|
|
381
|
-
for branch in "${branches_to_delete[@]}"; do
|
|
382
|
-
echo " - origin/$branch"
|
|
383
|
-
done
|
|
384
|
-
echo ""
|
|
385
|
-
|
|
386
|
-
read -rp "Delete these ${#branches_to_delete[@]} branch(es) from remote? (y/N): " confirm
|
|
387
|
-
case "$confirm" in
|
|
388
|
-
[yY][eE][sS]|[yY])
|
|
389
|
-
echo "Deleting branches..."
|
|
390
|
-
for branch in "${branches_to_delete[@]}"; do
|
|
391
|
-
echo "Deleting origin/$branch ..."
|
|
392
|
-
if git push origin --delete "$branch" 2>/dev/null; then
|
|
393
|
-
echo "✓ Deleted origin/$branch"
|
|
394
|
-
else
|
|
395
|
-
echo "✗ Failed to delete origin/$branch"
|
|
396
|
-
fi
|
|
397
|
-
done
|
|
398
|
-
;;
|
|
399
|
-
*)
|
|
400
|
-
echo "Deletion cancelled."
|
|
401
|
-
;;
|
|
402
|
-
esac
|
|
403
|
-
}
|