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.
@@ -147,15 +147,15 @@ EOF
147
147
  # 3. Run fzf menu
148
148
  # -----------------------------
149
149
  local selection
150
- selection=$(printf "%s\n" "$choices" \
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
- ) || true
156
+ --bind=enter:accept \
157
+ <<< "$choices"
158
+ ) </dev/tty || true
159
159
 
160
160
  # ESC or Ctrl-C
161
161
  if [[ -z "$selection" ]]; then
@@ -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=$(printf "%s\n" "$choices" \
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
- ) || true
125
+ ' \
126
+ <<< "$choices"
127
+ ) </dev/tty || true
128
128
 
129
129
  # ESC or Ctrl-C
130
130
  if [[ -z "$selection" ]]; then
@@ -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
- if [[ ${#merged_branches[@]} -gt 0 ]]; then
191
- mapfile -t merged_branches < <(printf '%s\n' "${merged_branches[@]}" | sort -t'|' -k1 -rn)
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
- mapfile -t stale_branches < <(printf '%s\n' "${stale_branches[@]}" | sort -t'|' -k1 -rn)
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
- mapfile -t recent_branches < <(printf '%s\n' "${recent_branches[@]}" | sort -t'|' -k1 -rn)
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
- for entry in "${merged_branches[@]}" "${stale_branches[@]}" "${recent_branches[@]}"; do
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
- for entry in "${merged_branches[@]}"; do
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
- done
299
+ done
300
+ fi
271
301
 
272
302
  # Add stale branches (pre-selected)
273
- for entry in "${stale_branches[@]}"; do
274
- local branch date display_branch line
275
- branch=$(echo "$entry" | cut -d'|' -f2)
276
- date=$(echo "$entry" | cut -d'|' -f3)
277
- display_branch="$branch"
278
- if [[ ${#display_branch} -gt $max_branch_len ]]; then
279
- display_branch="${display_branch:0:$((max_branch_len - 3))}..."
280
- fi
281
- line=$(printf "[STALE] %-${max_branch_len}s %s\t%s" "$display_branch" "$date" "$branch")
282
- branch_list+="$line"$'\n'
283
- preselect_list+="$line"$'\n'
284
- ((idx++))
285
- done
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
- for entry in "${recent_branches[@]}"; do
289
- local branch date display_branch line
290
- branch=$(echo "$entry" | cut -d'|' -f2)
291
- date=$(echo "$entry" | cut -d'|' -f3)
292
- display_branch="$branch"
293
- if [[ ${#display_branch} -gt $max_branch_len ]]; then
294
- display_branch="${display_branch:0:$((max_branch_len - 3))}..."
295
- fi
296
- line=$(printf "[RECENT] %-${max_branch_len}s %s\t%s" "$display_branch" "$date" "$branch")
297
- branch_list+="$line"$'\n'
298
- ((idx++))
299
- done
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 total_count=$((${#merged_branches[@]} + ${#stale_branches[@]} + ${#recent_branches[@]}))
320
- local preselect_count=$((${#merged_branches[@]} + ${#stale_branches[@]}))
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=$(cat "$branch_file" | fzf \
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
- ) || true
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"
@@ -111,7 +111,7 @@ EOF
111
111
  # 4. Run fzf picker
112
112
  # -----------------------------
113
113
  local selection
114
- selection=$(cat "$commit_file" | fzf \
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
- ) || true
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
- cat "$stale_file" | fzf \
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
- ) || true
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"
@@ -146,15 +146,15 @@ EOF
146
146
  # 3. Run fzf menu
147
147
  # -----------------------------
148
148
  local selection
149
- selection=$(printf "%s\n" "$choices" \
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
- ) || true
155
+ --bind=enter:accept \
156
+ <<< "$choices"
157
+ ) </dev/tty || true
158
158
 
159
159
  # ESC or Ctrl-C
160
160
  if [[ -z "$selection" ]]; then
@@ -129,7 +129,7 @@ EOF
129
129
  while true; do
130
130
  local selected_output
131
131
  selected_output=$(
132
- echo "$status_output" | fzf \
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
- ) || true
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
@@ -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
- selected_branch=$(
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
- } | fzf \
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
- ) || true
134
+ --header="Current: $current_branch" \
135
+ <<< "$branch_list"
136
+ ) </dev/tty || true
132
137
 
133
138
  # -----------------------------
134
139
  # 4. Handle selection
@@ -111,8 +111,7 @@ EOF
111
111
  # 3. Run fzf full-screen picker with preview
112
112
  # -----------------------------
113
113
  local selection
114
- selection=$(printf "%s\n" "$choices" \
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
- ) || true
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitbash",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Opinionated git utilities for bash with fzf-powered menus",
5
5
  "author": "jaggli",
6
6
  "license": "MIT",
@@ -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
- }