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.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/bin/gitbash +124 -0
- package/commands/branch.sh +184 -0
- package/commands/cleanstash.sh +187 -0
- package/commands/cleanup.sh +452 -0
- package/commands/commit.sh +169 -0
- package/commands/commits.sh +211 -0
- package/commands/create.sh +205 -0
- package/commands/pr.sh +130 -0
- package/commands/stale.sh +403 -0
- package/commands/stale.zsh +403 -0
- package/commands/stash.sh +93 -0
- package/commands/stashes.sh +183 -0
- package/commands/status.sh +323 -0
- package/commands/switch.sh +174 -0
- package/commands/unstash.sh +174 -0
- package/commands/update.sh +191 -0
- package/package.json +42 -0
|
@@ -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
|
+
}
|