gitbash 1.9.0 → 2.0.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/commands/stale.sh CHANGED
@@ -1,21 +1,26 @@
1
1
  #!/usr/bin/env bash
2
- # shellcheck disable=SC2155
3
2
 
4
3
  # Source common utilities
5
- SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
4
+ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
5
  # shellcheck source=_utils.sh
7
6
  source "$SOURCE_DIR/_utils.sh"
8
7
 
8
+ # Unix timestamp of N months ago
9
+ _stale_months_ago() {
10
+ local months="$1" ts
11
+ ts=$(date -v-"${months}m" +%s 2>/dev/null) ||
12
+ ts=$(date -d "${months} months ago" +%s 2>/dev/null) ||
13
+ ts=$(( $(date +%s) - months * 30 * 86400 ))
14
+ echo "$ts"
15
+ }
16
+
9
17
  stale() {
10
- # -----------------------------
11
- # 0. Check for help/version flag and parse options
12
- # -----------------------------
13
18
  local json_mode=false
14
19
  local my_mode=false
15
20
  local all_mode=false
16
21
  local filter_args=()
17
22
  local stale_months="${GITBASH_STALE_MONTHS:-3}"
18
-
23
+
19
24
  while [[ $# -gt 0 ]]; do
20
25
  case "$1" in
21
26
  -v|--version)
@@ -26,8 +31,8 @@ stale() {
26
31
  cat << 'EOF'
27
32
  Usage: stale [OPTIONS] [FILTER...]
28
33
 
29
- Show a list of remote branches ordered by last modification date (oldest first).
30
- Useful for identifying stale branches that may need cleanup.
34
+ Show remote branches ordered by last commit date (oldest first) and delete
35
+ the ones that are no longer needed.
31
36
 
32
37
  Arguments:
33
38
  FILTER... Optional search filter words to pre-fill fzf (joined with spaces)
@@ -39,57 +44,30 @@ Options:
39
44
  --json Output branch data as JSON (non-interactive)
40
45
  --age=N Override stale threshold in months (default: 3, configurable via GITBASH_STALE_MONTHS)
41
46
 
42
- Features:
43
- - Lists all remote branches sorted by last commit date (oldest first)
44
- - By default, only shows branches older than N months (default 3)
45
- - Shows branch name, relative date, and author
46
- - Interactive selection with fzf
47
- - Preview shows recent commits on the selected branch
48
- - Delete selected branches directly from the list
49
-
50
- Configuration:
51
- GITBASH_STALE_MONTHS - Set default threshold in months (run 'gitbash --config')
47
+ Protected branches (the base branch and GITBASH_PROTECTED_BRANCHES, default
48
+ "main master develop release/*") are never listed and cannot be deleted.
52
49
 
53
50
  Navigation:
54
- ↑/↓ or j/k Navigate through branches
51
+ ↑/↓ Navigate through branches
55
52
  TAB Select/deselect branch (multi-select)
56
- Ctrl-A Toggle showing all branches (including recent ones)
57
- Enter Delete selected branch(es)
53
+ Ctrl-T Toggle between stale and all branches
54
+ Enter Delete selected branch(es) from the remote (asks first)
58
55
  ESC/Ctrl-C Exit without action
59
56
 
60
57
  Examples:
61
58
  $ stale
62
- Stale branches (oldest first) >
63
- > feature/very-old 6 months ago Bob Wilson
64
- feature/another-old-one 4 months ago Jane Smith
65
- feature/old-feature 3 months ago John Doe
66
- ✖ Abort
67
-
68
- $ stale --age=1
69
- # Show branches older than 1 month
70
-
71
59
  $ stale --age=6
72
- # Show branches older than 6 months
73
-
74
60
  $ stale --all
75
- # Start with all branches visible (including recent ones)
76
- # Press Ctrl-A to toggle between all/stale view
77
-
78
61
  $ stale --my
79
- # Pre-fills fzf filter with your git username to show only your branches
80
-
81
62
  $ stale Product refactoring
82
- # Pre-fills fzf filter with "Product refactoring"
83
-
84
63
  $ stale --json
85
- # Output JSON for scripting
86
64
 
87
65
  Output format:
88
- <branch-name> <relative-date> <author>
66
+ <branch-name> <relative-date> <last committer>
89
67
 
90
68
  Requirements:
91
69
  - Must be in a git repository
92
- - fzf (fuzzy finder) - will prompt to install if not found (not required for --json)
70
+ - fzf (not required for --json)
93
71
 
94
72
  EOF
95
73
  return 0
@@ -108,12 +86,16 @@ EOF
108
86
  ;;
109
87
  --age=*)
110
88
  stale_months="${1#--age=}"
111
- if ! [[ "$stale_months" =~ ^[0-9]+$ ]] || [[ "$stale_months" -lt 1 ]]; then
89
+ if ! [[ "$stale_months" =~ ^[1-9][0-9]*$ ]]; then
112
90
  print_error "Invalid age value: $stale_months"
113
91
  return 1
114
92
  fi
115
93
  shift
116
94
  ;;
95
+ -*)
96
+ print_error "Unknown option: $1"
97
+ return 1
98
+ ;;
117
99
  *)
118
100
  filter_args+=("$1")
119
101
  shift
@@ -122,221 +104,116 @@ EOF
122
104
  done
123
105
 
124
106
  # -----------------------------
125
- # 1. Check prerequisites
107
+ # 1. Prerequisites
126
108
  # -----------------------------
127
109
  if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
128
- if [[ "$json_mode" == true ]]; then
129
- echo "[]"
130
- else
131
- echo "Not inside a git repository."
132
- fi
110
+ if [[ "$json_mode" == true ]]; then echo "[]"; else print_error "Not inside a git repository."; fi
133
111
  return 1
134
112
  fi
135
-
136
113
  if [[ "$json_mode" == false ]]; then
137
114
  require_fzf || return 1
138
115
  fi
139
116
 
140
- # -----------------------------
141
- # 2. Handle filter (--my flag or arbitrary words)
142
- # -----------------------------
143
117
  local filter=""
144
118
  if [[ "$my_mode" == true ]]; then
145
119
  filter=$(git config user.name)
146
120
  if [[ -z "$filter" && "$json_mode" == false ]]; then
147
- echo "Warning: git config user.name is not set."
121
+ print_warning "git config user.name is not set."
148
122
  fi
149
123
  elif [[ ${#filter_args[@]} -gt 0 ]]; then
150
124
  filter="${filter_args[*]}"
151
125
  fi
152
126
 
153
- # -----------------------------
154
- # 3. Fetch latest from remote
155
- # -----------------------------
156
- if [[ "$json_mode" == false ]]; then
157
- echo "Fetching latest from remote..."
158
- fi
159
- if ! git fetch --prune origin 2>/dev/null; then
160
- if [[ "$json_mode" == false ]]; then
161
- echo "⚠ Fetch failed; continuing with local data." >&2
162
- fi
127
+ local remote
128
+ remote=$(gb_remote)
129
+ [[ "$json_mode" == false ]] && echo "Fetching latest from '$remote'..."
130
+ if ! git fetch --prune --quiet "$remote" 2>/dev/null; then
131
+ [[ "$json_mode" == false ]] && print_warning "Fetch failed; continuing with local data."
163
132
  fi
133
+ GB_BASE=$(gb_base_branch 2>/dev/null) || GB_BASE=""
164
134
 
165
135
  # -----------------------------
166
- # 4. Build branch list sorted by date (oldest first)
136
+ # 2. Build branch lists (oldest first)
167
137
  # -----------------------------
168
- local abort_label="✖ Abort"
169
- local max_branch_length=65
170
-
171
- # Calculate date N months ago (based on stale_months)
172
138
  local threshold_ago
173
- threshold_ago=$(date -v-"${stale_months}m" +%s 2>/dev/null || date -d "${stale_months} months ago" +%s 2>/dev/null)
174
-
175
- # Build full branch list (all branches)
176
- # Format: display_branch | date | author | full_branch (tab-separated, last field is full branch for operations)
177
- local all_branch_list
178
- all_branch_list=$(git for-each-ref --sort=committerdate --format='%(refname:short)|%(committerdate:relative)|%(authorname)|%(committerdate:unix)' refs/remotes/origin | \
179
- grep -v 'origin/HEAD' | \
180
- grep -v '^origin|' | \
181
- while IFS='|' read -r branch date author timestamp; do
182
- # Remove origin/ prefix for display
183
- local full_branch="${branch#origin/}"
184
- local display_branch="$full_branch"
185
- # Truncate branch name if too long
186
- if [[ ${#display_branch} -gt $max_branch_length ]]; then
187
- display_branch="${display_branch:0:$((max_branch_length - 3))}..."
188
- fi
189
- # Format: display branch (padded), date, author, TAB, full branch name
190
- printf "%-${max_branch_length}s %-20s %-25s\t%s\n" "$display_branch" "$date" "$author" "$full_branch"
191
- done || true)
192
-
193
- # Build stale branch list (only branches older than 3 months)
194
- local stale_branch_list
195
- stale_branch_list=$(git for-each-ref --sort=committerdate --format='%(refname:short)|%(committerdate:relative)|%(authorname)|%(committerdate:unix)' refs/remotes/origin | \
196
- grep -v 'origin/HEAD' | \
197
- grep -v '^origin|' | \
198
- while IFS='|' read -r branch date author timestamp; do
199
- # Only include if older than 3 months
200
- if [[ "$timestamp" -lt "$threshold_ago" ]]; then
201
- # Remove origin/ prefix for display
202
- local full_branch="${branch#origin/}"
203
- local display_branch="$full_branch"
204
- # Truncate branch name if too long
205
- if [[ ${#display_branch} -gt $max_branch_length ]]; then
206
- display_branch="${display_branch:0:$((max_branch_length - 3))}..."
207
- fi
208
- # Format: display branch (padded), date, author, TAB, full branch name
209
- printf "%-${max_branch_length}s %-20s %-25s\t%s\n" "$display_branch" "$date" "$author" "$full_branch"
210
- fi
211
- done || true)
139
+ threshold_ago=$(_stale_months_ago "$stale_months")
140
+
141
+ local max_len=65
142
+ local all_list="" stale_list="" json="[" first=true
143
+ local sep=$'\x1f' ref ts rel email name branch display row
144
+ while IFS="$sep" read -r ref ts rel email name; do
145
+ [[ -z "$ref" || "$ref" == "$remote" || "$ref" == "$remote/HEAD" ]] && continue
146
+ branch="${ref#"$remote"/}"
147
+ gb_is_protected "$branch" && continue
212
148
 
213
- if [[ -z "$all_branch_list" ]]; then
214
149
  if [[ "$json_mode" == true ]]; then
215
- echo "[]"
216
- else
217
- echo "No remote branches found."
150
+ if [[ "$all_mode" == true || "$ts" -lt "$threshold_ago" ]]; then
151
+ email="${email#<}"
152
+ email="${email%>}"
153
+ [[ "$first" == true ]] && first=false || json+=","
154
+ json+="{\"last_change_timestamp\":$ts,\"author_email\":\"$(gb_json_escape "$email")\",\"author_name\":\"$(gb_json_escape "$name")\",\"name\":\"$(gb_json_escape "$branch")\",\"last_change_relative\":\"$(gb_json_escape "$rel")\"}"
155
+ fi
156
+ continue
157
+ fi
158
+
159
+ display="$branch"
160
+ if [[ ${#display} -gt $max_len ]]; then
161
+ display="${display:0:$((max_len - 3))}..."
162
+ fi
163
+ row=$(printf "%-${max_len}s %-20s %s" "$display" "$rel" "$name")
164
+ row+=$'\t'"$branch"$'\n'
165
+ all_list+="$row"
166
+ if [[ "$ts" -lt "$threshold_ago" ]]; then
167
+ stale_list+="$row"
218
168
  fi
169
+ done < <(git for-each-ref --sort=committerdate \
170
+ --format="%(refname:short)%1f%(committerdate:unix)%1f%(committerdate:relative)%1f%(committeremail)%1f%(committername)" \
171
+ "refs/remotes/$remote" 2>/dev/null)
172
+
173
+ if [[ "$json_mode" == true ]]; then
174
+ echo "$json]"
219
175
  return 0
220
176
  fi
221
177
 
222
- # -----------------------------
223
- # 5. JSON mode output
224
- # -----------------------------
225
- if [[ "$json_mode" == true ]]; then
226
- local json_output="["
227
- local first=true
228
-
229
- # Helper function to escape JSON strings
230
- _json_escape() {
231
- local str="$1"
232
- str="${str//\\/\\\\}"
233
- str="${str//\"/\\\"}"
234
- str="${str//$'\n'/\\n}"
235
- str="${str//$'\r'/\\r}"
236
- str="${str//$'\t'/\\t}"
237
- echo "$str"
238
- }
239
-
240
- # Build JSON from branches (all or stale based on --all flag)
241
- {
242
- while IFS='|' read -r branch rel_date author_name author_email timestamp; do
243
- [[ -z "$branch" ]] && continue
244
- # Include based on all_mode flag
245
- if [[ "$all_mode" == true ]] || [[ "$timestamp" -lt "$threshold_ago" ]]; then
246
- local name="${branch#origin/}"
247
- # Remove angle brackets from email
248
- author_email="${author_email#<}"
249
- author_email="${author_email%>}"
250
-
251
- # Escape values for JSON
252
- name=$(_json_escape "$name")
253
- rel_date=$(_json_escape "$rel_date")
254
- author_email=$(_json_escape "$author_email")
255
- author_name=$(_json_escape "$author_name")
256
-
257
- if [[ "$first" == true ]]; then
258
- first=false
259
- else
260
- json_output+=","
261
- fi
262
-
263
- json_output+="{\"last_change_timestamp\":$timestamp,\"author_email\":\"$author_email\",\"author_name\":\"$author_name\",\"name\":\"$name\",\"last_change_relative\":\"$rel_date\"}"
264
- fi
265
- 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)
266
- } &>/dev/null
267
- json_output+="]"
268
- echo "$json_output"
178
+ if [[ -z "$all_list" ]]; then
179
+ echo "No deletable remote branches found."
269
180
  return 0
270
181
  fi
271
-
272
- # Create temp files for fzf reload
273
- local stale_file=$(mktemp)
274
- local all_file=$(mktemp)
275
- local state_file=$(mktemp)
276
- local toggle_script=$(mktemp)
277
-
278
- # Ensure cleanup on exit
279
- trap 'rm -f "$stale_file" "$all_file" "$state_file" "$toggle_script"' EXIT INT TERM
280
-
281
- # Write stale branches with header
182
+
183
+ # -----------------------------
184
+ # 3. fzf with a stale/all toggle (Ctrl-T)
185
+ # -----------------------------
186
+ local abort_label="✖ Abort"
187
+ local keys="[TAB] select | [Ctrl-T] toggle all/stale | [Enter] delete | [ESC] exit"
188
+ local tmp_dir
189
+ tmp_dir=$(mktemp -d)
282
190
  {
283
- echo "══ Stale branches (>${stale_months}mo, oldest first) ══ [TAB] select | [Ctrl-A] toggle all/stale | [Enter] delete | [ESC] exit"
284
- if [[ -n "$stale_branch_list" ]]; then
285
- echo "$stale_branch_list"
286
- fi
191
+ echo "══ Stale branches (>${stale_months}mo, oldest first) ══ $keys"
192
+ printf '%s' "$stale_list"
287
193
  echo "$abort_label"
288
- } > "$stale_file"
289
-
290
- # Write all branches with header
194
+ } > "$tmp_dir/stale"
291
195
  {
292
- echo "══ All branches (oldest first) ══ [TAB] select | [Ctrl-A] toggle all/stale | [Enter] delete | [ESC] exit"
293
- if [[ -n "$all_branch_list" ]]; then
294
- echo "$all_branch_list"
295
- fi
196
+ echo "══ All branches (oldest first) ══ $keys"
197
+ printf '%s' "$all_list"
296
198
  echo "$abort_label"
297
- } > "$all_file"
298
-
299
- # Set initial state based on --all flag
300
- if [[ "$all_mode" == true ]]; then
301
- printf "all" > "$state_file"
302
- else
303
- printf "stale" > "$state_file"
304
- fi
305
-
306
- # -----------------------------
307
- # 6. Run fzf picker with preview
308
- # -----------------------------
309
- local stale_count=$(echo "$stale_branch_list" | grep -c . || echo "0")
310
- local all_count=$(echo "$all_branch_list" | grep -c . || echo "0")
311
-
312
- # Create toggle script
313
- cat > "$toggle_script" << TOGGLE_EOF
314
- #!/bin/bash
315
- state=\$(cat "$state_file" | tr -d '\n')
316
- # Toggle: if currently stale, switch to all; if currently all, switch to stale
317
- if [[ "\$state" == "stale" ]]; then
318
- # Switching FROM stale TO all
319
- printf "all" > "$state_file"
320
- cat "$all_file"
199
+ } > "$tmp_dir/all"
200
+ if [[ "$all_mode" == true ]]; then echo "all" > "$tmp_dir/state"; else echo "stale" > "$tmp_dir/state"; fi
201
+ cat > "$tmp_dir/toggle" << 'TOGGLE_EOF'
202
+ dir="$1"
203
+ if [[ "$(cat "$dir/state")" == "stale" ]]; then
204
+ echo "all" > "$dir/state"
321
205
  else
322
- # Switching FROM all TO stale
323
- printf "stale" > "$state_file"
324
- cat "$stale_file"
206
+ echo "stale" > "$dir/state"
325
207
  fi
208
+ cat "$dir/$(cat "$dir/state")"
326
209
  TOGGLE_EOF
327
- chmod +x "$toggle_script"
328
-
329
- # Set initial input file based on mode
330
- local initial_input_file
331
- if [[ "$all_mode" == true ]]; then
332
- initial_input_file="$all_file"
333
- else
334
- initial_input_file="$stale_file"
335
- fi
336
-
210
+
211
+ local tmp_q remote_q
212
+ printf -v tmp_q '%q' "$tmp_dir"
213
+ printf -v remote_q '%q' "$remote"
214
+
337
215
  local selection
338
- selection=$(
339
- cat "$initial_input_file" | fzf \
216
+ selection=$(run_fzf \
340
217
  --query="$filter" \
341
218
  -i \
342
219
  --reverse \
@@ -345,81 +222,64 @@ TOGGLE_EOF
345
222
  --multi \
346
223
  --delimiter=$'\t' \
347
224
  --with-nth=1 \
348
- --bind=enter:accept \
349
- --bind="ctrl-a:reload(bash $toggle_script)+clear-query" \
350
- --preview='
351
- line={}
352
- if [[ "$line" == "✖ Abort" ]]; then
353
- echo "Exit without action";
354
- else
355
- # Extract full branch name (after tab)
356
- branch=$(echo "$line" | cut -f2)
357
- echo "Branch: origin/$branch";
358
- echo "";
359
- echo "Recent commits:";
360
- git log --oneline --color=always -n 15 "origin/$branch" 2>/dev/null || echo "No commits found";
361
- fi
362
- ' \
225
+ --bind="ctrl-t:reload(bash $tmp_q/toggle $tmp_q)+clear-query" \
226
+ --preview="
227
+ branch=\$(printf '%s' {} | cut -f2)
228
+ if [[ -z \"\$branch\" ]]; then echo 'Exit without action'; exit 0; fi
229
+ echo \"Branch: $remote_q/\$branch\"
230
+ echo
231
+ echo 'Recent commits:'
232
+ git log --oneline --color=always -n 15 \"refs/remotes/$remote_q/\$branch\" 2>/dev/null || echo 'No commits found'
233
+ " \
363
234
  --preview-window=right:35% \
364
- < "$initial_input_file"
365
- ) </dev/tty || true
235
+ < "$tmp_dir/$(cat "$tmp_dir/state")"
236
+ ) || true
237
+ rm -rf "$tmp_dir"
366
238
 
367
- # ESC or Ctrl-C
368
239
  if [[ -z "$selection" ]]; then
369
240
  echo "Exited."
370
241
  return 0
371
242
  fi
372
243
 
373
- # Abort option
374
- if echo "$selection" | grep -q "^$abort_label"; then
375
- echo "Aborted."
376
- return 0
377
- fi
378
-
379
244
  # -----------------------------
380
- # 7. Extract branch names and confirm deletion
245
+ # 4. Confirm and delete
381
246
  # -----------------------------
382
- local branches_to_delete=()
247
+ local branches_to_delete=() line
383
248
  while IFS= read -r line; do
384
- # Skip abort option
385
- if [[ "$line" == "✖ Abort" ]]; then
249
+ branch=$(printf '%s' "$line" | cut -f2)
250
+ [[ -z "$branch" ]] && continue
251
+ # Never delete protected branches, even if they got into the list somehow
252
+ if gb_is_protected "$branch"; then
253
+ print_warning "Skipping protected branch '$branch'."
386
254
  continue
387
255
  fi
388
- # Extract full branch name (after tab)
389
- local branch_name
390
- branch_name=$(echo "$line" | cut -f2)
391
- if [[ -n "$branch_name" ]]; then
392
- branches_to_delete+=("$branch_name")
393
- fi
256
+ branches_to_delete+=("$branch")
394
257
  done <<< "$selection"
395
258
 
396
259
  if [[ ${#branches_to_delete[@]} -eq 0 ]]; then
397
- echo "No branches selected."
260
+ echo "Aborted."
398
261
  return 0
399
262
  fi
400
263
 
401
- echo ""
402
- echo "Selected branches to delete from remote:"
264
+ echo
265
+ echo "Selected branches to delete from '$remote':"
266
+ printf ' - %s\n' "${branches_to_delete[@]}"
267
+ echo
268
+
269
+ if ! gb_confirm --strict "Delete these ${#branches_to_delete[@]} branch(es) from '$remote'? This cannot be undone from here." n; then
270
+ echo "Deletion cancelled."
271
+ return 0
272
+ fi
273
+
274
+ local failed=0 out
403
275
  for branch in "${branches_to_delete[@]}"; do
404
- echo " - origin/$branch"
276
+ if out=$(git push "$remote" --delete "$branch" 2>&1); then
277
+ print_success "Deleted $remote/$branch"
278
+ else
279
+ print_error "Failed to delete $remote/$branch:"
280
+ printf '%s\n' "$out" | sed 's/^/ /' >&2
281
+ failed=$((failed + 1))
282
+ fi
405
283
  done
406
- echo ""
407
-
408
- prompt_read "Delete these ${#branches_to_delete[@]} branch(es) from remote? (y/N): " confirm
409
- case "$confirm" in
410
- [yY][eE][sS]|[yY])
411
- echo "Deleting branches..."
412
- for branch in "${branches_to_delete[@]}"; do
413
- echo "Deleting origin/$branch ..."
414
- if git push origin --delete "$branch" 2>/dev/null; then
415
- echo "✓ Deleted origin/$branch"
416
- else
417
- echo "✗ Failed to delete origin/$branch"
418
- fi
419
- done
420
- ;;
421
- *)
422
- echo "Deletion cancelled."
423
- ;;
424
- esac
284
+ [[ $failed -eq 0 ]]
425
285
  }
package/commands/stash.sh CHANGED
@@ -1,15 +1,11 @@
1
1
  #!/usr/bin/env bash
2
- # shellcheck disable=SC2155
3
2
 
4
3
  # Source common utilities
5
- SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
4
+ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
5
  # shellcheck source=_utils.sh
7
6
  source "$SOURCE_DIR/_utils.sh"
8
7
 
9
8
  stash() {
10
- # -----------------------------
11
- # 0. Check for help/version flag
12
- # -----------------------------
13
9
  if [[ "${1:-}" == "-v" || "${1:-}" == "--version" ]]; then
14
10
  echo "gitbash ${FUNCNAME[0]} v$VERSION"
15
11
  return 0
@@ -25,29 +21,20 @@ Options:
25
21
  -h, --help Show this help message
26
22
 
27
23
  Features:
28
- - Stashes all changed files (staged and unstaged)
24
+ - Stashes all changes: staged, unstaged and untracked files
29
25
  - Custom stash message for easy identification
30
- - Interactive mode if no name provided
31
- - Works with untracked files
32
26
 
33
27
  Examples:
34
- # Interactive mode (prompts for name)
35
28
  $ stash
36
29
  Stash name: work in progress on login
37
30
  ✓ Created stash: work in progress on login
38
31
 
39
- # Direct mode (name as arguments)
40
32
  $ stash fix for authentication bug
41
33
  ✓ Created stash: fix for authentication bug
42
34
 
43
- # View your stashes
44
- $ git stash list
45
- stash@{0}: On main: fix for authentication bug
46
- stash@{1}: On main: work in progress on login
47
-
48
35
  Requirements:
49
- - Must be in a git repository
50
- - Must have changes to stash (staged, unstaged, or untracked)
36
+ - Must be in a git repository with at least one commit
37
+ - Must have changes to stash
51
38
 
52
39
  See also:
53
40
  stashes -h Show help for stashes menu
@@ -58,45 +45,30 @@ EOF
58
45
  return 0
59
46
  fi
60
47
 
61
- # -----------------------------
62
- # 1. Check if inside a git repository
63
- # -----------------------------
64
- if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
65
- echo "Error: Not inside a git repository."
48
+ require_git_repo || return 1
49
+
50
+ if ! git rev-parse --verify --quiet HEAD >/dev/null; then
51
+ print_error "Cannot stash before the first commit."
66
52
  return 1
67
53
  fi
68
54
 
69
- # -----------------------------
70
- # 2. Check for changes to stash
71
- # -----------------------------
72
- if git diff --quiet && git diff --cached --quiet && [[ -z "$(git ls-files --others --exclude-standard)" ]]; then
55
+ if [[ -z "$(git status --porcelain)" ]]; then
73
56
  echo "No changes to stash."
74
57
  return 1
75
58
  fi
76
59
 
77
- # -----------------------------
78
- # 3. Get stash name
79
- # -----------------------------
80
60
  local stash_name
81
-
82
61
  if [[ $# -gt 0 ]]; then
83
- # Name provided as arguments
84
62
  stash_name="$*"
85
63
  else
86
- # Interactive mode - prompt for name
87
- echo -n "Stash name: "
88
- read -r stash_name
89
-
64
+ prompt_read "Stash name: " stash_name || true
90
65
  if [[ -z "$stash_name" ]]; then
91
66
  print_error "Stash name cannot be empty."
92
67
  return 1
93
68
  fi
94
69
  fi
95
70
 
96
- # -----------------------------
97
- # 4. Create the stash
98
- # -----------------------------
99
- if git stash push -u -m "$stash_name"; then
71
+ if git stash push --include-untracked -m "$stash_name"; then
100
72
  print_success "Created stash: $stash_name"
101
73
  else
102
74
  print_error "Failed to create stash."