gitbash 2.2.2 → 3.0.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # gitbash
2
2
 
3
- Pure bash, zero-dependency git utilities. Reduce repetitive typing and supercharge git workflows with optional interactive previews and safe cleanup — see a [full comparison](./docs/workflows.md).
3
+ Git utilities written in pure bash: only git is required, fzf, delta and bat are optional. Reduce repetitive typing and supercharge git workflows with optional interactive previews and safe cleanup — see a [full comparison](./docs/workflows.md).
4
4
 
5
5
  ![screenshot-status.png](./docs/screenshot-status.png)
6
6
 
@@ -76,7 +76,7 @@ curl -fsSL https://raw.githubusercontent.com/jaggli/gitbash/main/install.sh | sh
76
76
  irm https://raw.githubusercontent.com/jaggli/gitbash/main/install.ps1 | iex
77
77
  ```
78
78
 
79
- It installs to `~/.local/share/gitbash` and links `~/.local/bin/gitbash` (add that folder to your PATH if the script says so); delete both to uninstall. Set `GITBASH_VERSION` for a specific version, and `GITBASH_INSTALL_DIR` / `GITBASH_BIN_DIR` for other locations.
79
+ It downloads the package published on npm, checks it against npm's sha512 checksum, and installs it to `~/.local/share/gitbash` and links `~/.local/bin/gitbash` (add that folder to your PATH if the script says so); delete both to uninstall. Set `GITBASH_VERSION` for a specific version, and `GITBASH_INSTALL_DIR` / `GITBASH_BIN_DIR` for other locations.
80
80
 
81
81
  ## Options
82
82
 
@@ -94,6 +94,8 @@ It installs to `~/.local/share/gitbash` and links `~/.local/bin/gitbash` (add th
94
94
 
95
95
  ## Commands
96
96
 
97
+ `-y` / `--yes` answers every yes/no question with yes and is passed on to the commands a command runs (e.g. `pr -y` → `commit`). Deleting unmerged work or remote branches and force-pushing always ask. Options with a value accept `--days=14` and `--days 14`.
98
+
97
99
  ### branch
98
100
 
99
101
  Interactive menu for branch operations (create/switch/update).
@@ -107,7 +109,7 @@ branch --version # Show version
107
109
  ### create
108
110
 
109
111
  ```bash
110
- create [--feature|--bugfix|--hotfix|--release|-t] [--push|--no-push] [JIRA_LINK|ISSUE] [TITLE...]
112
+ create [--feature|--bugfix|--hotfix|--release|-t] [-p|--push|--no-push] [-y] [JIRA_LINK|ISSUE] [TITLE...]
111
113
  ```
112
114
 
113
115
  Create a branch from the latest `origin/<base>` with optional Jira parsing, then push and track it (`GITBASH_CREATE_AUTO_PUSH`, `--no-push`).
@@ -140,7 +142,7 @@ create # Interactive mode (no Jira prompt)
140
142
  ```bash
141
143
  create PROJ-123 fix bug # → feature/PROJ-123-fix-bug
142
144
  create fix bug # → feature/NOISSUE-fix-bug
143
- create enhance login screen # → feature/enhance-login-screen (parsing disabled)
145
+ create --no-push PROJ-7 quick fix # → feature/PROJ-7-quick-fix, not pushed
144
146
  ```
145
147
 
146
148
  ### switch
@@ -191,7 +193,7 @@ Interactive staging with fzf and diff previews.
191
193
  ### pr
192
194
 
193
195
  ```bash
194
- pr [-p|--push] [--print]
196
+ pr [-p|--push] [--print] [-y|--yes]
195
197
  ```
196
198
 
197
199
  Open the pull request in the browser: the existing PR via the GitHub CLI when available, otherwise the create page (GitHub, GitHub Enterprise, GitLab, Bitbucket, Azure DevOps). Offers to commit local changes and to push a branch that is not on the remote yet. `-p` pushes first, `--print` prints the URL.
@@ -215,7 +217,7 @@ Reset the current branch like a fresh clone: fetch, `git reset --hard <remote>/<
215
217
  ### update
216
218
 
217
219
  ```bash
218
- update [-p|--push]
220
+ update [-p|--push] [-y|--yes]
219
221
  ```
220
222
 
221
223
  Merge the latest `origin/<base>` into the current branch. With local changes, asks to commit, stash (restored afterwards) or abort. Lists conflicts and opens the merge tool (`GITBASH_MERGE_COMMAND`). On the base branch itself, fast-forwards it.
@@ -240,23 +242,23 @@ Interactive stash menu: create, apply, or delete stashes.
240
242
  ### stash
241
243
 
242
244
  ```bash
243
- stash [NAME...]
245
+ stash [--] [NAME...]
244
246
  ```
245
247
 
246
- Create named stash (includes untracked files).
248
+ Create named stash (includes untracked files). Use `--` for a name starting with `-`.
247
249
 
248
250
  ### unstash
249
251
 
250
- Apply stash with fzf picker. Drops it afterwards unless you say no.
252
+ Apply stash with fzf picker. Drops it afterwards unless you say no. `-y` drops it without asking.
251
253
 
252
254
  ### cleanstash
253
255
 
254
- Delete stashes (multi-select with TAB).
256
+ Delete stashes (multi-select with TAB). `-y` skips the confirmation.
255
257
 
256
258
  ### cleanup
257
259
 
258
260
  ```bash
259
- cleanup [--dry-run] [--days=N] [-y|--yes] [--json]
261
+ cleanup [-n|--dry-run] [--days=N] [-y|--yes] [--json]
260
262
  ```
261
263
 
262
264
  Find and delete leftover local branches:
@@ -288,7 +290,7 @@ Unpushed commits are shown per branch. Deletes with `git branch -d`; force-delet
288
290
  ### commits
289
291
 
290
292
  ```bash
291
- commits [-a|--all] [COUNT]
293
+ commits [-a|--all] [-y|--yes] [COUNT]
292
294
  ```
293
295
 
294
296
  List the current branch's own commits with option to revert. On a feature branch only commits not in the base branch are shown; on the base branch its first-parent history (merged branches as their merge commit). If the upstream has new commits, offers to fast-forward first. Multi-select with TAB; selected commits are reverted newest first.
package/bin/gitbash CHANGED
@@ -20,7 +20,9 @@ if [[ "$SCRIPT_DIR" == */node_modules/.pnpm/gitbash@*/node_modules/gitbash &&
20
20
  fi
21
21
  COMMANDS_DIR="$SCRIPT_DIR/commands"
22
22
  VERSION="$(grep -o '"version": *"[^"]*"' "$SCRIPT_DIR/package.json" | head -1 | cut -d'"' -f4)"
23
- export SCRIPT_DIR GITBASH_BIN VERSION
23
+ # Not exported: they would leak into git hooks, editors and merge tools.
24
+ # Commands are sourced into this process; nested runs of gitbash set them again.
25
+ export -n SCRIPT_DIR GITBASH_BIN VERSION
24
26
 
25
27
  # shellcheck source=../commands/_utils.sh
26
28
  source "$COMMANDS_DIR/_utils.sh"
@@ -394,7 +396,8 @@ check_pwsh_integration() {
394
396
  # Restricted (the default of Windows PowerShell) and AllSigned block the profile
395
397
  case "$policy" in
396
398
  Restricted|AllSigned|Undefined)
397
- if gb_confirm "PowerShell blocks scripts, including your profile ($policy). Allow local scripts (execution policy RemoteSigned)?" y; then
399
+ # A security setting: never changed by pressing Enter or by GITBASH_ASSUME_YES
400
+ if gb_confirm --strict "PowerShell blocks scripts, including your profile ($policy). Allow local scripts (execution policy RemoteSigned)?" n; then
398
401
  if "$exe" -NoProfile -NonInteractive -Command 'Set-ExecutionPolicy -Scope CurrentUser RemoteSigned -Force'; then
399
402
  print_success "Local scripts allowed."
400
403
  else
@@ -111,7 +111,8 @@ prompt_read() {
111
111
  }
112
112
 
113
113
  # Yes/no question. Returns 0 for yes, 1 for no. EOF or Enter picks the default.
114
- # GITBASH_ASSUME_YES=1 (set by --yes flags) answers yes, unless --strict is given.
114
+ # GITBASH_ASSUME_YES=1 (exported by --yes flags, so commands run from a command
115
+ # inherit it) answers yes, unless --strict is given.
115
116
  # Usage: gb_confirm [--strict] "Question?" y|n
116
117
  gb_confirm() {
117
118
  local strict=false
@@ -269,7 +270,12 @@ _gb_normalize_config() {
269
270
  GITBASH_CLEANUP_DAYS=7
270
271
  fi
271
272
  GITBASH_CLEANUP_DAYS="${GITBASH_CLEANUP_DAYS:-7}"
272
- if ! [[ "${GITBASH_REMOTE:-origin}" =~ ^[A-Za-z0-9._-]+$ ]]; then
273
+ # Values reach git as arguments: a leading '-' would be read as an option
274
+ if [[ -n "$GITBASH_BASE_BRANCH" ]] && ! _gb_valid_branch_name "$GITBASH_BASE_BRANCH"; then
275
+ print_warning "Invalid GITBASH_BASE_BRANCH '$GITBASH_BASE_BRANCH', detecting the base branch instead."
276
+ GITBASH_BASE_BRANCH=""
277
+ fi
278
+ if ! [[ "${GITBASH_REMOTE:-origin}" =~ ^[A-Za-z0-9._][A-Za-z0-9._-]*$ ]]; then
273
279
  print_warning "Invalid GITBASH_REMOTE '$GITBASH_REMOTE', using 'origin'."
274
280
  GITBASH_REMOTE="origin"
275
281
  fi
@@ -345,15 +351,24 @@ gb_current_branch() {
345
351
  git symbolic-ref --quiet --short HEAD 2>/dev/null
346
352
  }
347
353
 
348
- # Detect the base branch: GITBASH_BASE_BRANCH, <remote>/HEAD, main, master
354
+ # Returns 0 if $1 is a valid branch name that can't be mistaken for an option.
355
+ # (git itself accepts refs like "refs/heads/--output=file", so check the dash too.)
356
+ _gb_valid_branch_name() {
357
+ [[ -n "$1" && "$1" != -* ]] && git check-ref-format "refs/heads/$1" >/dev/null 2>&1
358
+ }
359
+
360
+ # Detect the base branch: GITBASH_BASE_BRANCH, <remote>/HEAD, main, master.
361
+ # The result is passed to git as an argument: names from a committed .gitbashrc
362
+ # or from the remote are only used when valid.
349
363
  gb_base_branch() {
350
364
  local remote branch
351
365
  remote=$(gb_remote)
352
- if [[ -n "${GITBASH_BASE_BRANCH:-}" ]]; then
366
+ if [[ -n "${GITBASH_BASE_BRANCH:-}" ]] && _gb_valid_branch_name "$GITBASH_BASE_BRANCH"; then
353
367
  echo "$GITBASH_BASE_BRANCH"
354
368
  return 0
355
369
  fi
356
- if branch=$(git symbolic-ref --quiet --short "refs/remotes/$remote/HEAD" 2>/dev/null); then
370
+ if branch=$(git symbolic-ref --quiet --short "refs/remotes/$remote/HEAD" 2>/dev/null) &&
371
+ _gb_valid_branch_name "${branch#"$remote"/}"; then
357
372
  echo "${branch#"$remote"/}"
358
373
  return 0
359
374
  fi
@@ -434,18 +449,39 @@ gb_sync_and_push() {
434
449
  done
435
450
 
436
451
  local remote branch
437
- remote=$(gb_remote)
438
452
  if ! branch=$(gb_current_branch); then
439
453
  print_error "Detached HEAD: check out a branch before pushing."
440
454
  return 1
441
455
  fi
456
+
457
+ # Push to the branch's upstream (e.g. a fork, or a different name on the remote),
458
+ # else to <GITBASH_REMOTE>/<branch>. An upstream that is a protected branch (a
459
+ # branch started from origin/main tracks main) is never pushed to.
460
+ local target="$branch" set_upstream=false up_remote up_merge
461
+ remote=$(gb_remote)
462
+ up_remote=$(git config "branch.$branch.remote" 2>/dev/null) || up_remote=""
463
+ up_merge=$(git config "branch.$branch.merge" 2>/dev/null) || up_merge=""
464
+ up_merge="${up_merge#refs/heads/}"
465
+ if [[ -n "$up_remote" && "$up_remote" != "." && "$up_remote" != -* && -n "$up_merge" ]] &&
466
+ git remote get-url "$up_remote" >/dev/null 2>&1 &&
467
+ _gb_valid_branch_name "$up_merge" && ! gb_is_protected "$up_merge"; then
468
+ remote="$up_remote"
469
+ target="$up_merge"
470
+ else
471
+ set_upstream=true
472
+ fi
442
473
  if ! git remote get-url "$remote" >/dev/null 2>&1; then
443
474
  print_error "No remote '$remote' configured."
444
475
  return 1
445
476
  fi
477
+ local refspec="$branch" where="$remote"
478
+ if [[ "$target" != "$branch" ]]; then
479
+ refspec="$branch:refs/heads/$target"
480
+ where="$remote/$target"
481
+ fi
446
482
 
447
483
  local out
448
- if ! out=$(git ls-remote --heads "$remote" "refs/heads/$branch" 2>&1); then
484
+ if ! out=$(git ls-remote --heads "$remote" "refs/heads/$target" 2>&1); then
449
485
  print_error "Could not reach '$remote':"
450
486
  printf '%s\n' "$out" >&2
451
487
  return 1
@@ -453,40 +489,40 @@ gb_sync_and_push() {
453
489
 
454
490
  # New branch: first push sets up tracking
455
491
  if [[ -z "$out" ]]; then
456
- print_info "Pushing new branch '$branch' to '$remote'..."
457
- if git push -u "$remote" "$branch"; then
458
- print_success "Pushed '$branch' to '$remote'."
492
+ print_info "Pushing new branch '$branch' to '$where'..."
493
+ if git push -u "$remote" "$refspec"; then
494
+ print_success "Pushed '$branch' to '$where'."
459
495
  return 0
460
496
  fi
461
- print_error "Failed to push '$branch' to '$remote'."
497
+ print_error "Failed to push '$branch' to '$where'."
462
498
  return 1
463
499
  fi
464
500
 
465
- if ! git fetch --quiet "$remote" "+refs/heads/$branch:refs/remotes/$remote/$branch"; then
466
- print_error "Failed to fetch '$remote/$branch'."
501
+ if ! git fetch --quiet "$remote" "+refs/heads/$target:refs/remotes/$remote/$target"; then
502
+ print_error "Failed to fetch '$remote/$target'."
467
503
  return 1
468
504
  fi
469
505
 
470
506
  local local_commit remote_commit
471
507
  local_commit=$(git rev-parse HEAD)
472
- remote_commit=$(git rev-parse "refs/remotes/$remote/$branch")
508
+ remote_commit=$(git rev-parse "refs/remotes/$remote/$target")
473
509
 
474
510
  if [[ "$local_commit" == "$remote_commit" ]]; then
475
- print_info "'$remote/$branch' is already up to date."
511
+ print_info "'$remote/$target' is already up to date."
476
512
  return 0
477
513
  fi
478
514
 
479
515
  local push_args=()
480
- if [[ -z "$(git config "branch.$branch.remote" 2>/dev/null)" ]]; then
516
+ if [[ "$set_upstream" == true ]]; then
481
517
  push_args+=("-u")
482
518
  fi
483
519
 
484
520
  if git merge-base --is-ancestor "$remote_commit" "$local_commit"; then
485
521
  : # Ahead of the remote: plain push
486
522
  elif git merge-base --is-ancestor "$local_commit" "$remote_commit"; then
487
- print_info "'$remote/$branch' has new commits. Fast-forwarding..."
488
- if git merge --ff-only --quiet "refs/remotes/$remote/$branch"; then
489
- print_success "Up to date with '$remote/$branch'. Nothing to push."
523
+ print_info "'$remote/$target' has new commits. Fast-forwarding..."
524
+ if git merge --ff-only --quiet "refs/remotes/$remote/$target"; then
525
+ print_success "Up to date with '$remote/$target'. Nothing to push."
490
526
  return 0
491
527
  fi
492
528
  print_error "Fast-forward failed."
@@ -496,10 +532,10 @@ gb_sync_and_push() {
496
532
  ahead=$(git rev-list --count "$remote_commit..$local_commit")
497
533
  behind=$(git rev-list --count "$local_commit..$remote_commit")
498
534
  if [[ "$amend" == true || "$force" == true ]]; then
499
- print_warning "'$remote/$branch' differs from your rewritten branch ($ahead local vs $behind remote commit(s))."
500
- if gb_confirm --strict "Overwrite '$remote/$branch' with your version (push --force-with-lease)?" n; then
501
- if git push "${push_args[@]+"${push_args[@]}"}" --force-with-lease="refs/heads/$branch:$remote_commit" "$remote" "$branch"; then
502
- print_success "Force-pushed '$branch' to '$remote'."
535
+ print_warning "'$remote/$target' differs from your rewritten branch ($ahead local vs $behind remote commit(s))."
536
+ if gb_confirm --strict "Overwrite '$remote/$target' with your version (push --force-with-lease)?" n; then
537
+ if git push "${push_args[@]+"${push_args[@]}"}" --force-with-lease="refs/heads/$target:$remote_commit" "$remote" "$refspec"; then
538
+ print_success "Force-pushed '$branch' to '$where'."
503
539
  return 0
504
540
  fi
505
541
  print_error "Force push failed (someone else may have pushed in the meantime)."
@@ -509,18 +545,18 @@ gb_sync_and_push() {
509
545
  return 1
510
546
  fi
511
547
 
512
- print_warning "Your branch and '$remote/$branch' have diverged ($ahead local, $behind remote commit(s))."
548
+ print_warning "Your branch and '$remote/$target' have diverged ($ahead local, $behind remote commit(s))."
513
549
  local choice
514
- gb_choice choice "[r]ebase onto '$remote/$branch', [m]erge it, or [a]bort? (r/m/A):" "rma" "a"
550
+ gb_choice choice "[r]ebase onto '$remote/$target', [m]erge it, or [a]bort? (r/m/A):" "rma" "a"
515
551
  case "$choice" in
516
552
  r)
517
- if ! git rebase --rebase-merges "refs/remotes/$remote/$branch"; then
553
+ if ! git rebase --rebase-merges "refs/remotes/$remote/$target"; then
518
554
  print_error "Rebase stopped. Resolve the conflicts and run 'git rebase --continue', or 'git rebase --abort'."
519
555
  return 1
520
556
  fi
521
557
  ;;
522
558
  m)
523
- if ! git merge --no-edit "refs/remotes/$remote/$branch"; then
559
+ if ! git merge --no-edit "refs/remotes/$remote/$target"; then
524
560
  print_error "Merge stopped. Resolve the conflicts and commit, or run 'git merge --abort'."
525
561
  return 1
526
562
  fi
@@ -532,12 +568,12 @@ gb_sync_and_push() {
532
568
  esac
533
569
  fi
534
570
 
535
- print_info "Pushing '$branch' to '$remote'..."
536
- if git push "${push_args[@]+"${push_args[@]}"}" "$remote" "$branch"; then
537
- print_success "Pushed '$branch' to '$remote'."
571
+ print_info "Pushing '$branch' to '$where'..."
572
+ if git push "${push_args[@]+"${push_args[@]}"}" "$remote" "$refspec"; then
573
+ print_success "Pushed '$branch' to '$where'."
538
574
  return 0
539
575
  fi
540
- print_error "Failed to push '$branch' to '$remote'."
576
+ print_error "Failed to push '$branch' to '$where'."
541
577
  return 1
542
578
  }
543
579
 
@@ -30,60 +30,12 @@ Features:
30
30
  - Update: Update current branch with latest main/master
31
31
 
32
32
  Navigation:
33
- ↑/↓ or j/k Navigate through options
33
+ ↑/↓ Navigate through options (typing filters the list)
34
34
  Enter Select action
35
35
  ESC/Ctrl-C Abort
36
36
 
37
37
  Examples:
38
- $ branch
39
- Branch action >
40
- > 🌿 Create - Create a new feature branch
41
- 🔀 Switch - Switch to another branch
42
- ⬆️ Update - Update current branch with main/master
43
- ✖ Abort
44
-
45
- # Select "Create" and press Enter
46
- Enter Jira link (e.g., https://jira.company.com/browse/PROJ-123):
47
- > PROJ-123
48
- Parsed issue number: PROJ-123
49
-
50
- Enter branch title (will be converted to lowercase with dashes):
51
- > add new feature
52
-
53
- Branch name: feature/PROJ-123-add-new-feature
54
- Updating 'main' from origin...
55
- ✓ 'main' is up to date.
56
- ✓ Successfully created and switched to branch: feature/PROJ-123-add-new-feature (from main)
57
-
58
- ---
59
-
60
- $ branch
61
- Branch action >
62
- > 🌿 Create - Create a new feature branch
63
- 🔀 Switch - Switch to another branch
64
- ⬆️ Update - Update current branch with main/master
65
- ✖ Abort
66
-
67
- # Select "Switch" and press Enter
68
- Select branch:
69
- Current: main
70
- > local: main
71
- local: feature/PROJ-123-add-new-feature
72
- remote: origin/develop
73
-
74
- ---
75
-
76
- $ branch
77
- Branch action >
78
- > 🌿 Create - Create a new feature branch
79
- 🔀 Switch - Switch to another branch
80
- ⬆️ Update - Update current branch with main/master
81
- ✖ Abort
82
-
83
- # Select "Update" and press Enter
84
- Fetching latest 'main' from origin...
85
- Merging updated 'main' into 'feature/PROJ-123-add-new-feature'...
86
- ✓ 'feature/PROJ-123-add-new-feature' is now up-to-date with 'main'.
38
+ $ branch # pick Create, Switch or Update, then continue as in that command
87
39
 
88
40
  Actions:
89
41
  🌿 Create - Create a new feature branch with Jira parsing
@@ -102,6 +54,12 @@ EOF
102
54
  return 0
103
55
  fi
104
56
 
57
+ if [[ $# -gt 0 ]]; then
58
+ print_error "Unknown argument: $1"
59
+ echo "Usage: branch [-h|--help]" >&2
60
+ return 1
61
+ fi
62
+
105
63
  # -----------------------------
106
64
  # 1. Check for fzf
107
65
  # -----------------------------
@@ -20,6 +20,7 @@ Delete git stashes without applying them.
20
20
  Interactive fuzzy finder (fzf) with preview and multi-select support.
21
21
 
22
22
  Options:
23
+ -y, --yes Delete the selected stashes without asking again
23
24
  -h, --help Show this help message
24
25
 
25
26
  Navigation:
@@ -40,6 +41,19 @@ EOF
40
41
  return 0
41
42
  fi
42
43
 
44
+ while [[ $# -gt 0 ]]; do
45
+ case "$1" in
46
+ -h|--help) cleanstash --help; return 0 ;;
47
+ -y|--yes) export GITBASH_ASSUME_YES=1 ;;
48
+ *)
49
+ print_error "Unknown argument: $1"
50
+ echo "Usage: cleanstash [-y|--yes]" >&2
51
+ return 1
52
+ ;;
53
+ esac
54
+ shift
55
+ done
56
+
43
57
  require_git_repo || return 1
44
58
 
45
59
  local stash_list
@@ -45,7 +45,7 @@ Find and delete local branches that are no longer needed.
45
45
  Options:
46
46
  -h, --help Show this help message
47
47
  --json Output branch data as JSON (non-interactive)
48
- --dry-run List the branches that would be pre-selected, without deleting
48
+ -n, --dry-run List the branches that would be pre-selected, without deleting
49
49
  --days=N Override stale threshold (default: 7 days, configurable via GITBASH_CLEANUP_DAYS)
50
50
  -y, --yes Delete the pre-selected branches without the picker (safe delete only)
51
51
 
@@ -88,12 +88,17 @@ EOF
88
88
  json_mode=true
89
89
  shift
90
90
  ;;
91
- --dry-run)
91
+ -n|--dry-run)
92
92
  dry_run=true
93
93
  shift
94
94
  ;;
95
- --days=*)
96
- days_threshold="${1#--days=}"
95
+ --days=*|--days)
96
+ if [[ "$1" == --days ]]; then
97
+ days_threshold="${2:-}"
98
+ shift
99
+ else
100
+ days_threshold="${1#--days=}"
101
+ fi
97
102
  if ! [[ "$days_threshold" =~ ^[1-9][0-9]*$ ]]; then
98
103
  print_error "Invalid days value: $days_threshold"
99
104
  return 1
@@ -101,7 +106,7 @@ EOF
101
106
  shift
102
107
  ;;
103
108
  -y|--yes)
104
- GITBASH_ASSUME_YES=1
109
+ export GITBASH_ASSUME_YES=1
105
110
  shift
106
111
  ;;
107
112
  *)
@@ -83,7 +83,7 @@ EOF
83
83
  shift
84
84
  ;;
85
85
  -y|--yes)
86
- GITBASH_ASSUME_YES=1
86
+ export GITBASH_ASSUME_YES=1
87
87
  shift
88
88
  ;;
89
89
  --force-with-lease)
@@ -13,7 +13,7 @@ commits() {
13
13
  fi
14
14
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
15
15
  gb_help << 'EOF'
16
- Usage: commits [-a|--all] [COUNT]
16
+ Usage: commits [-a|--all] [-y|--yes] [COUNT]
17
17
 
18
18
  List the commits made on the current branch with option to revert selected ones.
19
19
 
@@ -26,6 +26,7 @@ Arguments:
26
26
 
27
27
  Options:
28
28
  -a, --all Show all recent commits reachable from HEAD
29
+ -y, --yes Revert the selected commits without asking again
29
30
 
30
31
  Navigation:
31
32
  ↑/↓ Navigate through commits
@@ -59,6 +60,7 @@ EOF
59
60
  while [[ $# -gt 0 ]]; do
60
61
  case "$1" in
61
62
  -a|--all) all=true ;;
63
+ -y|--yes) export GITBASH_ASSUME_YES=1 ;;
62
64
  -*) print_error "Unknown option '$1'. See 'commits --help'."; return 1 ;;
63
65
  *)
64
66
  if [[ -n "$count" ]]; then
@@ -63,8 +63,9 @@ Options:
63
63
  --bugfix Use 'bugfix/' type
64
64
  --hotfix Use 'hotfix/' type
65
65
  --release Use 'release/' type
66
- --push Push the new branch to the remote (default: GITBASH_CREATE_AUTO_PUSH)
66
+ -p, --push Push the new branch to the remote (default: GITBASH_CREATE_AUTO_PUSH)
67
67
  --no-push Don't push the new branch
68
+ -y, --yes Don't ask for confirmations (switches to an existing branch of the same name)
68
69
 
69
70
  Configuration (run 'gitbash --config'):
70
71
  - GITBASH_CREATE_BRANCH_PREFIX: Custom prefix between type and issue (default: "")
@@ -101,7 +102,8 @@ EOF
101
102
  --bugfix) branch_type="bugfix/"; shift ;;
102
103
  --hotfix) branch_type="hotfix/"; shift ;;
103
104
  --release) branch_type="release/"; shift ;;
104
- --push) auto_push="yes"; shift ;;
105
+ -p|--push) auto_push="yes"; shift ;;
106
+ -y|--yes) export GITBASH_ASSUME_YES=1; shift ;;
105
107
  --no-push) auto_push="no"; shift ;;
106
108
  --)
107
109
  shift
package/commands/pr.sh CHANGED
@@ -78,7 +78,7 @@ EOF
78
78
  shift
79
79
  ;;
80
80
  -y|--yes)
81
- GITBASH_ASSUME_YES=1
81
+ export GITBASH_ASSUME_YES=1
82
82
  shift
83
83
  ;;
84
84
  *)
package/commands/stale.sh CHANGED
@@ -84,8 +84,13 @@ EOF
84
84
  my_mode=true
85
85
  shift
86
86
  ;;
87
- --age=*)
88
- stale_months="${1#--age=}"
87
+ --age=*|--age)
88
+ if [[ "$1" == --age ]]; then
89
+ stale_months="${2:-}"
90
+ shift
91
+ else
92
+ stale_months="${1#--age=}"
93
+ fi
89
94
  if ! [[ "$stale_months" =~ ^[1-9][0-9]*$ ]]; then
90
95
  print_error "Invalid age value: $stale_months"
91
96
  return 1
@@ -273,7 +278,8 @@ TOGGLE_EOF
273
278
 
274
279
  local failed=0 out
275
280
  for branch in "${branches_to_delete[@]}"; do
276
- if out=$(git push "$remote" --delete "$branch" 2>&1); then
281
+ # A refspec, not a bare name: branch names come from the remote
282
+ if out=$(git push "$remote" ":refs/heads/$branch" 2>&1); then
277
283
  print_success "Deleted $remote/$branch"
278
284
  else
279
285
  print_error "Failed to delete $remote/$branch:"
package/commands/stash.sh CHANGED
@@ -12,13 +12,14 @@ stash() {
12
12
  fi
13
13
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
14
14
  gb_help << 'EOF'
15
- Usage: stash [NAME...]
15
+ Usage: stash [--] [NAME...]
16
16
 
17
17
  Create a git stash with a descriptive name.
18
18
  If no name is provided, prompts for one interactively.
19
19
 
20
20
  Options:
21
21
  -h, --help Show this help message
22
+ -- Everything after this is the name (even if it starts with '-')
22
23
 
23
24
  Features:
24
25
  - Stashes all changes: staged, unstaged and untracked files
@@ -45,6 +46,20 @@ EOF
45
46
  return 0
46
47
  fi
47
48
 
49
+ local name_parts=()
50
+ while [[ $# -gt 0 ]]; do
51
+ case "$1" in
52
+ -h|--help) stash --help; return 0 ;;
53
+ --) shift; name_parts+=("$@"); break ;;
54
+ -*)
55
+ print_error "Unknown option: $1 (use 'stash -- $1' for a name starting with '-')"
56
+ return 1
57
+ ;;
58
+ *) name_parts+=("$1") ;;
59
+ esac
60
+ shift
61
+ done
62
+
48
63
  require_git_repo || return 1
49
64
 
50
65
  if ! git rev-parse --verify --quiet HEAD >/dev/null; then
@@ -58,8 +73,8 @@ EOF
58
73
  fi
59
74
 
60
75
  local stash_name
61
- if [[ $# -gt 0 ]]; then
62
- stash_name="$*"
76
+ if [[ ${#name_parts[@]} -gt 0 ]]; then
77
+ stash_name="${name_parts[*]}"
63
78
  else
64
79
  prompt_read "Stash name: " stash_name || true
65
80
  if [[ -z "$stash_name" ]]; then
@@ -29,60 +29,12 @@ Features:
29
29
  - Clean up: Delete stashes without applying them
30
30
 
31
31
  Navigation:
32
- ↑/↓ or j/k Navigate through options
32
+ ↑/↓ Navigate through options (typing filters the list)
33
33
  Enter Select action
34
34
  ESC/Ctrl-C Abort
35
35
 
36
36
  Examples:
37
- $ stashes
38
- Stash action >
39
- > 💾 Stash - Create a new stash with a name
40
- 📦 Unstash - Apply and optionally drop a stash
41
- 🗑️ Clean up - Delete stashes without applying
42
- ✖ Abort
43
-
44
- # Select "Stash" and press Enter
45
- Stash name: work in progress
46
- ✓ Created stash: work in progress
47
-
48
- ---
49
-
50
- $ stashes
51
- Stash action >
52
- > 💾 Stash - Create a new stash with a name
53
- 📦 Unstash - Apply and optionally drop a stash
54
- 🗑️ Clean up - Delete stashes without applying
55
- ✖ Abort
56
-
57
- # Select "Unstash" and press Enter
58
- Available stashes >
59
- > stash@{0}: WIP on main: 1a2b3c4 commit message
60
- stash@{1}: WIP on feature: 5d6e7f8 another commit
61
- ✖ Abort
62
-
63
- Applying stash@{0} ...
64
- Drop stash@{0} now? (Y/n): y
65
-
66
- ---
67
-
68
- $ stashes
69
- Stash action >
70
- > 📦 Unstash - Apply and optionally drop a stash
71
- 🗑️ Clean up - Delete stashes without applying
72
- ✖ Abort
73
-
74
- # Select "Clean up" and press Enter
75
- Available stashes >
76
- > stash@{0}: WIP on main: 1a2b3c4 commit message
77
- stash@{1}: WIP on feature: 5d6e7f8 another commit
78
- ✖ Abort
79
-
80
- # Use TAB to select multiple stashes
81
- Selected stashes to delete:
82
- - stash@{0}
83
- - stash@{1}
84
-
85
- Delete these 2 stash(es)? (y/N): y
37
+ $ stashes # pick Stash, Unstash or Clean up, then continue as in that command
86
38
 
87
39
  Actions:
88
40
  💾 Stash - Create a new stash with all changes
@@ -101,6 +53,12 @@ EOF
101
53
  return 0
102
54
  fi
103
55
 
56
+ if [[ $# -gt 0 ]]; then
57
+ print_error "Unknown argument: $1"
58
+ echo "Usage: stashes [-h|--help]" >&2
59
+ return 1
60
+ fi
61
+
104
62
  # -----------------------------
105
63
  # 1. Check for fzf
106
64
  # -----------------------------
@@ -74,6 +74,11 @@ Requirements:
74
74
  EOF
75
75
  return 0
76
76
  fi
77
+ if [[ $# -gt 0 ]]; then
78
+ print_error "Unknown argument: $1"
79
+ echo "Usage: status [-h|--help]" >&2
80
+ return 1
81
+ fi
77
82
 
78
83
  require_git_repo || return 1
79
84
  require_fzf || return 1
@@ -139,6 +139,15 @@ EOF
139
139
  ;;
140
140
  esac
141
141
 
142
+ # Branch names never start with '-': anything like that is a mistyped option
143
+ local arg
144
+ for arg in "$@"; do
145
+ case "$arg" in
146
+ -h|--help) switch --help; return 0 ;;
147
+ -*) print_error "Unknown option: $arg"; echo "Usage: switch [FILTER...]" >&2; return 1 ;;
148
+ esac
149
+ done
150
+
142
151
  require_git_repo || return 1
143
152
  require_fzf || return 1
144
153
 
@@ -32,6 +32,7 @@ Apply a git stash to your working directory with optional removal.
32
32
  Interactive fuzzy finder (fzf) with preview support.
33
33
 
34
34
  Options:
35
+ -y, --yes Don't ask for confirmations (drops the stash after applying it)
35
36
  -h, --help Show this help message
36
37
 
37
38
  Workflow:
@@ -49,6 +50,19 @@ EOF
49
50
  return 0
50
51
  fi
51
52
 
53
+ while [[ $# -gt 0 ]]; do
54
+ case "$1" in
55
+ -h|--help) unstash --help; return 0 ;;
56
+ -y|--yes) export GITBASH_ASSUME_YES=1 ;;
57
+ *)
58
+ print_error "Unknown argument: $1"
59
+ echo "Usage: unstash [-y|--yes]" >&2
60
+ return 1
61
+ ;;
62
+ esac
63
+ shift
64
+ done
65
+
52
66
  require_git_repo || return 1
53
67
 
54
68
  local stash_list
@@ -57,7 +57,7 @@ EOF
57
57
  shift
58
58
  ;;
59
59
  -y|--yes)
60
- GITBASH_ASSUME_YES=1
60
+ export GITBASH_ASSUME_YES=1
61
61
  shift
62
62
  ;;
63
63
  *)
@@ -78,8 +78,8 @@ We use [Changesets](https://github.com/changesets/changesets) to manage versions
78
78
 
79
79
  ```bash
80
80
  # 1. Create your feature branch
81
- ./bin/gitbash create
82
- # Select "feature", enter "NOISSUE" or issue number, add description
81
+ ./bin/gitbash create -t
82
+ # Pick the type (feature/bugfix), enter the issue number or press Enter for NOISSUE, then a title
83
83
 
84
84
  # 2. Make your changes
85
85
  vim commands/stale.sh
@@ -98,13 +98,14 @@ npm run changeset
98
98
 
99
99
  ### What happens to changesets:
100
100
 
101
- When your PR is merged:
102
- 1. The changeset file is included in the main branch
103
- 2. When ready for release, maintainers run `npm run version`
104
- 3. Changesets are consumed and version is bumped
105
- 4. CHANGELOG.md is automatically updated
106
- 5. Changes are published with `npm run release`
107
- 6. Pushing the `vX.Y.Z` tag triggers the Release workflow, which creates the GitHub release with that version's CHANGELOG.md section as its notes (re-run it for an existing tag via "Run workflow")
101
+ Releases run only in GitHub Actions ([release.yml](../.github/workflows/release.yml)), never from a laptop:
102
+
103
+ 1. When your PR is merged, its changeset lands on `main`.
104
+ 2. The Release workflow runs CI, then opens (or updates) the **`chore: release vX.Y.Z`** pull request: `changeset version` bumps `package.json` and adds the CHANGELOG.md section. More merged changesets are added to the same pull request.
105
+ 3. On the release pull request, approve its CI run ("Approve workflows to run"): runs for pull requests opened by GitHub Actions wait for a maintainer. Merging it **stages** the version on npm with [trusted publishing](https://docs.npmjs.com/trusted-publishers) (no npm token, with provenance) and creates a draft GitHub release (no tag yet, not visible to `gitbash --update`).
106
+ 4. A maintainer reviews and approves the staged version on npmjs.com or with `npm stage approve` ([staged publishing](https://docs.npmjs.com/staged-publishing/), needs 2FA; the workflow cannot approve).
107
+ 5. Within 15 minutes a scheduled run sees the version live on npm and publishes the GitHub release, which creates the `vX.Y.Z` tag. To do it right away: `gh workflow run release.yml`.
108
+ 6. If a run fails, re-run it: finished steps are skipped. To re-stage a rejected version, delete its draft release and re-run.
108
109
 
109
110
  ## Development Guidelines
110
111
 
package/docs/SECURITY.md CHANGED
@@ -6,10 +6,10 @@ The following versions of this project are currently receiving security updates:
6
6
 
7
7
  | Version | Supported |
8
8
  |--------|-----------|
9
- | 2.x.x | ✅ |
10
- | < 2.0 | ❌ |
9
+ | 3.x.x | ✅ |
10
+ | < 3.0 | ❌ |
11
11
 
12
- Security fixes are only applied to the latest minor/patch release of the **2.x.x** series. Older versions must be upgraded to receive patches.
12
+ Security fixes are only applied to the latest minor/patch release of the **3.x.x** series. Older versions must be upgraded to receive patches.
13
13
 
14
14
  Versions before 2.0 executed repository `.gitbashrc` files as shell code, so opening a cloned repository with a malicious `.gitbashrc` could run arbitrary commands. Upgrade to 2.0 or later.
15
15
 
@@ -19,11 +19,21 @@ Versions before 2.0 executed repository `.gitbashrc` files as shell code, so ope
19
19
 
20
20
  - Configuration files (`~/.gitbashrc`, a repository's `.gitbashrc` and `.gitbashrc-user`) are **parsed, never executed**. Only plain `GITBASH_*="value"` lines from a fixed list of settings are read; values containing `$`, backticks, backslashes or quotes are rejected.
21
21
  - A repository's committed `.gitbashrc` is treated as untrusted: it cannot set `GITBASH_MERGE_COMMAND` (the only setting that names a program to run). That setting is only read from `~/.gitbashrc` and `.gitbashrc-user`.
22
+ - Settings that reach git as arguments are validated: `GITBASH_BASE_BRANCH` (and a base branch taken from the remote's `HEAD`) must be a valid branch name that does not start with `-`, and `GITBASH_REMOTE` must not start with `-`. Before 3.0.0, a committed `.gitbashrc` could set `GITBASH_BASE_BRANCH="--output=<file>"` and make `switch` or `cleanup` overwrite that file.
22
23
  - `gitbash --init` prints wrapper functions that call the installed `gitbash` binary; command code is not sourced into your shell.
23
24
  - `pr` and `repo` never pass credentials from the remote URL to the browser.
24
25
 
25
26
  ---
26
27
 
28
+ ## Releases
29
+
30
+ - Releases are staged only by the Release workflow in GitHub Actions, with npm trusted publishing (OIDC, no long-lived npm token) and [provenance](https://docs.npmjs.com/generating-provenance-statements). A version goes live only after a maintainer approves it with 2FA ([staged publishing](https://docs.npmjs.com/staged-publishing/)); neither the workflow nor a leaked GitHub token can publish on its own. Check an installed copy with `npm audit signatures`.
31
+ - The install script (`install.sh`, also used by `gitbash --update` for script installs) downloads that same npm package and installs it only if it matches the sha512 checksum npm records for the version, so a moved git tag cannot change what gets installed.
32
+ - The npm package is published before the GitHub release, which `gitbash --update` and the install script use.
33
+ - Third-party actions are pinned to commit SHAs and kept up to date by Dependabot; each workflow job gets only the permissions it needs.
34
+
35
+ ---
36
+
27
37
  ## Reporting a Vulnerability
28
38
 
29
39
  We take security vulnerabilities seriously and appreciate your efforts to responsibly disclose them.
package/docs/workflows.md CHANGED
@@ -10,7 +10,7 @@ Gitbash reduces repetitive typing and provides interactive previews and safe cle
10
10
  | Checkout base branch | `switch main` | `git switch main` |
11
11
  | Create feature branch (from latest main) | `create PROJ-123 add login` | `git switch -c feature/PROJ-123-add-login main`<br>`git push -u origin feature/PROJ-123-add-login` |
12
12
  | Create a stash (WIP) | `stash "WIP PROJ-123 implement login"` | `git stash push -u -m "WIP PROJ-123 implement login"` |
13
- | Create + switch to another branch (ensure main up-to-date) | `create OTHER-456 fix typo` | `git switch main`<br>`git pull origin main`<br>`git switch -c bugfix/OTHER-456-fix-typo main`<br>`git push -u origin bugfix/OTHER-456-fix-typo` |
13
+ | Create + switch to another branch (ensure main up-to-date) | `create --bugfix OTHER-456 fix typo` | `git switch main`<br>`git pull origin main`<br>`git switch -c bugfix/OTHER-456-fix-typo main`<br>`git push -u origin bugfix/OTHER-456-fix-typo` |
14
14
  | Implement changes on other branch, commit & push | `commit -p "fix(OTHER-456): fix typo"` | `git add -A`<br>`git commit -m "fix(OTHER-456): fix typo"`<br>`git push -u origin bugfix/OTHER-456-fix-typo` |
15
15
  | Switch back to original Jira branch | `switch PROJ-123` | `git switch feature/PROJ-123-add-login` |
16
16
  | Verify main was up-to-date at creation time<br>Update feature branch by merging current `main` into it | `update` | `git fetch origin`<br>`git log --oneline HEAD..origin/main`<br>`git pull origin main` (run while on feature branch) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitbash",
3
- "version": "2.2.2",
3
+ "version": "3.0.1",
4
4
  "description": "Opinionated git utilities for bash with fzf-powered menus",
5
5
  "author": "jaggli",
6
6
  "license": "MIT",
@@ -38,14 +38,17 @@
38
38
  "linux",
39
39
  "win32"
40
40
  ],
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "provenance": true
44
+ },
41
45
  "engines": {
42
46
  "node": ">=14"
43
47
  },
44
48
  "scripts": {
45
49
  "test": "bats test",
46
50
  "changeset": "changeset",
47
- "version": "changeset version",
48
- "release": "scripts/release.sh"
51
+ "version": "changeset version"
49
52
  },
50
53
  "devDependencies": {
51
54
  "@changesets/cli": "^2.29.8",