gitbash 1.9.1 → 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.
@@ -1,130 +1,221 @@
1
1
  #!/usr/bin/env bash
2
- # shellcheck disable=SC2155
3
- # Common utilities for gitbash commands - compatible with bash and zsh
4
-
5
- # Load config files if they exist
6
- # Global config first, then local repo config, then user config (which can override)
7
- CONFIG_FILE="$HOME/.gitbashrc"
8
- LOCAL_CONFIG_FILE=".gitbashrc"
9
- USER_CONFIG_FILE=".gitbashrc-user"
10
-
11
- if [[ -f "$CONFIG_FILE" ]]; then
12
- # shellcheck disable=SC1090
13
- source "$CONFIG_FILE"
14
- fi
2
+ # Common utilities for gitbash commands.
3
+ # Commands always run in a bash subprocess (bash 3.2+), started by bin/gitbash.
15
4
 
16
- # Source local config from repo root if present (overrides global)
17
- _repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
18
- if [[ -n "$_repo_root" && -f "$_repo_root/$LOCAL_CONFIG_FILE" ]]; then
19
- # shellcheck disable=SC1090
20
- source "$_repo_root/$LOCAL_CONFIG_FILE"
5
+ # Only load once, even if several command files source this file.
6
+ if [[ -n "${_GB_UTILS_LOADED:-}" ]]; then
7
+ # shellcheck disable=SC2317 # exit only runs when executed instead of sourced
8
+ return 0 2>/dev/null || exit 0
21
9
  fi
10
+ _GB_UTILS_LOADED=1
22
11
 
23
- # Source user config from repo root if present (overrides both)
24
- if [[ -n "$_repo_root" && -f "$_repo_root/$USER_CONFIG_FILE" ]]; then
25
- # shellcheck disable=SC1090
26
- source "$_repo_root/$USER_CONFIG_FILE"
27
- fi
28
- unset _repo_root
12
+ GB_FZF_MIN_VERSION="0.36.0"
29
13
 
30
- # Color codes for consistent output
31
- COLOR_RED='\033[0;31m'
32
- COLOR_GREEN='\033[0;32m'
33
- COLOR_YELLOW='\033[0;33m'
34
- COLOR_BLUE='\033[0;34m'
35
- COLOR_RESET='\033[0m'
14
+ # =============================================================================
15
+ # Output
16
+ # =============================================================================
36
17
 
37
- # Print success message (green checkmark)
38
- print_success() {
39
- echo -e "${COLOR_GREEN}✓${COLOR_RESET} $*"
18
+ # Colors only when writing to a terminal and NO_COLOR is not set
19
+ _gb_color_ok() {
20
+ [[ -z "${NO_COLOR:-}" && -t "$1" ]]
40
21
  }
41
22
 
42
- # Print error message (red X)
43
- print_error() {
44
- echo -e "${COLOR_RED}✗${COLOR_RESET} $*" >&2
23
+ _gb_print() {
24
+ local fd="$1" color="$2" symbol="$3"
25
+ shift 3
26
+ if _gb_color_ok "$fd"; then
27
+ printf '\033[%sm%s\033[0m %s\n' "$color" "$symbol" "$*" >&"$fd"
28
+ else
29
+ printf '%s %s\n' "$symbol" "$*" >&"$fd"
30
+ fi
45
31
  }
46
32
 
47
- # Print warning message (yellow warning)
48
- print_warning() {
49
- echo -e "${COLOR_YELLOW}⚠${COLOR_RESET} $*"
50
- }
33
+ print_success() { _gb_print 1 "0;32" "✓" "$@"; }
34
+ print_info() { _gb_print 1 "0;34" "ℹ" "$@"; }
35
+ print_warning() { _gb_print 2 "0;33" "⚠" "$@"; }
36
+ print_error() { _gb_print 2 "0;31" "✗" "$@"; }
51
37
 
52
- # Print info message (blue)
53
- print_info() {
54
- echo -e "${COLOR_BLUE}ℹ${COLOR_RESET} $*"
55
- }
38
+ # =============================================================================
39
+ # Prompts
40
+ # =============================================================================
56
41
 
57
- # Portable read with prompt that works in both bash and zsh
42
+ # Read a line into a variable. Returns non-zero on EOF.
58
43
  # Usage: prompt_read "prompt text" variable_name
59
- # The result is stored in the variable name provided
60
44
  prompt_read() {
61
- local prompt="$1"
62
- local varname="$2"
63
-
64
- if [[ -n "${ZSH_VERSION:-}" ]]; then
65
- # zsh: use read with ?prompt syntax
66
- read -r "${varname}?${prompt}"
67
- else
68
- # bash: use read -p
69
- read -rp "$prompt" "$varname"
45
+ local __prompt="$1" __var="$2" __value=""
46
+ printf '%s' "$__prompt" >&2
47
+ if IFS= read -r __value; then
48
+ printf -v "$__var" '%s' "$__value"
49
+ return 0
70
50
  fi
51
+ printf -v "$__var" '%s' "$__value"
52
+ echo >&2
53
+ return 1
71
54
  }
72
55
 
73
- # Portable read with readline editing (for defaults)
74
- # Usage: prompt_read_edit "prompt text" variable_name
75
- prompt_read_edit() {
76
- local prompt="$1"
77
- local varname="$2"
78
-
79
- if [[ -n "${ZSH_VERSION:-}" ]]; then
80
- # zsh: use vared or read with ?prompt
81
- read -r "${varname}?${prompt}"
82
- else
83
- # bash: use read -e -p
84
- read -e -rp "$prompt" "$varname"
56
+ # Yes/no question. Returns 0 for yes, 1 for no. EOF or Enter picks the default.
57
+ # GITBASH_ASSUME_YES=1 (set by --yes flags) answers yes, unless --strict is given.
58
+ # Usage: gb_confirm [--strict] "Question?" y|n
59
+ gb_confirm() {
60
+ local strict=false
61
+ if [[ "$1" == "--strict" ]]; then
62
+ strict=true
63
+ shift
85
64
  fi
86
- }
87
-
88
- # Check if fzf is installed, offer to install if not
89
- # Returns 0 if fzf is available, 1 if not
90
- require_fzf() {
91
- if command -v fzf >/dev/null 2>&1; then
65
+ local question="$1" default="${2:-n}" hint answer
66
+ if [[ "$strict" == false && "${GITBASH_ASSUME_YES:-}" == "1" ]]; then
92
67
  return 0
93
68
  fi
69
+ if [[ "$default" == "y" ]]; then hint="Y/n"; else hint="y/N"; fi
70
+ while true; do
71
+ prompt_read "$question ($hint): " answer || answer=""
72
+ case "$answer" in
73
+ "") [[ "$default" == "y" ]]; return ;;
74
+ [yY]|[yY][eE][sS]) return 0 ;;
75
+ [nN]|[nN][oO]) return 1 ;;
76
+ *) echo "Please answer y or n." >&2 ;;
77
+ esac
78
+ done
79
+ }
94
80
 
95
- print_error "fzf is not installed."
96
-
97
- local ans
98
- prompt_read "Install fzf with Homebrew? (y/N): " ans
99
- case "$ans" in
100
- [yY][eE][sS]|[yY])
101
- echo "Installing fzf with brew..."
102
- if ! command -v brew >/dev/null 2>&1; then
103
- print_error "Homebrew is not installed. Aborting."
104
- return 1
105
- fi
106
- if brew install fzf; then
107
- # Optional: install shell integrations automatically
108
- if [[ -f "$(brew --prefix)/opt/fzf/install" ]]; then
109
- echo "Running fzf install script..."
110
- yes | "$(brew --prefix)/opt/fzf/install"
111
- fi
112
- print_success "fzf installed successfully."
113
- return 0
114
- else
115
- print_error "fzf install failed. Aborting."
116
- return 1
81
+ # Single-letter choice. EOF or Enter picks the default letter.
82
+ # Usage: gb_choice var_name "Question [a]/[b]?" "ab" "a"
83
+ gb_choice() {
84
+ local __var="$1" question="$2" letters="$3" default="$4" __ans
85
+ while true; do
86
+ prompt_read "$question " __ans || __ans=""
87
+ __ans=$(printf '%s' "${__ans:0:1}" | tr '[:upper:]' '[:lower:]')
88
+ [[ -z "$__ans" ]] && __ans="$default"
89
+ if [[ -n "$__ans" && "$letters" == *"$__ans"* ]]; then
90
+ printf -v "$__var" '%s' "$__ans"
91
+ return 0
92
+ fi
93
+ echo "Please answer one of: $(printf '%s' "$letters" | sed 's/./& /g')" >&2
94
+ done
95
+ }
96
+
97
+ # =============================================================================
98
+ # Configuration (parsed, never sourced)
99
+ # =============================================================================
100
+
101
+ GB_CONFIG_KEYS="GITBASH_CREATE_BRANCH_PREFIX GITBASH_FEATURE_BRANCH_PREFIX GITBASH_MERGE_COMMAND \
102
+ GITBASH_CREATE_NO_ISSUE_PARSING GITBASH_CREATE_ISSUE_PARSING_FALLBACK GITBASH_CREATE_AUTO_PUSH \
103
+ GITBASH_THEME GITBASH_STALE_MONTHS GITBASH_CLEANUP_DAYS GITBASH_PROTECTED_BRANCHES \
104
+ GITBASH_BASE_BRANCH GITBASH_REMOTE"
105
+
106
+ _gb_config_key_allowed() {
107
+ [[ " $GB_CONFIG_KEYS " == *" $1 "* ]]
108
+ }
109
+
110
+ # Print the valid KEY=value entries of a config file, one per line.
111
+ # Nothing in the file is executed or expanded.
112
+ # Usage: _gb_config_entries <file> [warn]
113
+ _gb_config_entries() {
114
+ local file="$1" warn="${2:-}" line key value n=0
115
+ local re_assign='^[[:space:]]*(export[[:space:]]+)?(GITBASH_[A-Z_]+)=(.*)$'
116
+ local re_dq='^"([^"$`\\]*)"[[:space:]]*(#.*)?$'
117
+ local re_sq="^'([^']*)'[[:space:]]*(#.*)?\$"
118
+ local re_bare='^([^[:space:]#"'"'"'$`\\]*)[[:space:]]*(#.*)?$'
119
+ [[ -f "$file" && -r "$file" ]] || return 0
120
+ while IFS= read -r line || [[ -n "$line" ]]; do
121
+ n=$((n + 1))
122
+ line="${line%$'\r'}"
123
+ # Skip blank lines and comments
124
+ [[ "$line" =~ ^[[:space:]]*(#.*)?$ ]] && continue
125
+ if ! [[ "$line" =~ $re_assign ]]; then
126
+ [[ -n "$warn" ]] && print_warning "$file:$n: ignored (config files are no longer executed): $line"
127
+ continue
128
+ fi
129
+ key="${BASH_REMATCH[2]}"
130
+ value="${BASH_REMATCH[3]}"
131
+ if [[ "$value" =~ $re_dq || "$value" =~ $re_sq || "$value" =~ $re_bare ]]; then
132
+ value="${BASH_REMATCH[1]}"
133
+ else
134
+ [[ -n "$warn" ]] && print_warning "$file:$n: ignored $key (value must be a plain string without \$, \`, \\ or quotes)"
135
+ continue
136
+ fi
137
+ if ! _gb_config_key_allowed "$key"; then
138
+ [[ -n "$warn" ]] && print_warning "$file:$n: ignored unknown setting $key"
139
+ continue
140
+ fi
141
+ printf '%s=%s\n' "$key" "$value"
142
+ done < "$file"
143
+ }
144
+
145
+ gb_global_config_file() { echo "$HOME/.gitbashrc"; }
146
+
147
+ # Load ~/.gitbashrc, then <repo>/.gitbashrc, then <repo>/.gitbashrc-user.
148
+ gb_load_config() {
149
+ local repo_root="" file scope key value
150
+ repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || repo_root=""
151
+ for scope in global local user; do
152
+ case "$scope" in
153
+ global) file=$(gb_global_config_file) ;;
154
+ local) [[ -n "$repo_root" ]] || continue; file="$repo_root/.gitbashrc" ;;
155
+ user) [[ -n "$repo_root" ]] || continue; file="$repo_root/.gitbashrc-user" ;;
156
+ esac
157
+ while IFS='=' read -r key value; do
158
+ [[ -z "$key" ]] && continue
159
+ if [[ "$key" == "GITBASH_MERGE_COMMAND" && "$scope" == "local" ]]; then
160
+ print_warning "$file: ignored GITBASH_MERGE_COMMAND (a repository cannot choose which program runs; set it in ~/.gitbashrc or .gitbashrc-user)"
161
+ continue
117
162
  fi
118
- ;;
119
- *)
120
- echo "Aborted (skipped fzf installation)."
121
- return 1
122
- ;;
163
+ printf -v "$key" '%s' "$value"
164
+ done < <(_gb_config_entries "$file" warn)
165
+ done
166
+ _gb_normalize_config
167
+ }
168
+
169
+ # Apply defaults and validate values
170
+ _gb_normalize_config() {
171
+ # Branch prefix, with backward compatibility for GITBASH_FEATURE_BRANCH_PREFIX
172
+ if [[ -z "${GITBASH_CREATE_BRANCH_PREFIX:-}" && -n "${GITBASH_FEATURE_BRANCH_PREFIX:-}" ]]; then
173
+ GITBASH_CREATE_BRANCH_PREFIX="${GITBASH_FEATURE_BRANCH_PREFIX#feature/}"
174
+ fi
175
+ GITBASH_CREATE_BRANCH_PREFIX="${GITBASH_CREATE_BRANCH_PREFIX:-}"
176
+ GITBASH_CREATE_BRANCH_PREFIX="${GITBASH_CREATE_BRANCH_PREFIX%/}"
177
+
178
+ GITBASH_MERGE_COMMAND="${GITBASH_MERGE_COMMAND:-fork}"
179
+ GITBASH_CREATE_ISSUE_PARSING_FALLBACK="${GITBASH_CREATE_ISSUE_PARSING_FALLBACK:-NOISSUE}"
180
+ GITBASH_PROTECTED_BRANCHES="${GITBASH_PROTECTED_BRANCHES:-main master develop release/*}"
181
+ GITBASH_BASE_BRANCH="${GITBASH_BASE_BRANCH:-}"
182
+
183
+ case "${GITBASH_CREATE_NO_ISSUE_PARSING:-no}" in
184
+ yes|no) GITBASH_CREATE_NO_ISSUE_PARSING="${GITBASH_CREATE_NO_ISSUE_PARSING:-no}" ;;
185
+ *) print_warning "Invalid GITBASH_CREATE_NO_ISSUE_PARSING '$GITBASH_CREATE_NO_ISSUE_PARSING', using 'no'."
186
+ GITBASH_CREATE_NO_ISSUE_PARSING="no" ;;
187
+ esac
188
+ case "${GITBASH_CREATE_AUTO_PUSH:-yes}" in
189
+ yes|no) GITBASH_CREATE_AUTO_PUSH="${GITBASH_CREATE_AUTO_PUSH:-yes}" ;;
190
+ *) print_warning "Invalid GITBASH_CREATE_AUTO_PUSH '$GITBASH_CREATE_AUTO_PUSH', using 'yes'."
191
+ GITBASH_CREATE_AUTO_PUSH="yes" ;;
123
192
  esac
193
+ case "${GITBASH_THEME:-auto}" in
194
+ auto|dark|light) GITBASH_THEME="${GITBASH_THEME:-auto}" ;;
195
+ *) print_warning "Invalid GITBASH_THEME '$GITBASH_THEME', using 'auto'."
196
+ GITBASH_THEME="auto" ;;
197
+ esac
198
+ if ! [[ "${GITBASH_STALE_MONTHS:-3}" =~ ^[1-9][0-9]*$ ]]; then
199
+ print_warning "Invalid GITBASH_STALE_MONTHS '$GITBASH_STALE_MONTHS', using 3."
200
+ GITBASH_STALE_MONTHS=3
201
+ fi
202
+ GITBASH_STALE_MONTHS="${GITBASH_STALE_MONTHS:-3}"
203
+ if ! [[ "${GITBASH_CLEANUP_DAYS:-7}" =~ ^[1-9][0-9]*$ ]]; then
204
+ print_warning "Invalid GITBASH_CLEANUP_DAYS '$GITBASH_CLEANUP_DAYS', using 7."
205
+ GITBASH_CLEANUP_DAYS=7
206
+ fi
207
+ GITBASH_CLEANUP_DAYS="${GITBASH_CLEANUP_DAYS:-7}"
208
+ if ! [[ "${GITBASH_REMOTE:-origin}" =~ ^[A-Za-z0-9._-]+$ ]]; then
209
+ print_warning "Invalid GITBASH_REMOTE '$GITBASH_REMOTE', using 'origin'."
210
+ GITBASH_REMOTE="origin"
211
+ fi
212
+ GITBASH_REMOTE="${GITBASH_REMOTE:-origin}"
124
213
  }
125
214
 
126
- # Check if inside a git repository
127
- # Returns 0 if in a git repo, 1 if not
215
+ # =============================================================================
216
+ # Requirements
217
+ # =============================================================================
218
+
128
219
  require_git_repo() {
129
220
  if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
130
221
  return 0
@@ -133,71 +224,308 @@ require_git_repo() {
133
224
  return 1
134
225
  }
135
226
 
136
- # Get delta arguments based on theme setting
137
- # Uses GITBASH_THEME: light (default), dark, or auto
138
- _get_delta_args() {
139
- local theme="${GITBASH_THEME:-light}"
140
- case "$theme" in
141
- dark)
142
- echo "--dark"
143
- ;;
144
- auto)
145
- # auto - let delta detect
146
- echo ""
147
- ;;
148
- *)
149
- # light is default
150
- echo "--light"
151
- ;;
152
- esac
227
+ # Returns 0 if version $1 >= version $2 (dotted numbers)
228
+ _gb_version_ge() {
229
+ local IFS=.
230
+ local -a a b
231
+ read -r -a a <<< "$1"
232
+ read -r -a b <<< "$2"
233
+ local i x y
234
+ for i in 0 1 2; do
235
+ x="${a[$i]:-0}"; y="${b[$i]:-0}"
236
+ x="${x%%[!0-9]*}"; y="${y%%[!0-9]*}"
237
+ x="${x:-0}"; y="${y:-0}"
238
+ if (( 10#$x > 10#$y )); then return 0; fi
239
+ if (( 10#$x < 10#$y )); then return 1; fi
240
+ done
241
+ return 0
153
242
  }
154
243
 
155
- # Get bat arguments based on theme setting
156
- _get_bat_args() {
157
- local theme="${GITBASH_THEME:-light}"
158
- case "$theme" in
159
- dark)
160
- echo "--theme=Dracula"
161
- ;;
162
- auto)
163
- # auto - let bat detect
164
- echo ""
165
- ;;
166
- *)
167
- # light is default
168
- echo "--theme=GitHub"
169
- ;;
170
- esac
244
+ # Check that fzf is installed and recent enough
245
+ require_fzf() {
246
+ 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)."
248
+ return 1
249
+ fi
250
+ local version
251
+ version=$(fzf --version 2>/dev/null | awk '{print $1}')
252
+ if ! _gb_version_ge "$version" "$GB_FZF_MIN_VERSION"; then
253
+ print_error "fzf $version is too old; gitbash needs fzf $GB_FZF_MIN_VERSION or newer."
254
+ return 1
255
+ fi
256
+ return 0
171
257
  }
172
258
 
173
- # Get the diff command to use (delta if installed, otherwise git diff)
174
- # Usage: get_diff_cmd
175
- # Returns the command name to use for diffs
176
- get_diff_cmd() {
177
- if command -v delta >/dev/null 2>&1; then
178
- local delta_args
179
- delta_args=$(_get_delta_args)
180
- if [[ -n "$delta_args" ]]; then
181
- echo "delta $delta_args"
182
- else
183
- echo "delta"
259
+ # Run fzf isolated from the user's fzf defaults (which can change the output
260
+ # format), with previews and reloads executed by bash.
261
+ run_fzf() {
262
+ local bash_path
263
+ bash_path=$(command -v bash)
264
+ FZF_DEFAULT_OPTS="" FZF_DEFAULT_OPTS_FILE="" SHELL="$bash_path" fzf "$@"
265
+ }
266
+
267
+ # =============================================================================
268
+ # Git helpers
269
+ # =============================================================================
270
+
271
+ gb_remote() {
272
+ echo "${GITBASH_REMOTE:-origin}"
273
+ }
274
+
275
+ # Current branch name; fails on detached HEAD
276
+ gb_current_branch() {
277
+ git symbolic-ref --quiet --short HEAD 2>/dev/null
278
+ }
279
+
280
+ # Detect the base branch: GITBASH_BASE_BRANCH, <remote>/HEAD, main, master
281
+ gb_base_branch() {
282
+ local remote branch
283
+ remote=$(gb_remote)
284
+ if [[ -n "${GITBASH_BASE_BRANCH:-}" ]]; then
285
+ echo "$GITBASH_BASE_BRANCH"
286
+ return 0
287
+ fi
288
+ if branch=$(git symbolic-ref --quiet --short "refs/remotes/$remote/HEAD" 2>/dev/null); then
289
+ echo "${branch#"$remote"/}"
290
+ return 0
291
+ fi
292
+ for branch in main master; do
293
+ if git show-ref --verify --quiet "refs/heads/$branch" ||
294
+ git show-ref --verify --quiet "refs/remotes/$remote/$branch"; then
295
+ echo "$branch"
296
+ return 0
297
+ fi
298
+ done
299
+ return 1
300
+ }
301
+
302
+ # The ref to compare against: <remote>/<base> if it exists, else the local base
303
+ gb_base_ref() {
304
+ local base="$1" remote
305
+ remote=$(gb_remote)
306
+ if git show-ref --verify --quiet "refs/remotes/$remote/$base"; then
307
+ echo "$remote/$base"
308
+ else
309
+ echo "$base"
310
+ fi
311
+ }
312
+
313
+ # Returns 0 if the branch is protected (base branch or GITBASH_PROTECTED_BRANCHES).
314
+ # Set GB_BASE beforehand to avoid detecting the base branch on every call.
315
+ gb_is_protected() {
316
+ local branch="$1" base="${GB_BASE:-}" pattern
317
+ [[ -z "$base" ]] && base=$(gb_base_branch 2>/dev/null)
318
+ [[ -n "$base" && "$branch" == "$base" ]] && return 0
319
+ local -a patterns
320
+ IFS=' ,' read -r -a patterns <<< "${GITBASH_PROTECTED_BRANCHES:-main master develop release/*}"
321
+ for pattern in ${patterns[@]+"${patterns[@]}"}; do
322
+ # shellcheck disable=SC2053 # pattern is a glob on purpose
323
+ [[ "$branch" == $pattern ]] && return 0
324
+ done
325
+ return 1
326
+ }
327
+
328
+ # Run another gitbash command (same installation)
329
+ gb_run() {
330
+ "${GITBASH_BIN:-gitbash}" "$@"
331
+ }
332
+
333
+ # Fetch the current branch, sync with its remote counterpart and push.
334
+ # Never rebases or overwrites without asking.
335
+ # Usage: gb_sync_and_push [--amend] [--force-with-lease]
336
+ gb_sync_and_push() {
337
+ local amend=false force=false arg
338
+ for arg in "$@"; do
339
+ case "$arg" in
340
+ --amend) amend=true ;;
341
+ --force-with-lease) force=true ;;
342
+ esac
343
+ done
344
+
345
+ local remote branch
346
+ remote=$(gb_remote)
347
+ if ! branch=$(gb_current_branch); then
348
+ print_error "Detached HEAD: check out a branch before pushing."
349
+ return 1
350
+ fi
351
+ if ! git remote get-url "$remote" >/dev/null 2>&1; then
352
+ print_error "No remote '$remote' configured."
353
+ return 1
354
+ fi
355
+
356
+ local out
357
+ if ! out=$(git ls-remote --heads "$remote" "refs/heads/$branch" 2>&1); then
358
+ print_error "Could not reach '$remote':"
359
+ printf '%s\n' "$out" >&2
360
+ return 1
361
+ fi
362
+
363
+ # New branch: first push sets up tracking
364
+ if [[ -z "$out" ]]; then
365
+ print_info "Pushing new branch '$branch' to '$remote'..."
366
+ if git push -u "$remote" "$branch"; then
367
+ print_success "Pushed '$branch' to '$remote'."
368
+ return 0
369
+ fi
370
+ print_error "Failed to push '$branch' to '$remote'."
371
+ return 1
372
+ fi
373
+
374
+ if ! git fetch --quiet "$remote" "+refs/heads/$branch:refs/remotes/$remote/$branch"; then
375
+ print_error "Failed to fetch '$remote/$branch'."
376
+ return 1
377
+ fi
378
+
379
+ local local_commit remote_commit
380
+ local_commit=$(git rev-parse HEAD)
381
+ remote_commit=$(git rev-parse "refs/remotes/$remote/$branch")
382
+
383
+ if [[ "$local_commit" == "$remote_commit" ]]; then
384
+ print_info "'$remote/$branch' is already up to date."
385
+ return 0
386
+ fi
387
+
388
+ local push_args=()
389
+ if [[ -z "$(git config "branch.$branch.remote" 2>/dev/null)" ]]; then
390
+ push_args+=("-u")
391
+ fi
392
+
393
+ if git merge-base --is-ancestor "$remote_commit" "$local_commit"; then
394
+ : # Ahead of the remote: plain push
395
+ elif git merge-base --is-ancestor "$local_commit" "$remote_commit"; then
396
+ print_info "'$remote/$branch' has new commits. Fast-forwarding..."
397
+ if git merge --ff-only --quiet "refs/remotes/$remote/$branch"; then
398
+ print_success "Up to date with '$remote/$branch'. Nothing to push."
399
+ return 0
184
400
  fi
401
+ print_error "Fast-forward failed."
402
+ return 1
185
403
  else
186
- echo "git diff"
404
+ local ahead behind
405
+ ahead=$(git rev-list --count "$remote_commit..$local_commit")
406
+ behind=$(git rev-list --count "$local_commit..$remote_commit")
407
+ if [[ "$amend" == true || "$force" == true ]]; then
408
+ print_warning "'$remote/$branch' differs from your rewritten branch ($ahead local vs $behind remote commit(s))."
409
+ if gb_confirm --strict "Overwrite '$remote/$branch' with your version (push --force-with-lease)?" n; then
410
+ if git push "${push_args[@]+"${push_args[@]}"}" --force-with-lease="refs/heads/$branch:$remote_commit" "$remote" "$branch"; then
411
+ print_success "Force-pushed '$branch' to '$remote'."
412
+ return 0
413
+ fi
414
+ print_error "Force push failed (someone else may have pushed in the meantime)."
415
+ return 1
416
+ fi
417
+ print_info "Not pushed."
418
+ return 1
419
+ fi
420
+
421
+ print_warning "Your branch and '$remote/$branch' have diverged ($ahead local, $behind remote commit(s))."
422
+ local choice
423
+ gb_choice choice "[r]ebase onto '$remote/$branch', [m]erge it, or [a]bort? (r/m/A):" "rma" "a"
424
+ case "$choice" in
425
+ r)
426
+ if ! git rebase --rebase-merges "refs/remotes/$remote/$branch"; then
427
+ print_error "Rebase stopped. Resolve the conflicts and run 'git rebase --continue', or 'git rebase --abort'."
428
+ return 1
429
+ fi
430
+ ;;
431
+ m)
432
+ if ! git merge --no-edit "refs/remotes/$remote/$branch"; then
433
+ print_error "Merge stopped. Resolve the conflicts and commit, or run 'git merge --abort'."
434
+ return 1
435
+ fi
436
+ ;;
437
+ *)
438
+ print_info "Not pushed."
439
+ return 1
440
+ ;;
441
+ esac
187
442
  fi
443
+
444
+ print_info "Pushing '$branch' to '$remote'..."
445
+ if git push "${push_args[@]+"${push_args[@]}"}" "$remote" "$branch"; then
446
+ print_success "Pushed '$branch' to '$remote'."
447
+ return 0
448
+ fi
449
+ print_error "Failed to push '$branch' to '$remote'."
450
+ return 1
188
451
  }
189
452
 
190
- # Show a diff using delta if available, otherwise git diff
191
- # Usage: show_diff [git diff args...]
192
- # Example: show_diff --cached
193
- # Example: show_diff HEAD~1
194
- show_diff() {
453
+ # =============================================================================
454
+ # Previews
455
+ # =============================================================================
456
+
457
+ # Command that colors a diff read from stdin (delta if installed)
458
+ gb_diff_pager() {
195
459
  if command -v delta >/dev/null 2>&1; then
196
- local delta_args
197
- delta_args=$(_get_delta_args)
198
- # shellcheck disable=SC2086
199
- git diff "$@" | delta $delta_args
460
+ case "${GITBASH_THEME:-auto}" in
461
+ dark) echo "delta --dark" ;;
462
+ light) echo "delta --light" ;;
463
+ *) echo "delta" ;;
464
+ esac
465
+ else
466
+ echo "cat"
467
+ fi
468
+ }
469
+
470
+ # Command that shows a file with syntax highlighting (bat if installed)
471
+ gb_bat_cmd() {
472
+ local bat=""
473
+ if command -v bat >/dev/null 2>&1; then
474
+ bat="bat"
475
+ elif command -v batcat >/dev/null 2>&1; then
476
+ bat="batcat"
477
+ fi
478
+ if [[ -z "$bat" ]]; then
479
+ echo "cat"
480
+ return 0
481
+ fi
482
+ 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" ;;
486
+ esac
487
+ }
488
+
489
+ # =============================================================================
490
+ # Misc
491
+ # =============================================================================
492
+
493
+ gb_json_escape() {
494
+ local str="$1"
495
+ str="${str//\\/\\\\}"
496
+ str="${str//\"/\\\"}"
497
+ str="${str//$'\n'/\\n}"
498
+ str="${str//$'\r'/\\r}"
499
+ str="${str//$'\t'/\\t}"
500
+ printf '%s' "$str"
501
+ }
502
+
503
+ gb_urlencode() {
504
+ local LC_ALL=C
505
+ local str="$1" out="" char code hex i
506
+ for ((i = 0; i < ${#str}; i++)); do
507
+ char="${str:i:1}"
508
+ case "$char" in
509
+ [a-zA-Z0-9._~/-]) out+="$char" ;;
510
+ *)
511
+ # bash 3.2 reads bytes >= 0x80 as negative numbers: keep the low 8 bits
512
+ printf -v code '%d' "'$char"
513
+ printf -v hex '%%%02X' "$((code & 255))"
514
+ out+="$hex"
515
+ ;;
516
+ esac
517
+ done
518
+ printf '%s' "$out"
519
+ }
520
+
521
+ # Open a URL in the browser, or print it if no opener is available
522
+ gb_open_url() {
523
+ local url="$1"
524
+ if command -v open >/dev/null 2>&1 && [[ "$(uname -s)" == "Darwin" ]]; then
525
+ open "$url"
526
+ elif command -v xdg-open >/dev/null 2>&1; then
527
+ xdg-open "$url" >/dev/null 2>&1
200
528
  else
201
- git diff "$@"
529
+ echo "$url"
202
530
  fi
203
531
  }