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,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
|
+
}
|