gitbash 0.2.3 → 0.2.5

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/README.md CHANGED
@@ -16,6 +16,7 @@ npm i -g gitbash
16
16
  gitbash --help # Show help
17
17
  gitbash --version # Show version
18
18
  gitbash --init # Print shell init code
19
+ gitbash --config # Interactive configuration wizard
19
20
  ```
20
21
 
21
22
  ### Shell Aliases
@@ -56,6 +57,20 @@ brew install git-delta # Better diff highlighting
56
57
  brew install bat # File preview with syntax highlighting
57
58
  ```
58
59
 
60
+ ### Configuration
61
+
62
+ gitbash reads configuration from `~/.gitbashrc` if it exists. You can set the following variables:
63
+
64
+ ```bash
65
+ # Prefix for feature branches (default: "feature/")
66
+ GITBASH_FEATURE_BRANCH_PREFIX="feature/"
67
+
68
+ # Command for merging (default: "merge")
69
+ GITBASH_MERGE_COMMAND="merge"
70
+ ```
71
+
72
+ You can create this file manually or use `gitbash --config` to configure interactively.
73
+
59
74
  ## Commands
60
75
 
61
76
  ### branch
package/bin/gitbash CHANGED
@@ -3,6 +3,42 @@
3
3
 
4
4
  set -euo pipefail
5
5
 
6
+ CONFIG_FILE="$HOME/.gitbashrc"
7
+
8
+ load_config() {
9
+ if [[ -f "$CONFIG_FILE" ]]; then
10
+ # shellcheck disable=SC1090
11
+ source "$CONFIG_FILE"
12
+ fi
13
+ }
14
+
15
+ configure_gitbash() {
16
+ load_config
17
+
18
+ local current_prefix="${GITBASH_FEATURE_BRANCH_PREFIX:-feature/}"
19
+ local current_merge="${GITBASH_MERGE_COMMAND:-fork}"
20
+
21
+ echo "Configure gitbash (writes to $CONFIG_FILE)"
22
+
23
+ local input prefix merge_cmd
24
+
25
+ read -e -p "Feature branch prefix (used by create) [${current_prefix}]: " input
26
+ prefix=${input:-$current_prefix}
27
+
28
+ read -e -p "Merge tool command (used by update on conflicts) [${current_merge}]: " input
29
+ merge_cmd=${input:-$current_merge}
30
+
31
+ cat > "$CONFIG_FILE" <<EOF
32
+ # gitbash configuration
33
+ # Prefix for feature branches created by 'create'
34
+ GITBASH_FEATURE_BRANCH_PREFIX="$prefix"
35
+ # Merge tool command invoked by 'update' when conflicts occur
36
+ GITBASH_MERGE_COMMAND="$merge_cmd"
37
+ EOF
38
+
39
+ echo "Saved configuration to $CONFIG_FILE"
40
+ }
41
+
6
42
  # Resolve symlinks to get the actual script location
7
43
  SOURCE="${BASH_SOURCE[0]}"
8
44
  while [[ -L "$SOURCE" ]]; do
@@ -38,9 +74,10 @@ show_help() {
38
74
  gitbash - Interactive git utilities for bash
39
75
 
40
76
  Usage:
41
- gitbash <command> [args...]
42
- gitbash --help
43
- gitbash --init
77
+ gitbash <command> [args...]
78
+ gitbash --help
79
+ gitbash --init
80
+ gitbash --config
44
81
 
45
82
  Commands:
46
83
  branch Interactive menu for branch operations
@@ -62,6 +99,7 @@ Options:
62
99
  -h, --help Show this help message
63
100
  -v, --version Show version number
64
101
  --init Print shell initialization code (for sourcing)
102
+ --config Configure gitbash and save to ~/.gitbashrc
65
103
 
66
104
  Examples:
67
105
  gitbash commit fix bug -p # Commit and push
@@ -102,6 +140,7 @@ EOF
102
140
 
103
141
  # Main entry point
104
142
  main() {
143
+ load_config
105
144
  case "${1:-}" in
106
145
  -h|--help|"")
107
146
  show_help
@@ -115,6 +154,10 @@ main() {
115
154
  show_init
116
155
  exit 0
117
156
  ;;
157
+ --config)
158
+ configure_gitbash
159
+ exit 0
160
+ ;;
118
161
  *)
119
162
  local cmd="$1"
120
163
  shift
@@ -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
@@ -144,9 +147,12 @@ EOF
144
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"
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitbash",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Opinionated git utilities for bash with fzf-powered menus",
5
5
  "author": "jaggli",
6
6
  "license": "MIT",
@@ -1,404 +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 || true)
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 || true)
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$' || true)
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
- 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
- < "$stale_file"
341
- ) </dev/tty || true
342
-
343
- # Cleanup temp files
344
- rm -f "$stale_file" "$all_file" "$state_file" "$toggle_script" "$prompt_script" "$header_script"
345
-
346
- # ESC or Ctrl-C
347
- if [[ -z "$selection" ]]; then
348
- echo "Exited."
349
- return 0
350
- fi
351
-
352
- # Abort option
353
- if echo "$selection" | grep -q "^$abort_label"; then
354
- echo "Aborted."
355
- return 0
356
- fi
357
-
358
- # -----------------------------
359
- # 7. Extract branch names and confirm deletion
360
- # -----------------------------
361
- local branches_to_delete=()
362
- while IFS= read -r line; do
363
- # Skip abort option
364
- if [[ "$line" == "✖ Abort" ]]; then
365
- continue
366
- fi
367
- # Extract full branch name (after tab)
368
- local branch_name
369
- branch_name=$(echo "$line" | cut -f2)
370
- if [[ -n "$branch_name" ]]; then
371
- branches_to_delete+=("$branch_name")
372
- fi
373
- done <<< "$selection"
374
-
375
- if [[ ${#branches_to_delete[@]} -eq 0 ]]; then
376
- echo "No branches selected."
377
- return 0
378
- fi
379
-
380
- echo ""
381
- echo "Selected branches to delete from remote:"
382
- for branch in "${branches_to_delete[@]}"; do
383
- echo " - origin/$branch"
384
- done
385
- echo ""
386
-
387
- read -rp "Delete these ${#branches_to_delete[@]} branch(es) from remote? (y/N): " confirm
388
- case "$confirm" in
389
- [yY][eE][sS]|[yY])
390
- echo "Deleting branches..."
391
- for branch in "${branches_to_delete[@]}"; do
392
- echo "Deleting origin/$branch ..."
393
- if git push origin --delete "$branch" 2>/dev/null; then
394
- echo "✓ Deleted origin/$branch"
395
- else
396
- echo "✗ Failed to delete origin/$branch"
397
- fi
398
- done
399
- ;;
400
- *)
401
- echo "Deletion cancelled."
402
- ;;
403
- esac
404
- }