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,452 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck disable=SC2155
3
+
4
+ # Cleanup local branches that are no longer needed
5
+ cleanup() {
6
+ # -----------------------------
7
+ # 0. Check for help flag and parse options
8
+ # -----------------------------
9
+ local json_mode=false
10
+
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ -h|--help)
14
+ cat << 'EOF'
15
+ Usage: cleanup [OPTIONS]
16
+
17
+ Find and delete local branches that are no longer needed.
18
+
19
+ Options:
20
+ -h, --help Show this help message
21
+ --json Output branch data as JSON (non-interactive)
22
+
23
+ Categories (pre-selected for deletion):
24
+ - Merged branches: Local branches whose remote was merged and deleted
25
+ - Stale branches: Local branches with no commits in 7+ days
26
+
27
+ Not pre-selected (but listed):
28
+ - Recent branches: Local branches with commits in the last 7 days
29
+
30
+ Navigation:
31
+ ↑/↓ or j/k Navigate through branches
32
+ TAB Select/deselect branch
33
+ Enter Delete selected branch(es)
34
+ ESC/Ctrl-C Exit without action
35
+
36
+ Notes:
37
+ - Only deletes LOCAL branches (never touches remote)
38
+ - If current branch is selected, switches to main/master first
39
+ - Force deletes branches (even if not fully merged)
40
+
41
+ Examples:
42
+ $ cleanup
43
+ Local branches to clean up >
44
+ > [MERGED] feature/old-feature 2 weeks ago
45
+ [STALE] feature/abandoned 8 days ago
46
+ [RECENT] feature/work-in-progress 2 days ago
47
+ ✖ Abort
48
+
49
+ $ cleanup --json
50
+ [{"last_change_timestamp":1733123456,"author_email":"dev@example.com",...}]
51
+
52
+ Requirements:
53
+ - Must be in a git repository
54
+ - fzf (fuzzy finder) - not required for --json mode
55
+
56
+ EOF
57
+ return 0
58
+ ;;
59
+ --json)
60
+ json_mode=true
61
+ shift
62
+ ;;
63
+ *)
64
+ echo "Unknown option: $1"
65
+ return 1
66
+ ;;
67
+ esac
68
+ done
69
+
70
+ # -----------------------------
71
+ # 1. Check prerequisites
72
+ # -----------------------------
73
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
74
+ if [[ "$json_mode" == true ]]; then
75
+ echo "[]"
76
+ else
77
+ echo "Not inside a git repository." >&2
78
+ fi
79
+ return 1
80
+ fi
81
+
82
+ if [[ "$json_mode" == false ]]; then
83
+ if ! command -v fzf >/dev/null 2>&1; then
84
+ echo "fzf is not installed."
85
+ read -rp "Install fzf with Homebrew? (y/N): " ans
86
+ case "$ans" in
87
+ [yY][eE][sS]|[yY])
88
+ echo "Installing fzf with brew..."
89
+ if ! command -v brew >/dev/null 2>&1; then
90
+ echo "Homebrew is not installed. Aborting."
91
+ return 1
92
+ fi
93
+ brew install fzf || { echo "fzf install failed. Aborting."; return 1; }
94
+ ;;
95
+ *)
96
+ echo "Aborted (skipped fzf installation)."
97
+ return 1
98
+ ;;
99
+ esac
100
+ fi
101
+ fi
102
+
103
+ # -----------------------------
104
+ # 2. Fetch and prune to sync with remote
105
+ # -----------------------------
106
+ if [[ "$json_mode" == false ]]; then
107
+ echo "Fetching and pruning remote..."
108
+ fi
109
+ git fetch --prune origin 2>/dev/null
110
+
111
+ # -----------------------------
112
+ # 3. Detect base branch
113
+ # -----------------------------
114
+ local base_branch
115
+ if git show-ref --verify --quiet refs/heads/main; then
116
+ base_branch="main"
117
+ elif git show-ref --verify --quiet refs/heads/master; then
118
+ base_branch="master"
119
+ else
120
+ if [[ "$json_mode" == false ]]; then
121
+ echo "Could not detect 'main' or 'master' locally."
122
+ else
123
+ echo "[]"
124
+ fi
125
+ return 1
126
+ fi
127
+
128
+ local current_branch
129
+ current_branch=$(git rev-parse --abbrev-ref HEAD)
130
+
131
+ # -----------------------------
132
+ # 4. Build branch lists
133
+ # -----------------------------
134
+ local abort_label="✖ Abort"
135
+ local one_week_ago
136
+ one_week_ago=$(date -v-7d +%s 2>/dev/null || date -d "7 days ago" +%s 2>/dev/null)
137
+
138
+ # Get list of remote branches (without origin/ prefix)
139
+ local remote_branches
140
+ remote_branches=$(git for-each-ref --format='%(refname:short)' refs/remotes/origin | sed 's|^origin/||' | grep -v '^HEAD$')
141
+
142
+ # Arrays to hold branches by category (format: timestamp|branch|relative_date|author_email|author_name)
143
+ local merged_branches=()
144
+ local stale_branches=()
145
+ local recent_branches=()
146
+
147
+ # Process each local branch
148
+ while IFS= read -r branch; do
149
+ # Skip base branch
150
+ [[ "$branch" == "$base_branch" ]] && continue
151
+ [[ -z "$branch" ]] && continue
152
+
153
+ # Get last commit info for all branches
154
+ local last_commit_ts relative_date author_email author_name
155
+ last_commit_ts=$(git log -1 --format='%ct' "$branch" 2>/dev/null)
156
+ relative_date=$(git log -1 --format='%cr' "$branch" 2>/dev/null)
157
+ author_email=$(git log -1 --format='%ae' "$branch" 2>/dev/null)
158
+ author_name=$(git log -1 --format='%an' "$branch" 2>/dev/null)
159
+
160
+ # Check if remote branch exists
161
+ local has_remote=false
162
+ if echo "$remote_branches" | grep -qx "$branch"; then
163
+ has_remote=true
164
+ fi
165
+
166
+ # Check if it HAD an upstream (was pushed before)
167
+ local configured_upstream
168
+ configured_upstream=$(git config "branch.$branch.remote" 2>/dev/null)
169
+
170
+ # Entry format: timestamp|branch|relative_date|author_email|author_name
171
+ local entry="$last_commit_ts|$branch|$relative_date|$author_email|$author_name"
172
+
173
+ if [[ "$has_remote" == false && -n "$configured_upstream" ]]; then
174
+ # Had upstream but remote is gone = merged and deleted remotely
175
+ merged_branches+=("$entry")
176
+ elif [[ -n "$last_commit_ts" && "$last_commit_ts" -lt "$one_week_ago" ]]; then
177
+ # Stale: no commits in 7+ days (pre-select for deletion)
178
+ stale_branches+=("$entry")
179
+ else
180
+ # Recent: has recent activity (don't pre-select)
181
+ recent_branches+=("$entry")
182
+ fi
183
+ done < <(git for-each-ref --format='%(refname:short)' refs/heads)
184
+
185
+ # Sort each category by timestamp (most recent first)
186
+ if [[ ${#merged_branches[@]} -gt 0 ]]; then
187
+ mapfile -t merged_branches < <(printf '%s\n' "${merged_branches[@]}" | sort -t'|' -k1 -rn)
188
+ fi
189
+ if [[ ${#stale_branches[@]} -gt 0 ]]; then
190
+ mapfile -t stale_branches < <(printf '%s\n' "${stale_branches[@]}" | sort -t'|' -k1 -rn)
191
+ fi
192
+ if [[ ${#recent_branches[@]} -gt 0 ]]; then
193
+ mapfile -t recent_branches < <(printf '%s\n' "${recent_branches[@]}" | sort -t'|' -k1 -rn)
194
+ fi
195
+
196
+ # -----------------------------
197
+ # 5. JSON mode output
198
+ # -----------------------------
199
+ if [[ "$json_mode" == true ]]; then
200
+ local json_output="["
201
+ local first=true
202
+
203
+ # Helper function to escape JSON strings
204
+ _json_escape() {
205
+ local str="$1"
206
+ str="${str//\\/\\\\}"
207
+ str="${str//\"/\\\"}"
208
+ str="${str//$'\n'/\\n}"
209
+ str="${str//$'\r'/\\r}"
210
+ str="${str//$'\t'/\\t}"
211
+ echo "$str"
212
+ }
213
+
214
+ # Process all branches for JSON output
215
+ for entry in "${merged_branches[@]}" "${stale_branches[@]}" "${recent_branches[@]}"; do
216
+ [[ -z "$entry" ]] && continue
217
+
218
+ local ts name rel_date email author
219
+ ts=$(echo "$entry" | cut -d'|' -f1)
220
+ name=$(echo "$entry" | cut -d'|' -f2)
221
+ rel_date=$(echo "$entry" | cut -d'|' -f3)
222
+ email=$(echo "$entry" | cut -d'|' -f4)
223
+ author=$(echo "$entry" | cut -d'|' -f5)
224
+
225
+ # Escape values for JSON
226
+ name=$(_json_escape "$name")
227
+ rel_date=$(_json_escape "$rel_date")
228
+ email=$(_json_escape "$email")
229
+ author=$(_json_escape "$author")
230
+
231
+ if [[ "$first" == true ]]; then
232
+ first=false
233
+ else
234
+ json_output+=","
235
+ fi
236
+
237
+ json_output+="{\"last_change_timestamp\":$ts,\"author_email\":\"$email\",\"author_name\":\"$author\",\"name\":\"$name\",\"last_change_relative\":\"$rel_date\"}"
238
+ done
239
+
240
+ json_output+="]"
241
+ echo "$json_output"
242
+ return 0
243
+ fi
244
+
245
+ # -----------------------------
246
+ # 6. Build fzf input with pre-selection markers
247
+ # -----------------------------
248
+ local branch_list=""
249
+ local preselect_list=""
250
+ local idx=0
251
+ local max_branch_len=60
252
+
253
+ # Add merged branches (pre-selected)
254
+ for entry in "${merged_branches[@]}"; do
255
+ local branch date display_branch line
256
+ branch=$(echo "$entry" | cut -d'|' -f2)
257
+ date=$(echo "$entry" | cut -d'|' -f3)
258
+ display_branch="$branch"
259
+ if [[ ${#display_branch} -gt $max_branch_len ]]; then
260
+ display_branch="${display_branch:0:$((max_branch_len - 3))}..."
261
+ fi
262
+ line=$(printf "[MERGED] %-${max_branch_len}s %s\t%s" "$display_branch" "$date" "$branch")
263
+ branch_list+="$line"$'\n'
264
+ preselect_list+="$line"$'\n'
265
+ ((idx++))
266
+ done
267
+
268
+ # Add stale branches (pre-selected)
269
+ for entry in "${stale_branches[@]}"; do
270
+ local branch date display_branch line
271
+ branch=$(echo "$entry" | cut -d'|' -f2)
272
+ date=$(echo "$entry" | cut -d'|' -f3)
273
+ display_branch="$branch"
274
+ if [[ ${#display_branch} -gt $max_branch_len ]]; then
275
+ display_branch="${display_branch:0:$((max_branch_len - 3))}..."
276
+ fi
277
+ line=$(printf "[STALE] %-${max_branch_len}s %s\t%s" "$display_branch" "$date" "$branch")
278
+ branch_list+="$line"$'\n'
279
+ preselect_list+="$line"$'\n'
280
+ ((idx++))
281
+ done
282
+
283
+ # Add recent branches (not pre-selected)
284
+ for entry in "${recent_branches[@]}"; do
285
+ local branch date display_branch line
286
+ branch=$(echo "$entry" | cut -d'|' -f2)
287
+ date=$(echo "$entry" | cut -d'|' -f3)
288
+ display_branch="$branch"
289
+ if [[ ${#display_branch} -gt $max_branch_len ]]; then
290
+ display_branch="${display_branch:0:$((max_branch_len - 3))}..."
291
+ fi
292
+ line=$(printf "[RECENT] %-${max_branch_len}s %s\t%s" "$display_branch" "$date" "$branch")
293
+ branch_list+="$line"$'\n'
294
+ ((idx++))
295
+ done
296
+
297
+ if [[ -z "$branch_list" ]]; then
298
+ echo "No local branches to clean up."
299
+ return 0
300
+ fi
301
+
302
+ # Add abort option
303
+ branch_list+="$abort_label"$'\t\t'
304
+
305
+ # Create temp files
306
+ local branch_file preselect_file
307
+ branch_file=$(mktemp)
308
+ preselect_file=$(mktemp)
309
+ echo -e "$branch_list" > "$branch_file"
310
+ echo -e "$preselect_list" > "$preselect_file"
311
+
312
+ # -----------------------------
313
+ # 7. Run fzf picker
314
+ # -----------------------------
315
+ local total_count=$((${#merged_branches[@]} + ${#stale_branches[@]} + ${#recent_branches[@]}))
316
+ local preselect_count=$((${#merged_branches[@]} + ${#stale_branches[@]}))
317
+
318
+ # Build toggle sequence for pre-selection (toggle first N items)
319
+ local toggle_sequence="first"
320
+ for ((i=0; i<preselect_count; i++)); do
321
+ toggle_sequence+="+toggle+down"
322
+ done
323
+ toggle_sequence+="+first"
324
+
325
+ local selection
326
+ selection=$(cat "$branch_file" | fzf \
327
+ --prompt="Local branches to clean up > " \
328
+ -i \
329
+ --reverse \
330
+ --border \
331
+ --header="[TAB] toggle | [Enter] delete selected | [ESC] exit
332
+ Found $total_count branches ($preselect_count pre-selected for deletion)" \
333
+ --multi \
334
+ --delimiter=$'\t' \
335
+ --with-nth=1 \
336
+ --bind=enter:accept \
337
+ --preview='
338
+ line={}
339
+ if [[ "$line" == "✖ Abort"* ]]; then
340
+ echo "Exit without action";
341
+ else
342
+ branch=$(echo "$line" | cut -f2)
343
+ echo "Branch: $branch";
344
+ echo "";
345
+ echo "Recent commits:";
346
+ git log --oneline --color=always -n 15 "$branch" 2>/dev/null || echo "No commits found";
347
+ fi
348
+ ' \
349
+ --preview-window=right:35% \
350
+ --bind "load:$toggle_sequence"
351
+ )
352
+
353
+ # Cleanup temp files
354
+ rm -f "$branch_file" "$preselect_file"
355
+
356
+ # ESC or Ctrl-C
357
+ if [[ -z "$selection" ]]; then
358
+ echo "Exited."
359
+ return 0
360
+ fi
361
+
362
+ # Abort option
363
+ if echo "$selection" | grep -q "^$abort_label"; then
364
+ echo "Aborted."
365
+ return 0
366
+ fi
367
+
368
+ # -----------------------------
369
+ # 8. Extract branch names and confirm deletion
370
+ # -----------------------------
371
+ local branches_to_delete=()
372
+ local need_switch=false
373
+ local only_merged=true
374
+
375
+ while IFS= read -r line; do
376
+ [[ "$line" == "✖ Abort"* ]] && continue
377
+ local branch_name
378
+ branch_name=$(echo "$line" | cut -f2)
379
+ if [[ -n "$branch_name" ]]; then
380
+ branches_to_delete+=("$branch_name")
381
+ if [[ "$branch_name" == "$current_branch" ]]; then
382
+ need_switch=true
383
+ fi
384
+ # Check if this is not a MERGED branch
385
+ if [[ ! "$line" == "[MERGED]"* ]]; then
386
+ only_merged=false
387
+ fi
388
+ fi
389
+ done <<< "$selection"
390
+
391
+ if [[ ${#branches_to_delete[@]} -eq 0 ]]; then
392
+ echo "No branches selected."
393
+ return 0
394
+ fi
395
+
396
+ echo ""
397
+ echo "Selected local branches to delete:"
398
+ for branch in "${branches_to_delete[@]}"; do
399
+ if [[ "$branch" == "$current_branch" ]]; then
400
+ echo " - $branch (current branch)"
401
+ else
402
+ echo " - $branch"
403
+ fi
404
+ done
405
+
406
+ if [[ "$need_switch" == true ]]; then
407
+ echo ""
408
+ echo "Note: Will switch to '$base_branch' first (current branch selected for deletion)"
409
+ fi
410
+ echo ""
411
+
412
+ # Default to Y if only merged branches, otherwise N
413
+ local confirm
414
+ if [[ "$only_merged" == true ]]; then
415
+ read -rp "Delete these ${#branches_to_delete[@]} local branch(es)? (Y/n): " confirm
416
+ case "$confirm" in
417
+ [nN][oO]|[nN])
418
+ echo "Deletion cancelled."
419
+ return 0
420
+ ;;
421
+ esac
422
+ else
423
+ read -rp "Delete these ${#branches_to_delete[@]} local branch(es)? (y/N): " confirm
424
+ case "$confirm" in
425
+ [yY][eE][sS]|[yY])
426
+ ;;
427
+ *)
428
+ echo "Deletion cancelled."
429
+ return 0
430
+ ;;
431
+ esac
432
+ fi
433
+
434
+ # Switch to base branch if needed
435
+ if [[ "$need_switch" == true ]]; then
436
+ echo "Switching to '$base_branch'..."
437
+ git checkout "$base_branch" || {
438
+ echo "⚠ Failed to switch to '$base_branch'. Aborting deletion."
439
+ return 1
440
+ }
441
+ fi
442
+
443
+ echo "Deleting branches..."
444
+ for branch in "${branches_to_delete[@]}"; do
445
+ echo "Deleting $branch ..."
446
+ if git branch -D "$branch" 2>/dev/null; then
447
+ echo "✓ Deleted $branch"
448
+ else
449
+ echo "✗ Failed to delete $branch"
450
+ fi
451
+ done
452
+ }
@@ -0,0 +1,169 @@
1
+ # Stage all changes and commit with a message.
2
+ # If no commit message argument is given, prompt the user for one.
3
+ commit() {
4
+ local should_push=false
5
+ local staged_only=false
6
+ local msg_parts=()
7
+
8
+ # Parse arguments
9
+ while [[ $# -gt 0 ]]; do
10
+ case $1 in
11
+ -h|--help)
12
+ cat << 'EOF'
13
+ Usage: commit [MESSAGE] [OPTIONS]
14
+
15
+ Stage all changes (git add -A) and commit with a message.
16
+
17
+ Options:
18
+ -p, --push Push to origin after successful commit
19
+ -s, --staged Commit only staged changes (skip prompt, ignore unstaged)
20
+ -h, --help Show this help message
21
+
22
+ Interactive mode (no message):
23
+ $ commit
24
+ Commit message: fix login bug
25
+ [main 1a2b3c4] fix login bug
26
+ 1 file changed, 5 insertions(+), 2 deletions(-)
27
+
28
+ With message argument:
29
+ $ commit implement new feature
30
+ [main 5d6e7f8] implement new feature
31
+ 3 files changed, 42 insertions(+), 8 deletions(-)
32
+
33
+ With push option:
34
+ $ commit update documentation -p
35
+ [main 9a8b7c6] update documentation
36
+ 1 file changed, 10 insertions(+), 3 deletions(-)
37
+ Pushing 'main' to origin...
38
+ ✓ Successfully pushed 'main' to origin.
39
+
40
+ $ commit -p hotfix: critical bug
41
+ [main 3c2d1e0] hotfix: critical bug
42
+ 2 files changed, 15 insertions(+), 5 deletions(-)
43
+ Pushing 'main' to origin...
44
+ ✓ Successfully pushed 'main' to origin.
45
+
46
+ Examples:
47
+ commit # Interactive mode
48
+ commit add user validation # Quick commit
49
+ commit refactor auth module --push # Commit and push
50
+ commit -p update dependencies # Commit and push (short flag)
51
+ commit -s -p fix bug # Commit only staged and push
52
+
53
+ EOF
54
+ return 0
55
+ ;;
56
+ -p|--push)
57
+ should_push=true
58
+ shift
59
+ ;;
60
+ -s|--staged)
61
+ staged_only=true
62
+ shift
63
+ ;;
64
+ -*)
65
+ echo "Unknown option: $1"
66
+ echo "Usage: commit [message] [-p|--push] [-s|--staged]"
67
+ return 1
68
+ ;;
69
+ *)
70
+ # Collect all non-flag arguments as message parts
71
+ msg_parts+=("$1")
72
+ shift
73
+ ;;
74
+ esac
75
+ done
76
+
77
+ # Join message parts with spaces
78
+ local msg="${msg_parts[*]}"
79
+
80
+ # If no commit message was provided...
81
+ if [ -z "$msg" ]; then
82
+ # Prompt the user for a commit message (no newline, no extra spaces)
83
+ read -rp "Commit message: " msg
84
+ fi
85
+
86
+ # Check if there are staged changes
87
+ local has_staged
88
+ has_staged=$(git diff --cached --name-only)
89
+
90
+ local has_unstaged
91
+ has_unstaged=$(git diff --name-only)
92
+
93
+ # If --staged flag is set, only commit staged changes
94
+ if [[ "$staged_only" == true ]]; then
95
+ if [[ -z "$has_staged" ]]; then
96
+ echo "No staged changes to commit."
97
+ return 1
98
+ fi
99
+ echo "Committing staged changes only..."
100
+ git commit -m "$msg"
101
+ # If there are both staged and unstaged changes, ask what to commit
102
+ elif [[ -n "$has_staged" && -n "$has_unstaged" ]]; then
103
+ echo
104
+ echo "You have both staged and unstaged changes."
105
+ echo
106
+ read -rp "Commit [s]taged only, or [a]ll changes? (s/a): " commit_choice
107
+
108
+ case "$commit_choice" in
109
+ [sS])
110
+ # Commit only staged changes
111
+ echo "Committing staged changes only..."
112
+ git commit -m "$msg"
113
+ ;;
114
+ [aA]|*)
115
+ # Stage all and commit
116
+ echo "Staging all changes and committing..."
117
+ git add -A && git commit -m "$msg"
118
+ ;;
119
+ esac
120
+ elif [[ -n "$has_staged" ]]; then
121
+ # Only staged changes exist
122
+ echo "Committing staged changes..."
123
+ git commit -m "$msg"
124
+ else
125
+ # No staged changes, stage all and commit
126
+ git add -A && git commit -m "$msg"
127
+ fi
128
+
129
+ # Push if requested and commit was successful
130
+ if [[ $? -eq 0 && "$should_push" == true ]]; then
131
+ current_branch=$(git rev-parse --abbrev-ref HEAD)
132
+
133
+ # Check if remote has updates and pull first
134
+ git fetch origin "$current_branch" 2>/dev/null
135
+ local local_commit=$(git rev-parse HEAD)
136
+ local remote_commit=$(git rev-parse "origin/$current_branch" 2>/dev/null)
137
+
138
+ if [[ -n "$remote_commit" && "$local_commit" != "$remote_commit" ]]; then
139
+ # Check if we're behind the remote
140
+ if git merge-base --is-ancestor "$local_commit" "$remote_commit" 2>/dev/null; then
141
+ echo "Remote has updates. Pulling first..."
142
+ git pull --rebase origin "$current_branch"
143
+ if [[ $? -ne 0 ]]; then
144
+ echo "⚠ Failed to pull remote changes. Please resolve conflicts and try again."
145
+ return 1
146
+ fi
147
+ echo "✓ Pulled latest changes."
148
+ elif ! git merge-base --is-ancestor "$remote_commit" "$local_commit" 2>/dev/null; then
149
+ # Branches have diverged
150
+ echo "Remote has diverged. Pulling with rebase..."
151
+ git pull --rebase origin "$current_branch"
152
+ if [[ $? -ne 0 ]]; then
153
+ echo "⚠ Failed to pull remote changes. Please resolve conflicts and try again."
154
+ return 1
155
+ fi
156
+ echo "✓ Rebased on latest changes."
157
+ fi
158
+ fi
159
+
160
+ echo "Pushing '$current_branch' to origin..."
161
+ git push origin "$current_branch"
162
+ if [[ $? -eq 0 ]]; then
163
+ echo "✓ Successfully pushed '$current_branch' to origin."
164
+ else
165
+ echo "⚠ Failed to push '$current_branch' to origin."
166
+ return 1
167
+ fi
168
+ fi
169
+ }