gitbash 1.9.1 → 2.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 +100 -89
- package/bin/gitbash +345 -723
- package/commands/_utils.sh +485 -157
- package/commands/branch.sh +9 -12
- package/commands/cleanstash.sh +47 -122
- package/commands/cleanup.sh +305 -441
- package/commands/commit.sh +136 -172
- package/commands/commits.sh +81 -114
- package/commands/create.sh +231 -314
- package/commands/pr.sh +158 -146
- package/commands/stale.sh +137 -277
- package/commands/stash.sh +11 -39
- package/commands/stashes.sh +10 -13
- package/commands/status.sh +197 -250
- package/commands/switch.sh +186 -274
- package/commands/unstash.sh +45 -109
- package/commands/update.sh +123 -142
- package/docs/CONTRIBUTING.md +16 -5
- package/docs/SECURITY.md +14 -3
- package/package.json +5 -3
package/commands/_utils.sh
CHANGED
|
@@ -1,130 +1,221 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
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
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
COLOR_YELLOW='\033[0;33m'
|
|
34
|
-
COLOR_BLUE='\033[0;34m'
|
|
35
|
-
COLOR_RESET='\033[0m'
|
|
14
|
+
# =============================================================================
|
|
15
|
+
# Output
|
|
16
|
+
# =============================================================================
|
|
36
17
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
38
|
+
# =============================================================================
|
|
39
|
+
# Prompts
|
|
40
|
+
# =============================================================================
|
|
56
41
|
|
|
57
|
-
#
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
local
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
#
|
|
127
|
-
#
|
|
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
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
local
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
#
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
529
|
+
echo "$url"
|
|
202
530
|
fi
|
|
203
531
|
}
|