gitbash 2.0.1 → 2.2.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.
@@ -0,0 +1,323 @@
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
+ # Windows: C:\Users\...\npm\node_modules
22
+ if gb_is_windows && command -v cygpath >/dev/null 2>&1; then
23
+ root=$(cygpath -u "$root") || return 1
24
+ fi
25
+ (cd -P "$root/gitbash" 2>/dev/null && pwd)
26
+ }
27
+
28
+ # How this copy of gitbash was installed: npm, pnpm, npx, package, script, git or unknown
29
+ gb_install_method() {
30
+ case "$SCRIPT_DIR" in
31
+ */_npx/*)
32
+ echo "npx"
33
+ return 0
34
+ ;;
35
+ */node_modules/gitbash)
36
+ local pm
37
+ for pm in npm pnpm; do
38
+ if command -v "$pm" >/dev/null 2>&1 &&
39
+ [[ "$(_gb_global_package_dir "$pm")" == "$SCRIPT_DIR" ]]; then
40
+ echo "$pm"
41
+ return 0
42
+ fi
43
+ done
44
+ # A dependency of some project, or a package manager we don't know
45
+ echo "package"
46
+ return 0
47
+ ;;
48
+ esac
49
+ if [[ -f "$SCRIPT_DIR/.gitbash-install" ]]; then
50
+ echo "script"
51
+ elif [[ -e "$SCRIPT_DIR/.git" ]]; then
52
+ echo "git"
53
+ elif [[ -f "$SCRIPT_DIR/package.json" && ! -e "$SCRIPT_DIR/README.md" ]]; then
54
+ # Installed by an install.sh that did not leave a marker yet
55
+ echo "script"
56
+ else
57
+ echo "unknown"
58
+ fi
59
+ }
60
+
61
+ # =============================================================================
62
+ # State (~/.local/state/gitbash/update-check)
63
+ # =============================================================================
64
+
65
+ _gb_update_state_file() {
66
+ echo "${XDG_STATE_HOME:-$HOME/.local/state}/gitbash/update-check"
67
+ }
68
+
69
+ # Read the state into _GB_UC_CHECKED, _GB_UC_LATEST, _GB_UC_PROMPTED and _GB_UC_SKIPPED
70
+ _gb_update_state_load() {
71
+ local file key value
72
+ file=$(_gb_update_state_file)
73
+ _GB_UC_CHECKED=0 _GB_UC_LATEST="" _GB_UC_PROMPTED=0 _GB_UC_SKIPPED=""
74
+ [[ -f "$file" ]] || return 0
75
+ while IFS='=' read -r key value; do
76
+ case "$key" in
77
+ checked) [[ "$value" =~ ^[0-9]+$ ]] && _GB_UC_CHECKED="$value" ;;
78
+ latest) _GB_UC_LATEST="$value" ;;
79
+ prompted) [[ "$value" =~ ^[0-9]+$ ]] && _GB_UC_PROMPTED="$value" ;;
80
+ skipped) _GB_UC_SKIPPED="$value" ;;
81
+ esac
82
+ done < "$file"
83
+ }
84
+
85
+ # Set one value, keeping the others (re-read first: a background check may have written)
86
+ # Usage: _gb_update_state_set checked|latest|prompted|skipped <value>
87
+ _gb_update_state_set() {
88
+ local file dir tmp
89
+ file=$(_gb_update_state_file)
90
+ dir=$(dirname "$file")
91
+ _gb_update_state_load
92
+ case "$1" in
93
+ checked) _GB_UC_CHECKED="$2" ;;
94
+ latest) _GB_UC_LATEST="$2" ;;
95
+ prompted) _GB_UC_PROMPTED="$2" ;;
96
+ skipped) _GB_UC_SKIPPED="$2" ;;
97
+ esac
98
+ mkdir -p "$dir" 2>/dev/null || return 1
99
+ tmp=$(mktemp "$dir/.update-check.XXXXXX" 2>/dev/null) || return 1
100
+ if ! printf 'checked=%s\nlatest=%s\nprompted=%s\nskipped=%s\n' \
101
+ "$_GB_UC_CHECKED" "$_GB_UC_LATEST" "$_GB_UC_PROMPTED" "$_GB_UC_SKIPPED" > "$tmp" ||
102
+ ! mv -f "$tmp" "$file"; then
103
+ rm -f "$tmp"
104
+ return 1
105
+ fi
106
+ }
107
+
108
+ _gb_now() {
109
+ if [[ -n "${EPOCHSECONDS:-}" ]]; then
110
+ echo "$EPOCHSECONDS"
111
+ else
112
+ date +%s
113
+ fi
114
+ }
115
+
116
+ # =============================================================================
117
+ # Versions
118
+ # =============================================================================
119
+
120
+ # Print the contents of a URL
121
+ _gb_fetch() {
122
+ if command -v curl >/dev/null 2>&1; then
123
+ curl -fsSL --max-time 10 "$1"
124
+ elif command -v wget >/dev/null 2>&1; then
125
+ wget -qO- -T 10 "$1"
126
+ else
127
+ return 1
128
+ fi
129
+ }
130
+
131
+ # Print the version of the latest GitHub release, e.g. 2.1.0
132
+ gb_latest_version() {
133
+ local json tag
134
+ json=$(_gb_fetch "https://api.github.com/repos/$GB_REPO/releases/latest" 2>/dev/null) || return 1
135
+ tag=$(printf '%s\n' "$json" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1)
136
+ tag="${tag#v}"
137
+ [[ "$tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
138
+ echo "$tag"
139
+ }
140
+
141
+ # Returns 0 if version $1 is newer than the running one
142
+ _gb_is_newer() {
143
+ [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && ! _gb_version_ge "$VERSION" "$1"
144
+ }
145
+
146
+ # =============================================================================
147
+ # Update
148
+ # =============================================================================
149
+
150
+ # Install version $1 the way this copy was installed
151
+ gb_self_update_to() {
152
+ local latest="$1" method
153
+ method=$(gb_install_method)
154
+ case "$method" in
155
+ npm|pnpm)
156
+ local -a cmd=(npm install -g "gitbash@$latest")
157
+ [[ "$method" == "pnpm" ]] && cmd=(pnpm add -g "gitbash@$latest")
158
+ print_info "Running: ${cmd[*]}"
159
+ if ! "${cmd[@]}"; then
160
+ print_error "Update failed. Run '${cmd[*]}' yourself (it may need more permissions)."
161
+ return 1
162
+ fi
163
+ ;;
164
+ script)
165
+ _gb_self_update_script "$latest" || return 1
166
+ ;;
167
+ npx)
168
+ print_info "gitbash runs through npx: use 'npx gitbash@latest' to get the newest version."
169
+ return 1
170
+ ;;
171
+ git)
172
+ print_info "gitbash runs from a git checkout ($SCRIPT_DIR): update it with 'git pull'."
173
+ return 1
174
+ ;;
175
+ package)
176
+ print_info "gitbash is a dependency in $SCRIPT_DIR: update it with your package manager."
177
+ return 1
178
+ ;;
179
+ *)
180
+ print_error "Could not detect how gitbash was installed ($SCRIPT_DIR)."
181
+ echo "Reinstall it with 'npm i -g gitbash' or the install script (see the README)." >&2
182
+ return 1
183
+ ;;
184
+ esac
185
+
186
+ # GITBASH_BIN, not SCRIPT_DIR: pnpm installs the new version in a new folder
187
+ local installed pkg_dir="${GITBASH_BIN%/bin/gitbash}"
188
+ installed=$(grep -o '"version": *"[^"]*"' "$pkg_dir/package.json" 2>/dev/null | head -1 | cut -d'"' -f4)
189
+ if [[ "$installed" != "$latest" ]]; then
190
+ print_error "Update finished, but gitbash in $pkg_dir is still at ${installed:-an unknown version}."
191
+ return 1
192
+ fi
193
+ print_success "Updated gitbash $VERSION → $latest"
194
+ }
195
+
196
+ # Re-run the install script of the new release for the same locations
197
+ _gb_self_update_script() {
198
+ local latest="$1" bin_dir="" key value tmp
199
+ if [[ -f "$SCRIPT_DIR/.gitbash-install" ]]; then
200
+ while IFS='=' read -r key value; do
201
+ [[ "$key" == "bin_dir" ]] && bin_dir="$value"
202
+ done < "$SCRIPT_DIR/.gitbash-install"
203
+ fi
204
+ bin_dir="${bin_dir:-$HOME/.local/bin}"
205
+
206
+ local -a vars=(GITBASH_VERSION="$latest" GITBASH_INSTALL_DIR="$SCRIPT_DIR" GITBASH_BIN_DIR="$bin_dir")
207
+ local hint="curl -fsSL https://raw.githubusercontent.com/$GB_REPO/main/install.sh | sudo ${vars[*]} sh"
208
+ if [[ ! -w "$(dirname "$SCRIPT_DIR")" ]]; then
209
+ print_error "No permission to write to $(dirname "$SCRIPT_DIR"). Update with:"
210
+ echo " $hint" >&2
211
+ return 1
212
+ fi
213
+
214
+ tmp=$(mktemp 2>/dev/null || mktemp -t gitbash) || return 1
215
+ if ! _gb_fetch "https://raw.githubusercontent.com/$GB_REPO/v$latest/install.sh" > "$tmp" 2>/dev/null ||
216
+ [[ ! -s "$tmp" ]]; then
217
+ rm -f "$tmp"
218
+ print_error "Could not download the install script of gitbash $latest."
219
+ return 1
220
+ fi
221
+ print_info "Running the install script of gitbash $latest"
222
+ if ! env "${vars[@]}" sh "$tmp"; then
223
+ rm -f "$tmp"
224
+ print_error "Update failed."
225
+ return 1
226
+ fi
227
+ rm -f "$tmp"
228
+ }
229
+
230
+ # gitbash --update
231
+ gb_self_update() {
232
+ local latest
233
+ print_info "Checking for updates..."
234
+ if ! latest=$(gb_latest_version); then
235
+ print_error "Could not check for updates (no network, or GitHub is not reachable)."
236
+ return 1
237
+ fi
238
+ _gb_update_state_set checked "$(_gb_now)"
239
+ _gb_update_state_set latest "$latest"
240
+ if ! _gb_is_newer "$latest"; then
241
+ print_success "gitbash $VERSION is up to date."
242
+ return 0
243
+ fi
244
+ gb_self_update_to "$latest"
245
+ }
246
+
247
+ # =============================================================================
248
+ # Background check
249
+ # =============================================================================
250
+
251
+ # Returns 0 if update checks are on: not turned off, not in CI, not a git checkout
252
+ gb_update_checks_enabled() {
253
+ case "$_GB_ENV_NO_UPDATE_CHECKS" in
254
+ "") [[ "${GITBASH_NO_UPDATE_CHECKS:-no}" == "yes" ]] && return 1 ;;
255
+ no|0|false) ;;
256
+ *) return 1 ;;
257
+ esac
258
+ local var
259
+ for var in CI CONTINUOUS_INTEGRATION BUILD_NUMBER GITHUB_ACTIONS GITLAB_CI TF_BUILD \
260
+ JENKINS_URL BUILDKITE CIRCLECI TRAVIS TEAMCITY_VERSION BITBUCKET_BUILD_NUMBER; do
261
+ case "${!var:-}" in
262
+ ""|false|0) ;;
263
+ *) return 1 ;;
264
+ esac
265
+ done
266
+ [[ ! -e "$SCRIPT_DIR/.git" ]]
267
+ }
268
+
269
+ # Run before a command: start a background check when one is due, and offer a
270
+ # known new version (at most every other day, never again for a skipped one).
271
+ # On a successful update, runs the command with the new version instead.
272
+ # Usage: gb_update_check <command> [args...]
273
+ gb_update_check() {
274
+ # Not when gitbash runs itself from a command (the parent already checked)
275
+ [[ -z "${GITBASH_NESTED:-}" ]] || return 0
276
+ gb_update_checks_enabled || return 0
277
+
278
+ local now
279
+ now=$(_gb_now)
280
+ _gb_update_state_load
281
+ if (( now - _GB_UC_CHECKED >= GB_UPDATE_CHECK_INTERVAL || _GB_UC_CHECKED > now )); then
282
+ _gb_update_state_set checked "$now"
283
+ (
284
+ local latest
285
+ latest=$(gb_latest_version) && _gb_update_state_set latest "$latest"
286
+ ) < /dev/null > /dev/null 2>&1 &
287
+ fi
288
+
289
+ local latest="$_GB_UC_LATEST"
290
+ _gb_is_newer "$latest" || return 0
291
+ [[ "$latest" != "$_GB_UC_SKIPPED" ]] || return 0
292
+ (( now - _GB_UC_PROMPTED >= GB_UPDATE_PROMPT_INTERVAL || _GB_UC_PROMPTED > now )) || return 0
293
+ # Only ask people at a terminal, never scripts
294
+ [[ -t 0 && -t 1 && -t 2 && "${GITBASH_ASSUME_YES:-}" != "1" ]] || return 0
295
+ local arg
296
+ for arg in "${@:2}"; do
297
+ [[ "$arg" == "-y" || "$arg" == "--yes" ]] && return 0
298
+ done
299
+
300
+ _gb_update_state_set prompted "$now"
301
+ print_info "gitbash $latest is available (you have $VERSION)."
302
+ local choice
303
+ gb_choice choice "Update now? [y]es, [n]ot now, [s]kip this version (y/N/s):" "yns" "n"
304
+ case "$choice" in
305
+ y)
306
+ if gb_self_update_to "$latest"; then
307
+ echo
308
+ exec "$GITBASH_BIN" "$@"
309
+ fi
310
+ echo
311
+ ;;
312
+ s)
313
+ _gb_update_state_set skipped "$latest"
314
+ print_info "You won't be asked about $latest again. Run 'gitbash --update' to update any time."
315
+ echo
316
+ ;;
317
+ *)
318
+ print_info "Run 'gitbash --update' to update any time."
319
+ echo
320
+ ;;
321
+ esac
322
+ return 0
323
+ }
@@ -9,7 +9,29 @@ if [[ -n "${_GB_UTILS_LOADED:-}" ]]; then
9
9
  fi
10
10
  _GB_UTILS_LOADED=1
11
11
 
12
- GB_FZF_MIN_VERSION="0.36.0"
12
+ # =============================================================================
13
+ # Platform
14
+ # =============================================================================
15
+
16
+ # Git Bash (MSYS2) or Cygwin on Windows
17
+ gb_is_windows() {
18
+ case "${OSTYPE:-}" in
19
+ msys*|cygwin*) return 0 ;;
20
+ esac
21
+ return 1
22
+ }
23
+
24
+ # Linux in WSL
25
+ _gb_is_wsl() {
26
+ [[ -n "${WSL_DISTRO_NAME:-}" || -n "${WSL_INTEROP:-}" ]]
27
+ }
28
+
29
+ # On Windows, fzf runs previews with bash and quotes {} for it since 0.54
30
+ if gb_is_windows; then
31
+ GB_FZF_MIN_VERSION="0.54.0"
32
+ else
33
+ GB_FZF_MIN_VERSION="0.36.0"
34
+ fi
13
35
 
14
36
  # =============================================================================
15
37
  # Output
@@ -20,6 +42,21 @@ _gb_color_ok() {
20
42
  [[ -z "${NO_COLOR:-}" && -t "$1" ]]
21
43
  }
22
44
 
45
+ # NO_COLOR (https://no-color.org): a non-empty value turns off all colors.
46
+ # GB_COLOR is the --color value for git and bat in fzf previews; it is
47
+ # exported because previews run in a separate bash started by fzf.
48
+ if [[ -n "${NO_COLOR:-}" ]]; then
49
+ GB_COLOR="never"
50
+ # Also for git's own output (added to any GIT_CONFIG_* entries already set)
51
+ _gb_n="${GIT_CONFIG_COUNT:-0}"
52
+ export "GIT_CONFIG_KEY_$_gb_n=color.ui" "GIT_CONFIG_VALUE_$_gb_n=never"
53
+ export GIT_CONFIG_COUNT=$((_gb_n + 1))
54
+ unset _gb_n
55
+ else
56
+ GB_COLOR="always"
57
+ fi
58
+ export GB_COLOR
59
+
23
60
  _gb_print() {
24
61
  local fd="$1" color="$2" symbol="$3"
25
62
  shift 3
@@ -35,6 +72,26 @@ print_info() { _gb_print 1 "0;34" "ℹ" "$@"; }
35
72
  print_warning() { _gb_print 2 "0;33" "⚠" "$@"; }
36
73
  print_error() { _gb_print 2 "0;31" "✗" "$@"; }
37
74
 
75
+ # Print help text from stdin, coloring the section headings
76
+ # ("Options:", "See also:", ...) and the "Usage:" label.
77
+ gb_help() {
78
+ if ! _gb_color_ok 1; then
79
+ cat
80
+ return 0
81
+ fi
82
+ local h=$'\033[1;38;5;209m' r=$'\033[0m' line
83
+ local heading='^[A-Z][A-Za-z ()]*:$'
84
+ while IFS= read -r line || [[ -n "$line" ]]; do
85
+ if [[ "$line" =~ $heading ]]; then
86
+ printf '%s%s%s\n' "$h" "$line" "$r"
87
+ elif [[ "$line" == "Usage: "* ]]; then
88
+ printf '%sUsage:%s%s\n' "$h" "$r" "${line#Usage:}"
89
+ else
90
+ printf '%s\n' "$line"
91
+ fi
92
+ done
93
+ }
94
+
38
95
  # =============================================================================
39
96
  # Prompts
40
97
  # =============================================================================
@@ -101,7 +158,7 @@ gb_choice() {
101
158
  GB_CONFIG_KEYS="GITBASH_CREATE_BRANCH_PREFIX GITBASH_FEATURE_BRANCH_PREFIX GITBASH_MERGE_COMMAND \
102
159
  GITBASH_CREATE_NO_ISSUE_PARSING GITBASH_CREATE_ISSUE_PARSING_FALLBACK GITBASH_CREATE_AUTO_PUSH \
103
160
  GITBASH_THEME GITBASH_STALE_MONTHS GITBASH_CLEANUP_DAYS GITBASH_PROTECTED_BRANCHES \
104
- GITBASH_BASE_BRANCH GITBASH_REMOTE"
161
+ GITBASH_BASE_BRANCH GITBASH_REMOTE GITBASH_NO_UPDATE_CHECKS GITBASH_RESET_KEEP"
105
162
 
106
163
  _gb_config_key_allowed() {
107
164
  [[ " $GB_CONFIG_KEYS " == *" $1 "* ]]
@@ -179,6 +236,7 @@ _gb_normalize_config() {
179
236
  GITBASH_CREATE_ISSUE_PARSING_FALLBACK="${GITBASH_CREATE_ISSUE_PARSING_FALLBACK:-NOISSUE}"
180
237
  GITBASH_PROTECTED_BRANCHES="${GITBASH_PROTECTED_BRANCHES:-main master develop release/*}"
181
238
  GITBASH_BASE_BRANCH="${GITBASH_BASE_BRANCH:-}"
239
+ GITBASH_RESET_KEEP="${GITBASH_RESET_KEEP:-}"
182
240
 
183
241
  case "${GITBASH_CREATE_NO_ISSUE_PARSING:-no}" in
184
242
  yes|no) GITBASH_CREATE_NO_ISSUE_PARSING="${GITBASH_CREATE_NO_ISSUE_PARSING:-no}" ;;
@@ -195,6 +253,12 @@ _gb_normalize_config() {
195
253
  *) print_warning "Invalid GITBASH_THEME '$GITBASH_THEME', using 'auto'."
196
254
  GITBASH_THEME="auto" ;;
197
255
  esac
256
+ case "${GITBASH_NO_UPDATE_CHECKS:-no}" in
257
+ yes|1|true) GITBASH_NO_UPDATE_CHECKS="yes" ;;
258
+ no|0|false) GITBASH_NO_UPDATE_CHECKS="no" ;;
259
+ *) print_warning "Invalid GITBASH_NO_UPDATE_CHECKS '$GITBASH_NO_UPDATE_CHECKS', using 'no'."
260
+ GITBASH_NO_UPDATE_CHECKS="no" ;;
261
+ esac
198
262
  if ! [[ "${GITBASH_STALE_MONTHS:-3}" =~ ^[1-9][0-9]*$ ]]; then
199
263
  print_warning "Invalid GITBASH_STALE_MONTHS '$GITBASH_STALE_MONTHS', using 3."
200
264
  GITBASH_STALE_MONTHS=3
@@ -244,7 +308,11 @@ _gb_version_ge() {
244
308
  # Check that fzf is installed and recent enough
245
309
  require_fzf() {
246
310
  if ! command -v fzf >/dev/null 2>&1; then
247
- print_error "fzf is not installed. Install it with 'brew install fzf' (macOS) or 'sudo apt install fzf' (Debian/Ubuntu)."
311
+ if gb_is_windows; then
312
+ print_error "fzf is not installed. Install it with 'winget install junegunn.fzf'."
313
+ else
314
+ print_error "fzf is not installed. Install it with 'brew install fzf' (macOS) or 'sudo apt install fzf' (Debian/Ubuntu)."
315
+ fi
248
316
  return 1
249
317
  fi
250
318
  local version
@@ -310,6 +378,29 @@ gb_base_ref() {
310
378
  fi
311
379
  }
312
380
 
381
+ # Fetch the current branch's upstream and, if it has new commits, offer to
382
+ # fast-forward. Returns 1 only when the fast-forward fails.
383
+ gb_offer_fast_forward() {
384
+ local branch upstream_remote upstream_merge behind
385
+ branch=$(gb_current_branch) || return 0
386
+ upstream_remote=$(git config "branch.$branch.remote" 2>/dev/null) || return 0
387
+ upstream_merge=$(git config "branch.$branch.merge" 2>/dev/null) || return 0
388
+ git fetch --quiet "$upstream_remote" "$upstream_merge" 2>/dev/null || return 0
389
+ behind=$(git rev-list --count "HEAD..@{upstream}" 2>/dev/null) || return 0
390
+ if [[ "$behind" -gt 0 ]]; then
391
+ print_warning "Branch is $behind commit(s) behind its upstream."
392
+ if gb_confirm "Fast-forward to the latest changes?" y; then
393
+ if git merge --ff-only --quiet "@{upstream}"; then
394
+ print_success "Up to date."
395
+ else
396
+ print_error "Could not fast-forward (you have local commits or changes). Run 'git pull' to reconcile."
397
+ return 1
398
+ fi
399
+ fi
400
+ fi
401
+ return 0
402
+ }
403
+
313
404
  # Returns 0 if the branch is protected (base branch or GITBASH_PROTECTED_BRANCHES).
314
405
  # Set GB_BASE beforehand to avoid detecting the base branch on every call.
315
406
  gb_is_protected() {
@@ -454,9 +545,9 @@ gb_sync_and_push() {
454
545
  # Previews
455
546
  # =============================================================================
456
547
 
457
- # Command that colors a diff read from stdin (delta if installed)
548
+ # Command that colors a diff read from stdin (delta if installed and colors are on)
458
549
  gb_diff_pager() {
459
- if command -v delta >/dev/null 2>&1; then
550
+ if [[ "$GB_COLOR" == "always" ]] && command -v delta >/dev/null 2>&1; then
460
551
  case "${GITBASH_THEME:-auto}" in
461
552
  dark) echo "delta --dark" ;;
462
553
  light) echo "delta --light" ;;
@@ -480,9 +571,9 @@ gb_bat_cmd() {
480
571
  return 0
481
572
  fi
482
573
  case "${GITBASH_THEME:-auto}" in
483
- dark) echo "$bat --color=always --style=numbers --theme=Dracula" ;;
484
- light) echo "$bat --color=always --style=numbers --theme=GitHub" ;;
485
- *) echo "$bat --color=always --style=numbers" ;;
574
+ dark) echo "$bat --color=$GB_COLOR --style=numbers --theme=Dracula" ;;
575
+ light) echo "$bat --color=$GB_COLOR --style=numbers --theme=GitHub" ;;
576
+ *) echo "$bat --color=$GB_COLOR --style=numbers" ;;
486
577
  esac
487
578
  }
488
579
 
@@ -518,10 +609,63 @@ gb_urlencode() {
518
609
  printf '%s' "$out"
519
610
  }
520
611
 
612
+ # Convert a remote URL to the repository's web URL (credentials and .git removed).
613
+ # Handles https://[user[:token]@]host/path, ssh://[user@]host[:port]/path and user@host:path.
614
+ gb_web_url() {
615
+ local url="$1" scheme="https" rest host path
616
+ case "$url" in
617
+ http://*|https://*)
618
+ scheme="${url%%://*}"
619
+ rest="${url#*://}"
620
+ host="${rest%%/*}"
621
+ host="${host##*@}"
622
+ path="${rest#*/}"
623
+ ;;
624
+ ssh://*)
625
+ rest="${url#ssh://}"
626
+ host="${rest%%/*}"
627
+ host="${host##*@}"
628
+ host="${host%%:*}"
629
+ path="${rest#*/}"
630
+ ;;
631
+ *@*:*)
632
+ host="${url%%:*}"
633
+ host="${host##*@}"
634
+ path="${url#*:}"
635
+ ;;
636
+ *)
637
+ return 1
638
+ ;;
639
+ esac
640
+ path="${path%/}"
641
+ path="${path%.git}"
642
+ if [[ -z "$host" || -z "$path" || ( -n "$rest" && "$path" == "$rest" ) ]]; then
643
+ return 1
644
+ fi
645
+
646
+ # Azure DevOps SSH: ssh.dev.azure.com:v3/org/project/repo
647
+ if [[ "$host" == "ssh.dev.azure.com" && "$path" == v3/*/*/* ]]; then
648
+ local org project repo
649
+ path="${path#v3/}"
650
+ org="${path%%/*}"; path="${path#*/}"
651
+ project="${path%%/*}"; repo="${path#*/}"
652
+ echo "https://dev.azure.com/$org/$project/_git/$repo"
653
+ return 0
654
+ fi
655
+ echo "$scheme://$host/$path"
656
+ }
657
+
521
658
  # Open a URL in the browser, or print it if no opener is available
522
659
  gb_open_url() {
523
660
  local url="$1"
524
- if command -v open >/dev/null 2>&1 && [[ "$(uname -s)" == "Darwin" ]]; then
661
+ if gb_is_windows && command -v rundll32 >/dev/null 2>&1; then
662
+ # Not cmd's start: it would need & and ^ in the URL escaped
663
+ rundll32 url.dll,FileProtocolHandler "$url"
664
+ elif _gb_is_wsl && command -v wslview >/dev/null 2>&1; then
665
+ wslview "$url"
666
+ elif _gb_is_wsl && command -v rundll32.exe >/dev/null 2>&1; then
667
+ rundll32.exe url.dll,FileProtocolHandler "$url"
668
+ elif command -v open >/dev/null 2>&1 && [[ "$(uname -s)" == "Darwin" ]]; then
525
669
  open "$url"
526
670
  elif command -v xdg-open >/dev/null 2>&1; then
527
671
  xdg-open "$url" >/dev/null 2>&1
@@ -14,7 +14,7 @@ branch() {
14
14
  return 0
15
15
  fi
16
16
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
17
- cat << 'EOF'
17
+ gb_help << 'EOF'
18
18
  Usage: branch [OPTIONS]
19
19
 
20
20
  Interactive menu to manage git branches.
@@ -86,8 +86,8 @@ Examples:
86
86
  ✓ 'feature/PROJ-123-add-new-feature' is now up-to-date with 'main'.
87
87
 
88
88
  Actions:
89
- 🌿 Create - Create a new feature branch with Jira parsing
90
- 🔀 Switch - Switch to another branch using fzf
89
+ 🌿 Create - Create a new feature branch with Jira parsing
90
+ 🔀 Switch - Switch to another branch using fzf
91
91
  ⬆️ Update - Update current branch with latest main/master
92
92
 
93
93
  Requirements:
@@ -13,7 +13,7 @@ cleanstash() {
13
13
  return 0
14
14
  fi
15
15
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
16
- cat << 'EOF'
16
+ gb_help << 'EOF'
17
17
  Usage: cleanstash [OPTIONS]
18
18
 
19
19
  Delete git stashes without applying them.
@@ -37,7 +37,7 @@ cleanup() {
37
37
  return 0
38
38
  ;;
39
39
  -h|--help)
40
- cat << 'EOF'
40
+ gb_help << 'EOF'
41
41
  Usage: cleanup [OPTIONS]
42
42
 
43
43
  Find and delete local branches that are no longer needed.
@@ -273,14 +273,14 @@ ${#sorted[@]} branches, $preselect_count pre-selected" \
273
273
  if [[ -z "$branch" ]]; then echo "Exit without action"; exit 0; fi
274
274
  echo "Branch: $branch"
275
275
  echo
276
- unpushed=$(git log --oneline --color=always "refs/heads/$branch" --not --remotes 2>/dev/null)
276
+ unpushed=$(git log --oneline --color=$GB_COLOR "refs/heads/$branch" --not --remotes 2>/dev/null)
277
277
  if [[ -n "$unpushed" ]]; then
278
278
  echo "Unpushed commits (only on this machine):"
279
279
  echo "$unpushed"
280
280
  echo
281
281
  fi
282
282
  echo "Recent commits:"
283
- git log --oneline --color=always -n 15 "refs/heads/$branch" 2>/dev/null
283
+ git log --oneline --color=$GB_COLOR -n 15 "refs/heads/$branch" 2>/dev/null
284
284
  ' \
285
285
  --preview-window=right:40% \
286
286
  --bind "load:$toggle_sequence" \
@@ -23,7 +23,7 @@ commit() {
23
23
  return 0
24
24
  ;;
25
25
  -h|--help)
26
- cat << 'EOF'
26
+ gb_help << 'EOF'
27
27
  Usage: commit [OPTIONS] [MESSAGE...] [-- MESSAGE...]
28
28
 
29
29
  Commit your changes with a message.