gitbash 0.1.0

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.
@@ -0,0 +1,403 @@
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
+ }