gitbash 2.0.1 → 2.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/README.md CHANGED
@@ -12,11 +12,35 @@ Pure bash, zero-dependency git utilities. Reduce repetitive typing and superchar
12
12
  npm i -g gitbash && gitbash --config
13
13
  ```
14
14
 
15
+ ### Via install script (no npm or node needed)
16
+
17
+ ```bash
18
+ # curl
19
+ curl -fsSL https://raw.githubusercontent.com/jaggli/gitbash/main/install.sh | sh
20
+
21
+ # wget
22
+ wget -qO- https://raw.githubusercontent.com/jaggli/gitbash/main/install.sh | sh
23
+ ```
24
+
25
+ This downloads the latest GitHub release to `~/.local/share/gitbash` and links `~/.local/bin/gitbash`. Run `gitbash --update` (or the script again) to upgrade. Options are set as environment variables:
26
+
27
+ ```bash
28
+ # Install a specific version
29
+ curl -fsSL https://raw.githubusercontent.com/jaggli/gitbash/main/install.sh | GITBASH_VERSION=2.0.1 sh
30
+
31
+ # Custom locations (e.g. system-wide)
32
+ curl -fsSL https://raw.githubusercontent.com/jaggli/gitbash/main/install.sh \
33
+ | sudo GITBASH_INSTALL_DIR=/usr/local/share/gitbash GITBASH_BIN_DIR=/usr/local/bin sh
34
+ ```
35
+
36
+ To uninstall, remove `~/.local/share/gitbash` and `~/.local/bin/gitbash`.
37
+
15
38
  ### Options
16
39
 
17
40
  ```bash
18
41
  gitbash --help # Show help
19
42
  gitbash --version # Show version
43
+ gitbash --update # Update to the latest release (npm or install script)
20
44
  gitbash --init # Print shell functions (see Shell integration)
21
45
  gitbash --config # Interactive configuration wizard
22
46
  gitbash --config-local # Configure overrides for the current repository (committed)
@@ -46,18 +70,6 @@ eval "$(gitbash --init --prefix=gb-)" # gb-commit, gb-switch, ... (avoids shadow
46
70
 
47
71
  This defines small wrapper functions; every command still runs in its own bash process, so nothing else is added to your shell. Individual aliases work too, e.g. `alias commit="gitbash commit"`.
48
72
 
49
- ### Upgrading to 2.0
50
-
51
- 2.0 is a security and safety release. What changes for you:
52
-
53
- - **Restart your shell** after upgrading, so `eval "$(gitbash --init)"` picks up the new wrapper functions.
54
- - Config files are **read, not executed**: only plain `GITBASH_*="value"` lines are used, other lines are ignored with a warning. `GITBASH_MERGE_COMMAND` is ignored in a committed `.gitbashrc`.
55
- - `status` no longer commits and pushes after staging. Use `Ctrl-O` to commit.
56
- - `stale` toggles all/stale branches with `Ctrl-T` (was `Ctrl-A`).
57
- - Protected branches (base branch and `GITBASH_PROTECTED_BRANCHES`) are never offered for deletion. `cleanup` and `switch` delete with `git branch -d` and ask again before force-deleting unmerged work.
58
- - When your branch and the remote diverged, `commit -p`, `pr -p` and `update -p` ask whether to rebase, merge or abort instead of rebasing silently.
59
- - fzf 0.36 or newer is required for the interactive menus.
60
-
61
73
  ## Commands
62
74
 
63
75
  ### branch
@@ -115,7 +127,7 @@ create enhance login screen # → feature/enhance-login-screen (pars
115
127
  switch [FILTER...]
116
128
  ```
117
129
 
118
- Switch branches with fzf. Shows local branches (`merged:` = fully merged into the base branch), then remote branches without a local copy. Preview shows commit history. If exactly one branch name matches the filter, switches directly. After switching, offers to fast-forward a branch that is behind.
130
+ Switch branches with fzf. Shows local branches (`merged:` = fully merged into the base branch), then remote branches without a local copy. Preview shows commit history. Switches directly when the filter is an exact branch name or matches exactly one branch name. After switching, offers to fast-forward a branch that is behind.
119
131
 
120
132
  - `Del` deletes the selected local branch (asks first, default no; unmerged commits need a second confirmation)
121
133
 
@@ -162,6 +174,22 @@ pr [-p|--push] [--print]
162
174
 
163
175
  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.
164
176
 
177
+ ### repo
178
+
179
+ ```bash
180
+ repo [--print]
181
+ ```
182
+
183
+ Open the repository's web page in the browser: via the GitHub CLI (`gh repo view --web`) when available, otherwise the URL derived from the remote (GitHub, GitHub Enterprise, GitLab, Bitbucket, Azure DevOps). `--print` prints the URL.
184
+
185
+ ### reset-repo
186
+
187
+ ```bash
188
+ reset-repo [-n|--dry-run] [-y|--yes]
189
+ ```
190
+
191
+ Reset the current branch like a fresh clone: fetch, `git reset --hard <remote>/<branch>` and delete all untracked and ignored files (`git clean -dx`). Lists the unpushed commits, local changes and files it removes and asks first. Aborts an unfinished merge or rebase. Files matching `GITBASH_RESET_KEEP` (e.g. `.env`), `.gitbashrc-user` and nested repositories are kept; stashes and other branches are not touched.
192
+
165
193
  ### update
166
194
 
167
195
  ```bash
@@ -238,14 +266,15 @@ Unpushed commits are shown per branch. Deletes with `git branch -d`; force-delet
238
266
  ### commits
239
267
 
240
268
  ```bash
241
- commits [COUNT]
269
+ commits [-a|--all] [COUNT]
242
270
  ```
243
271
 
244
- List recent commits with option to revert. Multi-select with TAB; selected commits are reverted newest first.
272
+ 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.
245
273
 
246
274
  ```bash
247
- commits # Show last 20 commits
248
- commits 50 # Show last 50 commits
275
+ commits # Show the branch's last 20 commits
276
+ commits 50 # Show up to 50
277
+ commits --all # All recent commits, like git log
249
278
  ```
250
279
 
251
280
  ## Configuration
@@ -278,11 +307,17 @@ GITBASH_CLEANUP_DAYS="7"
278
307
  # Branches never offered for deletion, space-separated globs
279
308
  GITBASH_PROTECTED_BRANCHES="main master develop release/*"
280
309
 
310
+ # Files 'reset-repo' keeps, space-separated .gitignore patterns, e.g. ".env .idea/" (default: "")
311
+ GITBASH_RESET_KEEP=""
312
+
281
313
  # Base branch (default: detected from origin/HEAD, then main, then master)
282
314
  GITBASH_BASE_BRANCH=""
283
315
 
284
316
  # Remote name (default: "origin")
285
317
  GITBASH_REMOTE="origin"
318
+
319
+ # Turn off the automatic update checks: yes or no (default: "no")
320
+ GITBASH_NO_UPDATE_CHECKS="no"
286
321
  ```
287
322
 
288
323
  Config files are **read, never executed**. Only `GITBASH_*="value"` lines are used (double, single or no quotes, optional `export`, `# comments`). Values cannot contain `$`, backticks, backslashes or quotes; anything else is ignored with a warning.
@@ -301,6 +336,18 @@ Run `gitbash --config-user` to create `.gitbashrc-user` for personal overrides i
301
336
  2. `.gitbashrc` - Repository settings (committed)
302
337
  3. `~/.gitbashrc` - Global settings
303
338
 
339
+ #### Updates
340
+
341
+ `gitbash --update` installs the latest release the same way gitbash was installed: with `npm install -g` (or `pnpm add -g`) for a global package, or by running the release's install script again for the same locations.
342
+
343
+ gitbash also checks for new releases on its own, at most once a day, in the background while a command runs, so commands never wait for the network. When a new version is known, the next command at a terminal asks (at most every other day) whether to update now, not now, or skip that version for good. It never asks in scripts or pipes.
344
+
345
+ To turn the checks off, answer "no" in `gitbash --config` or `--config-user`, set `GITBASH_NO_UPDATE_CHECKS="yes"` in any config file, or export `GITBASH_NO_UPDATE_CHECKS=1`. They are also off in CI (`CI` and similar variables) and when running from a git checkout. The check state is kept in `~/.local/state/gitbash/update-check` (`$XDG_STATE_HOME` if set).
346
+
347
+ #### Colors
348
+
349
+ gitbash supports [`NO_COLOR`](https://no-color.org): set it to any non-empty value (e.g. `export NO_COLOR=1`) to turn off all colors, including the help, messages, the git output in previews and `bat`. `delta` is not used then, and fzf uses its black-and-white theme. Colors are also left out when the output is not a terminal.
350
+
304
351
  ## License
305
352
 
306
353
  See [LICENSE](LICENSE) file for details.
package/bin/gitbash CHANGED
@@ -10,12 +10,20 @@ while [[ -L "$SOURCE" ]]; do
10
10
  done
11
11
  SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
12
12
  GITBASH_BIN="$SCRIPT_DIR/bin/gitbash"
13
+ # pnpm keeps each version in its own folder (node_modules/.pnpm/gitbash@X/node_modules/gitbash):
14
+ # run gitbash through the node_modules/gitbash link instead, which follows updates
15
+ if [[ "$SCRIPT_DIR" == */node_modules/.pnpm/gitbash@*/node_modules/gitbash &&
16
+ -x "${SCRIPT_DIR%%/.pnpm/*}/gitbash/bin/gitbash" ]]; then
17
+ GITBASH_BIN="${SCRIPT_DIR%%/.pnpm/*}/gitbash/bin/gitbash"
18
+ fi
13
19
  COMMANDS_DIR="$SCRIPT_DIR/commands"
14
20
  VERSION="$(grep -o '"version": *"[^"]*"' "$SCRIPT_DIR/package.json" | head -1 | cut -d'"' -f4)"
15
21
  export SCRIPT_DIR GITBASH_BIN VERSION
16
22
 
17
23
  # shellcheck source=../commands/_utils.sh
18
24
  source "$COMMANDS_DIR/_utils.sh"
25
+ # shellcheck source=../commands/_self_update.sh
26
+ source "$COMMANDS_DIR/_self_update.sh"
19
27
 
20
28
  # Available commands
21
29
  COMMANDS=(
@@ -26,6 +34,8 @@ COMMANDS=(
26
34
  "commits"
27
35
  "create"
28
36
  "pr"
37
+ "repo"
38
+ "reset-repo"
29
39
  "stale"
30
40
  "stash"
31
41
  "stashes"
@@ -50,7 +60,7 @@ is_command() {
50
60
  # key|type|question
51
61
  SETTINGS=(
52
62
  "GITBASH_CREATE_BRANCH_PREFIX|prefix|Branch prefix inserted between type and branch name (e.g. 'team' → feature/team/PROJ-123-fix-bug)"
53
- "GITBASH_CREATE_NO_ISSUE_PARSING|parsing|Parse Jira issue numbers into branch names? (yes/no)"
63
+ "GITBASH_CREATE_NO_ISSUE_PARSING|inverted|Parse Jira issue numbers into branch names? (yes/no)"
54
64
  "GITBASH_CREATE_ISSUE_PARSING_FALLBACK|text|Fallback issue prefix when no issue number is given"
55
65
  "GITBASH_CREATE_AUTO_PUSH|yesno|Push new branches to the remote right after 'create'? (yes/no)"
56
66
  "GITBASH_MERGE_COMMAND|command|Merge tool opened by 'update' on conflicts (not allowed in the committed .gitbashrc)"
@@ -58,8 +68,10 @@ SETTINGS=(
58
68
  "GITBASH_STALE_MONTHS|number|Stale remote branch threshold in months (used by 'stale')"
59
69
  "GITBASH_CLEANUP_DAYS|number|Stale local branch threshold in days (used by 'cleanup')"
60
70
  "GITBASH_PROTECTED_BRANCHES|text|Branches never offered for deletion (space-separated globs)"
71
+ "GITBASH_RESET_KEEP|text|Files kept by 'reset-repo' (space-separated .gitignore patterns, e.g. '.env .idea/')"
61
72
  "GITBASH_BASE_BRANCH|text|Base branch (empty = detect from the remote's HEAD, then main/master)"
62
73
  "GITBASH_REMOTE|text|Remote name"
74
+ "GITBASH_NO_UPDATE_CHECKS|inverted|Check for gitbash updates now and then, and offer to install them? (yes/no)"
63
75
  )
64
76
 
65
77
  # Default value of a setting
@@ -74,6 +86,7 @@ setting_default() {
74
86
  GITBASH_CLEANUP_DAYS) echo "7" ;;
75
87
  GITBASH_PROTECTED_BRANCHES) echo "main master develop release/*" ;;
76
88
  GITBASH_REMOTE) echo "origin" ;;
89
+ GITBASH_NO_UPDATE_CHECKS) echo "no" ;;
77
90
  *) echo "" ;;
78
91
  esac
79
92
  }
@@ -91,10 +104,10 @@ file_value() {
91
104
  }
92
105
 
93
106
  # Convert between the stored value and what the wizard shows.
94
- # GITBASH_CREATE_NO_ISSUE_PARSING is stored inverted.
107
+ # GITBASH_CREATE_NO_ISSUE_PARSING and GITBASH_NO_UPDATE_CHECKS are stored inverted.
95
108
  to_display() {
96
109
  local type="$1" value="$2"
97
- if [[ "$type" == "parsing" ]]; then
110
+ if [[ "$type" == "inverted" ]]; then
98
111
  [[ "$value" == "yes" ]] && echo "no" || echo "yes"
99
112
  else
100
113
  printf '%s' "$value"
@@ -109,7 +122,7 @@ to_stored() {
109
122
  return 1
110
123
  fi
111
124
  case "$type" in
112
- parsing)
125
+ inverted)
113
126
  case "$answer" in
114
127
  yes) echo "no" ;;
115
128
  no) echo "yes" ;;
@@ -183,7 +196,9 @@ configure_wizard() {
183
196
  for entry in "${SETTINGS[@]}"; do
184
197
  n=$((n + 1))
185
198
  IFS='|' read -r key type question <<< "$entry"
186
- [[ "$scope" == "local" && "$key" == "GITBASH_MERGE_COMMAND" ]] && continue
199
+ if [[ "$scope" == "local" ]]; then
200
+ [[ "$key" == "GITBASH_MERGE_COMMAND" || "$key" == "GITBASH_NO_UPDATE_CHECKS" ]] && continue
201
+ fi
187
202
 
188
203
  default=$(setting_default "$key")
189
204
  has_override=false
@@ -356,13 +371,37 @@ check_shell_integration() {
356
371
  # Help and shell integration
357
372
  # =============================================================================
358
373
 
359
- show_help() {
360
- cat << EOF
361
- gitbash $VERSION - Interactive git utilities for bash
374
+ # ASCII art logo with a color gradient, followed by the version.
375
+ # Plain text when stdout is not a terminal or NO_COLOR is set.
376
+ show_banner() {
377
+ local -a art=(
378
+ ' _ _ _ _ '
379
+ ' __ _(_) |_| |__ __ _ ___| |__ '
380
+ ' / _` | | __| '"'"'_ \ / _` / __| '"'"'_ \ '
381
+ ' | (_| | | |_| |_) | (_| \__ \ | | |'
382
+ ' \__, |_|\__|_.__/ \__,_|___/_| |_|'
383
+ ' |___/ '
384
+ )
385
+ local -a colors=(214 209 204 199 170 135)
386
+ local i
387
+ if _gb_color_ok 1; then
388
+ for i in "${!art[@]}"; do
389
+ printf '\033[1;38;5;%sm%s\033[0m\n' "${colors[$i]}" "${art[$i]}"
390
+ done
391
+ printf ' \033[1;38;5;135mv%s\033[0m \033[2m·\033[0m Interactive git utilities for bash\n' "$VERSION"
392
+ else
393
+ printf '%s\n' "${art[@]}"
394
+ printf ' v%s - Interactive git utilities for bash\n' "$VERSION"
395
+ fi
396
+ }
362
397
 
398
+ show_help() {
399
+ show_banner
400
+ echo
401
+ gb_help << EOF
363
402
  Usage:
364
403
  gitbash <command> [args...]
365
- gitbash --help | --version
404
+ gitbash --help | --version | --update
366
405
  gitbash --init [--prefix=PREFIX]
367
406
  gitbash --config | --config-local | --config-user
368
407
 
@@ -371,9 +410,11 @@ Commands:
371
410
  cleanstash Delete stashes (multi-select)
372
411
  cleanup Find and delete leftover local branches
373
412
  commit Commit with optional push
374
- commits List recent commits with revert option
413
+ commits List the branch's commits with revert option
375
414
  create Create feature branch with Jira parsing
376
415
  pr Open PR in browser
416
+ repo Open the repository in browser
417
+ reset-repo Reset the branch like a fresh clone
377
418
  stale List stale remote branches
378
419
  stash Create named stash
379
420
  stashes Interactive stash menu
@@ -385,6 +426,7 @@ Commands:
385
426
  Options:
386
427
  -h, --help Show this help message
387
428
  -v, --version Show version number
429
+ --update Update gitbash to the latest release
388
430
  --init Print shell functions for calling commands directly
389
431
  (--prefix=gb- creates gb-commit, gb-switch, ...)
390
432
  --config Configure gitbash and save to ~/.gitbashrc
@@ -445,6 +487,9 @@ main() {
445
487
  -v|--version)
446
488
  echo "gitbash $VERSION"
447
489
  ;;
490
+ --update)
491
+ gb_self_update
492
+ ;;
448
493
  --init)
449
494
  shift
450
495
  show_init "$@"
@@ -472,6 +517,9 @@ main() {
472
517
  return 1
473
518
  fi
474
519
  gb_load_config
520
+ gb_update_check "$cmd" "$@"
521
+ # Commands run gitbash again (e.g. pr runs commit): no update prompt in there
522
+ export GITBASH_NESTED=1
475
523
  # shellcheck source=/dev/null
476
524
  source "$COMMANDS_DIR/$cmd.sh"
477
525
  "$cmd" "$@"
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env bash
2
+ # Self-update for gitbash: 'gitbash --update' and the background update check.
3
+ # Sourced by bin/gitbash; needs SCRIPT_DIR, GITBASH_BIN and VERSION.
4
+
5
+ GB_REPO="jaggli/gitbash"
6
+ GB_UPDATE_CHECK_INTERVAL=86400 # look for a new version at most once a day
7
+ GB_UPDATE_PROMPT_INTERVAL=172800 # ask about it at most every other day
8
+
9
+ # GITBASH_NO_UPDATE_CHECKS from the environment wins over the config files
10
+ _GB_ENV_NO_UPDATE_CHECKS="${GITBASH_NO_UPDATE_CHECKS:-}"
11
+
12
+ # =============================================================================
13
+ # Install method
14
+ # =============================================================================
15
+
16
+ # Directory of a global package manager install of gitbash, symlinks resolved
17
+ _gb_global_package_dir() {
18
+ local root
19
+ root=$("$1" root -g 2>/dev/null) || return 1
20
+ [[ -n "$root" ]] || return 1
21
+ (cd -P "$root/gitbash" 2>/dev/null && pwd)
22
+ }
23
+
24
+ # How this copy of gitbash was installed: npm, pnpm, npx, package, script, git or unknown
25
+ gb_install_method() {
26
+ case "$SCRIPT_DIR" in
27
+ */_npx/*)
28
+ echo "npx"
29
+ return 0
30
+ ;;
31
+ */node_modules/gitbash)
32
+ local pm
33
+ for pm in npm pnpm; do
34
+ if command -v "$pm" >/dev/null 2>&1 &&
35
+ [[ "$(_gb_global_package_dir "$pm")" == "$SCRIPT_DIR" ]]; then
36
+ echo "$pm"
37
+ return 0
38
+ fi
39
+ done
40
+ # A dependency of some project, or a package manager we don't know
41
+ echo "package"
42
+ return 0
43
+ ;;
44
+ esac
45
+ if [[ -f "$SCRIPT_DIR/.gitbash-install" ]]; then
46
+ echo "script"
47
+ elif [[ -e "$SCRIPT_DIR/.git" ]]; then
48
+ echo "git"
49
+ elif [[ -f "$SCRIPT_DIR/package.json" && ! -e "$SCRIPT_DIR/README.md" ]]; then
50
+ # Installed by an install.sh that did not leave a marker yet
51
+ echo "script"
52
+ else
53
+ echo "unknown"
54
+ fi
55
+ }
56
+
57
+ # =============================================================================
58
+ # State (~/.local/state/gitbash/update-check)
59
+ # =============================================================================
60
+
61
+ _gb_update_state_file() {
62
+ echo "${XDG_STATE_HOME:-$HOME/.local/state}/gitbash/update-check"
63
+ }
64
+
65
+ # Read the state into _GB_UC_CHECKED, _GB_UC_LATEST, _GB_UC_PROMPTED and _GB_UC_SKIPPED
66
+ _gb_update_state_load() {
67
+ local file key value
68
+ file=$(_gb_update_state_file)
69
+ _GB_UC_CHECKED=0 _GB_UC_LATEST="" _GB_UC_PROMPTED=0 _GB_UC_SKIPPED=""
70
+ [[ -f "$file" ]] || return 0
71
+ while IFS='=' read -r key value; do
72
+ case "$key" in
73
+ checked) [[ "$value" =~ ^[0-9]+$ ]] && _GB_UC_CHECKED="$value" ;;
74
+ latest) _GB_UC_LATEST="$value" ;;
75
+ prompted) [[ "$value" =~ ^[0-9]+$ ]] && _GB_UC_PROMPTED="$value" ;;
76
+ skipped) _GB_UC_SKIPPED="$value" ;;
77
+ esac
78
+ done < "$file"
79
+ }
80
+
81
+ # Set one value, keeping the others (re-read first: a background check may have written)
82
+ # Usage: _gb_update_state_set checked|latest|prompted|skipped <value>
83
+ _gb_update_state_set() {
84
+ local file dir tmp
85
+ file=$(_gb_update_state_file)
86
+ dir=$(dirname "$file")
87
+ _gb_update_state_load
88
+ case "$1" in
89
+ checked) _GB_UC_CHECKED="$2" ;;
90
+ latest) _GB_UC_LATEST="$2" ;;
91
+ prompted) _GB_UC_PROMPTED="$2" ;;
92
+ skipped) _GB_UC_SKIPPED="$2" ;;
93
+ esac
94
+ mkdir -p "$dir" 2>/dev/null || return 1
95
+ tmp=$(mktemp "$dir/.update-check.XXXXXX" 2>/dev/null) || return 1
96
+ if ! printf 'checked=%s\nlatest=%s\nprompted=%s\nskipped=%s\n' \
97
+ "$_GB_UC_CHECKED" "$_GB_UC_LATEST" "$_GB_UC_PROMPTED" "$_GB_UC_SKIPPED" > "$tmp" ||
98
+ ! mv -f "$tmp" "$file"; then
99
+ rm -f "$tmp"
100
+ return 1
101
+ fi
102
+ }
103
+
104
+ _gb_now() {
105
+ if [[ -n "${EPOCHSECONDS:-}" ]]; then
106
+ echo "$EPOCHSECONDS"
107
+ else
108
+ date +%s
109
+ fi
110
+ }
111
+
112
+ # =============================================================================
113
+ # Versions
114
+ # =============================================================================
115
+
116
+ # Print the contents of a URL
117
+ _gb_fetch() {
118
+ if command -v curl >/dev/null 2>&1; then
119
+ curl -fsSL --max-time 10 "$1"
120
+ elif command -v wget >/dev/null 2>&1; then
121
+ wget -qO- -T 10 "$1"
122
+ else
123
+ return 1
124
+ fi
125
+ }
126
+
127
+ # Print the version of the latest GitHub release, e.g. 2.1.0
128
+ gb_latest_version() {
129
+ local json tag
130
+ json=$(_gb_fetch "https://api.github.com/repos/$GB_REPO/releases/latest" 2>/dev/null) || return 1
131
+ tag=$(printf '%s\n' "$json" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1)
132
+ tag="${tag#v}"
133
+ [[ "$tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
134
+ echo "$tag"
135
+ }
136
+
137
+ # Returns 0 if version $1 is newer than the running one
138
+ _gb_is_newer() {
139
+ [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && ! _gb_version_ge "$VERSION" "$1"
140
+ }
141
+
142
+ # =============================================================================
143
+ # Update
144
+ # =============================================================================
145
+
146
+ # Install version $1 the way this copy was installed
147
+ gb_self_update_to() {
148
+ local latest="$1" method
149
+ method=$(gb_install_method)
150
+ case "$method" in
151
+ npm|pnpm)
152
+ local -a cmd=(npm install -g "gitbash@$latest")
153
+ [[ "$method" == "pnpm" ]] && cmd=(pnpm add -g "gitbash@$latest")
154
+ print_info "Running: ${cmd[*]}"
155
+ if ! "${cmd[@]}"; then
156
+ print_error "Update failed. Run '${cmd[*]}' yourself (it may need more permissions)."
157
+ return 1
158
+ fi
159
+ ;;
160
+ script)
161
+ _gb_self_update_script "$latest" || return 1
162
+ ;;
163
+ npx)
164
+ print_info "gitbash runs through npx: use 'npx gitbash@latest' to get the newest version."
165
+ return 1
166
+ ;;
167
+ git)
168
+ print_info "gitbash runs from a git checkout ($SCRIPT_DIR): update it with 'git pull'."
169
+ return 1
170
+ ;;
171
+ package)
172
+ print_info "gitbash is a dependency in $SCRIPT_DIR: update it with your package manager."
173
+ return 1
174
+ ;;
175
+ *)
176
+ print_error "Could not detect how gitbash was installed ($SCRIPT_DIR)."
177
+ echo "Reinstall it with 'npm i -g gitbash' or the install script (see the README)." >&2
178
+ return 1
179
+ ;;
180
+ esac
181
+
182
+ # GITBASH_BIN, not SCRIPT_DIR: pnpm installs the new version in a new folder
183
+ local installed pkg_dir="${GITBASH_BIN%/bin/gitbash}"
184
+ installed=$(grep -o '"version": *"[^"]*"' "$pkg_dir/package.json" 2>/dev/null | head -1 | cut -d'"' -f4)
185
+ if [[ "$installed" != "$latest" ]]; then
186
+ print_error "Update finished, but gitbash in $pkg_dir is still at ${installed:-an unknown version}."
187
+ return 1
188
+ fi
189
+ print_success "Updated gitbash $VERSION → $latest"
190
+ }
191
+
192
+ # Re-run the install script of the new release for the same locations
193
+ _gb_self_update_script() {
194
+ local latest="$1" bin_dir="" key value tmp
195
+ if [[ -f "$SCRIPT_DIR/.gitbash-install" ]]; then
196
+ while IFS='=' read -r key value; do
197
+ [[ "$key" == "bin_dir" ]] && bin_dir="$value"
198
+ done < "$SCRIPT_DIR/.gitbash-install"
199
+ fi
200
+ bin_dir="${bin_dir:-$HOME/.local/bin}"
201
+
202
+ local -a vars=(GITBASH_VERSION="$latest" GITBASH_INSTALL_DIR="$SCRIPT_DIR" GITBASH_BIN_DIR="$bin_dir")
203
+ local hint="curl -fsSL https://raw.githubusercontent.com/$GB_REPO/main/install.sh | sudo ${vars[*]} sh"
204
+ if [[ ! -w "$(dirname "$SCRIPT_DIR")" ]]; then
205
+ print_error "No permission to write to $(dirname "$SCRIPT_DIR"). Update with:"
206
+ echo " $hint" >&2
207
+ return 1
208
+ fi
209
+
210
+ tmp=$(mktemp 2>/dev/null || mktemp -t gitbash) || return 1
211
+ if ! _gb_fetch "https://raw.githubusercontent.com/$GB_REPO/v$latest/install.sh" > "$tmp" 2>/dev/null ||
212
+ [[ ! -s "$tmp" ]]; then
213
+ rm -f "$tmp"
214
+ print_error "Could not download the install script of gitbash $latest."
215
+ return 1
216
+ fi
217
+ print_info "Running the install script of gitbash $latest"
218
+ if ! env "${vars[@]}" sh "$tmp"; then
219
+ rm -f "$tmp"
220
+ print_error "Update failed."
221
+ return 1
222
+ fi
223
+ rm -f "$tmp"
224
+ }
225
+
226
+ # gitbash --update
227
+ gb_self_update() {
228
+ local latest
229
+ print_info "Checking for updates..."
230
+ if ! latest=$(gb_latest_version); then
231
+ print_error "Could not check for updates (no network, or GitHub is not reachable)."
232
+ return 1
233
+ fi
234
+ _gb_update_state_set checked "$(_gb_now)"
235
+ _gb_update_state_set latest "$latest"
236
+ if ! _gb_is_newer "$latest"; then
237
+ print_success "gitbash $VERSION is up to date."
238
+ return 0
239
+ fi
240
+ gb_self_update_to "$latest"
241
+ }
242
+
243
+ # =============================================================================
244
+ # Background check
245
+ # =============================================================================
246
+
247
+ # Returns 0 if update checks are on: not turned off, not in CI, not a git checkout
248
+ gb_update_checks_enabled() {
249
+ case "$_GB_ENV_NO_UPDATE_CHECKS" in
250
+ "") [[ "${GITBASH_NO_UPDATE_CHECKS:-no}" == "yes" ]] && return 1 ;;
251
+ no|0|false) ;;
252
+ *) return 1 ;;
253
+ esac
254
+ local var
255
+ for var in CI CONTINUOUS_INTEGRATION BUILD_NUMBER GITHUB_ACTIONS GITLAB_CI TF_BUILD \
256
+ JENKINS_URL BUILDKITE CIRCLECI TRAVIS TEAMCITY_VERSION BITBUCKET_BUILD_NUMBER; do
257
+ case "${!var:-}" in
258
+ ""|false|0) ;;
259
+ *) return 1 ;;
260
+ esac
261
+ done
262
+ [[ ! -e "$SCRIPT_DIR/.git" ]]
263
+ }
264
+
265
+ # Run before a command: start a background check when one is due, and offer a
266
+ # known new version (at most every other day, never again for a skipped one).
267
+ # On a successful update, runs the command with the new version instead.
268
+ # Usage: gb_update_check <command> [args...]
269
+ gb_update_check() {
270
+ # Not when gitbash runs itself from a command (the parent already checked)
271
+ [[ -z "${GITBASH_NESTED:-}" ]] || return 0
272
+ gb_update_checks_enabled || return 0
273
+
274
+ local now
275
+ now=$(_gb_now)
276
+ _gb_update_state_load
277
+ if (( now - _GB_UC_CHECKED >= GB_UPDATE_CHECK_INTERVAL || _GB_UC_CHECKED > now )); then
278
+ _gb_update_state_set checked "$now"
279
+ (
280
+ local latest
281
+ latest=$(gb_latest_version) && _gb_update_state_set latest "$latest"
282
+ ) < /dev/null > /dev/null 2>&1 &
283
+ fi
284
+
285
+ local latest="$_GB_UC_LATEST"
286
+ _gb_is_newer "$latest" || return 0
287
+ [[ "$latest" != "$_GB_UC_SKIPPED" ]] || return 0
288
+ (( now - _GB_UC_PROMPTED >= GB_UPDATE_PROMPT_INTERVAL || _GB_UC_PROMPTED > now )) || return 0
289
+ # Only ask people at a terminal, never scripts
290
+ [[ -t 0 && -t 1 && -t 2 && "${GITBASH_ASSUME_YES:-}" != "1" ]] || return 0
291
+ local arg
292
+ for arg in "${@:2}"; do
293
+ [[ "$arg" == "-y" || "$arg" == "--yes" ]] && return 0
294
+ done
295
+
296
+ _gb_update_state_set prompted "$now"
297
+ print_info "gitbash $latest is available (you have $VERSION)."
298
+ local choice
299
+ gb_choice choice "Update now? [y]es, [n]ot now, [s]kip this version (y/N/s):" "yns" "n"
300
+ case "$choice" in
301
+ y)
302
+ if gb_self_update_to "$latest"; then
303
+ echo
304
+ exec "$GITBASH_BIN" "$@"
305
+ fi
306
+ echo
307
+ ;;
308
+ s)
309
+ _gb_update_state_set skipped "$latest"
310
+ print_info "You won't be asked about $latest again. Run 'gitbash --update' to update any time."
311
+ echo
312
+ ;;
313
+ *)
314
+ print_info "Run 'gitbash --update' to update any time."
315
+ echo
316
+ ;;
317
+ esac
318
+ return 0
319
+ }