gitbash 3.0.1 → 3.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 +4 -4
- package/bin/gitbash +4 -1
- package/commands/_self_update.sh +22 -22
- package/commands/_utils.sh +74 -22
- package/commands/cleanstash.sh +1 -1
- package/commands/cleanup.sh +6 -6
- package/commands/commit.sh +1 -1
- package/commands/commits.sh +8 -6
- package/commands/create.sh +4 -4
- package/commands/pr.sh +2 -2
- package/commands/repo.sh +1 -1
- package/commands/reset-repo.sh +14 -9
- package/commands/stale.sh +2 -2
- package/commands/status.sh +5 -5
- package/commands/switch.sh +8 -6
- package/commands/unstash.sh +1 -1
- package/commands/update.sh +3 -3
- package/docs/CONTRIBUTING.md +1 -1
- package/docs/SECURITY.md +5 -3
- package/install.sh +171 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -212,7 +212,7 @@ Open the repository's web page in the browser: via the GitHub CLI (`gh repo view
|
|
|
212
212
|
reset-repo [-n|--dry-run] [-y|--yes]
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
-
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.
|
|
215
|
+
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. Submodules you initialized are reset to the recorded commit; others are left alone (run `git submodule update --init` to clone them).
|
|
216
216
|
|
|
217
217
|
### update
|
|
218
218
|
|
|
@@ -348,11 +348,11 @@ Config files are **read, never executed**. Only `GITBASH_*="value"` lines are us
|
|
|
348
348
|
|
|
349
349
|
#### Local Configuration
|
|
350
350
|
|
|
351
|
-
Run `gitbash --config-local` inside a repository to create a committed `.gitbashrc` with overrides for everyone working on it (e.g. a team branch prefix or different thresholds). For security, a committed `.gitbashrc` cannot set `GITBASH_MERGE_COMMAND
|
|
351
|
+
Run `gitbash --config-local` inside a repository to create a committed `.gitbashrc` with overrides for everyone working on it (e.g. a team branch prefix or different thresholds). For security, a committed `.gitbashrc` cannot set `GITBASH_MERGE_COMMAND` or `GITBASH_NO_UPDATE_CHECKS`, and its `GITBASH_PROTECTED_BRANCHES` only adds to the branches your own settings protect.
|
|
352
352
|
|
|
353
353
|
#### User Configuration
|
|
354
354
|
|
|
355
|
-
Run `gitbash --config-user` to create `.gitbashrc-user` for personal overrides in one repository. It is excluded from git via `.git/info/exclude`.
|
|
355
|
+
Run `gitbash --config-user` to create `.gitbashrc-user` for personal overrides in one repository. It is excluded from git via `.git/info/exclude`. If a repository commits a `.gitbashrc-user` anyway, gitbash treats it like the committed `.gitbashrc` (it cannot set `GITBASH_MERGE_COMMAND`).
|
|
356
356
|
|
|
357
357
|
**Configuration priority (highest to lowest):**
|
|
358
358
|
|
|
@@ -362,7 +362,7 @@ Run `gitbash --config-user` to create `.gitbashrc-user` for personal overrides i
|
|
|
362
362
|
|
|
363
363
|
#### Updates
|
|
364
364
|
|
|
365
|
-
`gitbash --update` installs the latest
|
|
365
|
+
`gitbash --update` installs the latest version on npm the same way gitbash was installed: with `npm install -g` (or `pnpm add -g`) for a global package, or with the install script kept in the installation, for the same locations.
|
|
366
366
|
|
|
367
367
|
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.
|
|
368
368
|
|
package/bin/gitbash
CHANGED
|
@@ -23,6 +23,9 @@ VERSION="$(grep -o '"version": *"[^"]*"' "$SCRIPT_DIR/package.json" | head -1 |
|
|
|
23
23
|
# Not exported: they would leak into git hooks, editors and merge tools.
|
|
24
24
|
# Commands are sourced into this process; nested runs of gitbash set them again.
|
|
25
25
|
export -n SCRIPT_DIR GITBASH_BIN VERSION
|
|
26
|
+
# Read from the environment (a parent gitbash sets them), but not passed on to
|
|
27
|
+
# git hooks, editors and merge tools either
|
|
28
|
+
export -n GITBASH_ASSUME_YES GITBASH_NESTED GB_COLOR 2>/dev/null
|
|
26
29
|
|
|
27
30
|
# shellcheck source=../commands/_utils.sh
|
|
28
31
|
source "$COMMANDS_DIR/_utils.sh"
|
|
@@ -656,7 +659,7 @@ main() {
|
|
|
656
659
|
gb_load_config
|
|
657
660
|
gb_update_check "$cmd" "$@"
|
|
658
661
|
# Commands run gitbash again (e.g. pr runs commit): no update prompt in there
|
|
659
|
-
|
|
662
|
+
GITBASH_NESTED=1
|
|
660
663
|
# shellcheck source=/dev/null
|
|
661
664
|
source "$COMMANDS_DIR/$cmd.sh"
|
|
662
665
|
"$cmd" "$@"
|
package/commands/_self_update.sh
CHANGED
|
@@ -128,14 +128,14 @@ _gb_fetch() {
|
|
|
128
128
|
fi
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
# Print the version
|
|
131
|
+
# Print the latest version on npm, e.g. 2.1.0. npm's "latest" tag only moves
|
|
132
|
+
# when a maintainer approves a staged version (2FA); a GitHub release doesn't need that.
|
|
132
133
|
gb_latest_version() {
|
|
133
|
-
local json
|
|
134
|
-
json=$(_gb_fetch "https://
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
echo "$tag"
|
|
134
|
+
local json version
|
|
135
|
+
json=$(_gb_fetch "https://registry.npmjs.org/-/package/gitbash/dist-tags" 2>/dev/null) || return 1
|
|
136
|
+
version=$(printf '%s\n' "$json" | sed -n 's/.*"latest": *"\([^"]*\)".*/\1/p' | head -n 1)
|
|
137
|
+
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
|
|
138
|
+
echo "$version"
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
# Returns 0 if version $1 is newer than the running one
|
|
@@ -193,9 +193,12 @@ gb_self_update_to() {
|
|
|
193
193
|
print_success "Updated gitbash $VERSION → $latest"
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
#
|
|
196
|
+
# Install version $1 with the install script that came with this copy, for the
|
|
197
|
+
# same locations. Never one downloaded from a git tag: whoever can create a tag
|
|
198
|
+
# would decide what runs. The script downloads the version from npm and checks
|
|
199
|
+
# its sha512 before installing it.
|
|
197
200
|
_gb_self_update_script() {
|
|
198
|
-
local latest="$1" bin_dir="" key value
|
|
201
|
+
local latest="$1" bin_dir="" key value
|
|
199
202
|
if [[ -f "$SCRIPT_DIR/.gitbash-install" ]]; then
|
|
200
203
|
while IFS='=' read -r key value; do
|
|
201
204
|
[[ "$key" == "bin_dir" ]] && bin_dir="$value"
|
|
@@ -204,27 +207,24 @@ _gb_self_update_script() {
|
|
|
204
207
|
bin_dir="${bin_dir:-$HOME/.local/bin}"
|
|
205
208
|
|
|
206
209
|
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 |
|
|
208
|
-
if [[ ! -
|
|
209
|
-
|
|
210
|
+
local hint="curl -fsSL https://raw.githubusercontent.com/$GB_REPO/main/install.sh | ${vars[*]} sh"
|
|
211
|
+
if [[ ! -f "$SCRIPT_DIR/install.sh" ]]; then
|
|
212
|
+
# Installed before gitbash 3.1.0, which started to keep the script
|
|
213
|
+
print_error "This installation has no install script yet. Update once with:"
|
|
210
214
|
echo " $hint" >&2
|
|
211
215
|
return 1
|
|
212
216
|
fi
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
[[ ! -s "$tmp" ]]; then
|
|
217
|
-
rm -f "$tmp"
|
|
218
|
-
print_error "Could not download the install script of gitbash $latest."
|
|
217
|
+
if [[ ! -w "$(dirname "$SCRIPT_DIR")" ]]; then
|
|
218
|
+
print_error "No permission to write to $(dirname "$SCRIPT_DIR"). Update with:"
|
|
219
|
+
echo " curl -fsSL https://raw.githubusercontent.com/$GB_REPO/main/install.sh | sudo ${vars[*]} sh" >&2
|
|
219
220
|
return 1
|
|
220
221
|
fi
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
|
|
223
|
+
print_info "Installing gitbash $latest"
|
|
224
|
+
if ! env "${vars[@]}" sh "$SCRIPT_DIR/install.sh"; then
|
|
224
225
|
print_error "Update failed."
|
|
225
226
|
return 1
|
|
226
227
|
fi
|
|
227
|
-
rm -f "$tmp"
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
# gitbash --update
|
package/commands/_utils.sh
CHANGED
|
@@ -43,8 +43,8 @@ _gb_color_ok() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
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;
|
|
47
|
-
#
|
|
46
|
+
# GB_COLOR is the --color value for git and bat in fzf previews; run_fzf
|
|
47
|
+
# passes it to fzf, whose previews run in a separate bash.
|
|
48
48
|
if [[ -n "${NO_COLOR:-}" ]]; then
|
|
49
49
|
GB_COLOR="never"
|
|
50
50
|
# Also for git's own output (added to any GIT_CONFIG_* entries already set)
|
|
@@ -55,15 +55,25 @@ if [[ -n "${NO_COLOR:-}" ]]; then
|
|
|
55
55
|
else
|
|
56
56
|
GB_COLOR="always"
|
|
57
57
|
fi
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
# Remove terminal control characters from text read from stdin: C0 controls
|
|
60
|
+
# except tab and newline (ESC, BEL, CR, ...), DEL, and C1 controls in UTF-8.
|
|
61
|
+
# Commit messages, file names, branch names and config lines come from
|
|
62
|
+
# repositories and could otherwise change the clipboard or fake output.
|
|
63
|
+
gb_sanitize() {
|
|
64
|
+
local c1
|
|
65
|
+
c1=$(printf '\302[\200-\237]')
|
|
66
|
+
LC_ALL=C tr -d '\000-\010\013-\037\177' | LC_ALL=C sed "s/$c1//g"
|
|
67
|
+
}
|
|
59
68
|
|
|
60
69
|
_gb_print() {
|
|
61
|
-
local fd="$1" color="$2" symbol="$3"
|
|
70
|
+
local fd="$1" color="$2" symbol="$3" text
|
|
62
71
|
shift 3
|
|
72
|
+
text=$(printf '%s' "$*" | gb_sanitize)
|
|
63
73
|
if _gb_color_ok "$fd"; then
|
|
64
|
-
printf '\033[%sm%s\033[0m %s\n' "$color" "$symbol" "
|
|
74
|
+
printf '\033[%sm%s\033[0m %s\n' "$color" "$symbol" "$text" >&"$fd"
|
|
65
75
|
else
|
|
66
|
-
printf '%s %s\n' "$symbol" "
|
|
76
|
+
printf '%s %s\n' "$symbol" "$text" >&"$fd"
|
|
67
77
|
fi
|
|
68
78
|
}
|
|
69
79
|
|
|
@@ -111,8 +121,8 @@ prompt_read() {
|
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
# Yes/no question. Returns 0 for yes, 1 for no. EOF or Enter picks the default.
|
|
114
|
-
# GITBASH_ASSUME_YES=1 (
|
|
115
|
-
#
|
|
124
|
+
# GITBASH_ASSUME_YES=1 (set by --yes flags and passed on to commands run from
|
|
125
|
+
# a command) answers yes, unless --strict is given.
|
|
116
126
|
# Usage: gb_confirm [--strict] "Question?" y|n
|
|
117
127
|
gb_confirm() {
|
|
118
128
|
local strict=false
|
|
@@ -203,6 +213,7 @@ _gb_config_entries() {
|
|
|
203
213
|
gb_global_config_file() { echo "$HOME/.gitbashrc"; }
|
|
204
214
|
|
|
205
215
|
# Load ~/.gitbashrc, then <repo>/.gitbashrc, then <repo>/.gitbashrc-user.
|
|
216
|
+
# A .gitbashrc-user that is tracked by git is treated like .gitbashrc.
|
|
206
217
|
gb_load_config() {
|
|
207
218
|
local repo_root="" file scope key value
|
|
208
219
|
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || repo_root=""
|
|
@@ -210,14 +221,32 @@ gb_load_config() {
|
|
|
210
221
|
case "$scope" in
|
|
211
222
|
global) file=$(gb_global_config_file) ;;
|
|
212
223
|
local) [[ -n "$repo_root" ]] || continue; file="$repo_root/.gitbashrc" ;;
|
|
213
|
-
user)
|
|
224
|
+
user)
|
|
225
|
+
[[ -n "$repo_root" ]] || continue
|
|
226
|
+
file="$repo_root/.gitbashrc-user"
|
|
227
|
+
# A committed .gitbashrc-user came with the repository: as untrusted as .gitbashrc
|
|
228
|
+
if git -C "$repo_root" ls-files --error-unmatch -- .gitbashrc-user >/dev/null 2>&1; then
|
|
229
|
+
scope="local"
|
|
230
|
+
fi
|
|
231
|
+
;;
|
|
214
232
|
esac
|
|
233
|
+
# A repository's config file must be a file in the repository, not a link to
|
|
234
|
+
# something else on this machine (whose lines would be printed as warnings)
|
|
235
|
+
if [[ "$scope" == "local" && -L "$file" ]]; then
|
|
236
|
+
print_warning "$file: ignored (a symbolic link)"
|
|
237
|
+
continue
|
|
238
|
+
fi
|
|
215
239
|
while IFS='=' read -r key value; do
|
|
216
240
|
[[ -z "$key" ]] && continue
|
|
217
|
-
|
|
218
|
-
|
|
241
|
+
# A repository cannot choose which program runs, or turn off update checks
|
|
242
|
+
if [[ "$scope" == "local" && ( "$key" == "GITBASH_MERGE_COMMAND" || "$key" == "GITBASH_NO_UPDATE_CHECKS" ) ]]; then
|
|
243
|
+
print_warning "$file: ignored $key (a repository cannot set it; set it in ~/.gitbashrc or an uncommitted .gitbashrc-user)"
|
|
219
244
|
continue
|
|
220
245
|
fi
|
|
246
|
+
# A repository can protect more branches, but not unprotect yours
|
|
247
|
+
if [[ "$scope" == "local" && "$key" == "GITBASH_PROTECTED_BRANCHES" ]]; then
|
|
248
|
+
value="${GITBASH_PROTECTED_BRANCHES:-main master develop release/*} $value"
|
|
249
|
+
fi
|
|
221
250
|
printf -v "$key" '%s' "$value"
|
|
222
251
|
done < <(_gb_config_entries "$file" warn)
|
|
223
252
|
done
|
|
@@ -335,7 +364,9 @@ require_fzf() {
|
|
|
335
364
|
run_fzf() {
|
|
336
365
|
local bash_path
|
|
337
366
|
bash_path=$(command -v bash)
|
|
338
|
-
|
|
367
|
+
# GITBASH_NESTED: bindings that run gitbash (e.g. switch's Del) are nested runs
|
|
368
|
+
FZF_DEFAULT_OPTS="" FZF_DEFAULT_OPTS_FILE="" SHELL="$bash_path" GB_COLOR="$GB_COLOR" \
|
|
369
|
+
GITBASH_NESTED=1 GITBASH_ASSUME_YES="${GITBASH_ASSUME_YES:-}" fzf "$@"
|
|
339
370
|
}
|
|
340
371
|
|
|
341
372
|
# =============================================================================
|
|
@@ -346,9 +377,12 @@ gb_remote() {
|
|
|
346
377
|
echo "${GITBASH_REMOTE:-origin}"
|
|
347
378
|
}
|
|
348
379
|
|
|
349
|
-
# Current branch name; fails on detached HEAD
|
|
380
|
+
# Current branch name; fails on detached HEAD. Not --short: with a tag of the
|
|
381
|
+
# same name, that prints "heads/<branch>".
|
|
350
382
|
gb_current_branch() {
|
|
351
|
-
|
|
383
|
+
local ref
|
|
384
|
+
ref=$(git symbolic-ref --quiet HEAD 2>/dev/null) || return 1
|
|
385
|
+
echo "${ref#refs/heads/}"
|
|
352
386
|
}
|
|
353
387
|
|
|
354
388
|
# Returns 0 if $1 is a valid branch name that can't be mistaken for an option.
|
|
@@ -367,9 +401,11 @@ gb_base_branch() {
|
|
|
367
401
|
echo "$GITBASH_BASE_BRANCH"
|
|
368
402
|
return 0
|
|
369
403
|
fi
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
404
|
+
# Full ref name: --short would give "remotes/origin/main" when a tag "origin/main" exists
|
|
405
|
+
if branch=$(git symbolic-ref --quiet "refs/remotes/$remote/HEAD" 2>/dev/null) &&
|
|
406
|
+
[[ "$branch" == "refs/remotes/$remote/"* ]] &&
|
|
407
|
+
_gb_valid_branch_name "${branch#"refs/remotes/$remote/"}"; then
|
|
408
|
+
echo "${branch#"refs/remotes/$remote/"}"
|
|
373
409
|
return 0
|
|
374
410
|
fi
|
|
375
411
|
for branch in main master; do
|
|
@@ -382,17 +418,25 @@ gb_base_branch() {
|
|
|
382
418
|
return 1
|
|
383
419
|
}
|
|
384
420
|
|
|
385
|
-
# The ref to compare against
|
|
421
|
+
# The ref to compare against, as a full ref name: refs/remotes/<remote>/<base>
|
|
422
|
+
# if it exists, else refs/heads/<base>. Never a short name like origin/main:
|
|
423
|
+
# git resolves those to a tag of the same name first, and tags come from the remote.
|
|
386
424
|
gb_base_ref() {
|
|
387
425
|
local base="$1" remote
|
|
388
426
|
remote=$(gb_remote)
|
|
389
427
|
if git show-ref --verify --quiet "refs/remotes/$remote/$base"; then
|
|
390
|
-
echo "
|
|
428
|
+
echo "refs/remotes/$remote/$base"
|
|
391
429
|
else
|
|
392
|
-
echo "
|
|
430
|
+
echo "refs/heads/$base"
|
|
393
431
|
fi
|
|
394
432
|
}
|
|
395
433
|
|
|
434
|
+
# Short name of a full ref, for messages: refs/remotes/origin/main -> origin/main
|
|
435
|
+
gb_ref_short() {
|
|
436
|
+
local ref="${1#refs/remotes/}"
|
|
437
|
+
echo "${ref#refs/heads/}"
|
|
438
|
+
}
|
|
439
|
+
|
|
396
440
|
# Fetch the current branch's upstream and, if it has new commits, offer to
|
|
397
441
|
# fast-forward. Returns 1 only when the fast-forward fails.
|
|
398
442
|
gb_offer_fast_forward() {
|
|
@@ -431,9 +475,11 @@ gb_is_protected() {
|
|
|
431
475
|
return 1
|
|
432
476
|
}
|
|
433
477
|
|
|
434
|
-
# Run another gitbash command (same installation)
|
|
478
|
+
# Run another gitbash command (same installation). GITBASH_NESTED and
|
|
479
|
+
# GITBASH_ASSUME_YES (set by -y) are passed only to it, not exported to git
|
|
480
|
+
# hooks, editors or merge tools.
|
|
435
481
|
gb_run() {
|
|
436
|
-
"${GITBASH_BIN:-gitbash}" "$@"
|
|
482
|
+
GITBASH_NESTED=1 GITBASH_ASSUME_YES="${GITBASH_ASSUME_YES:-}" "${GITBASH_BIN:-gitbash}" "$@"
|
|
437
483
|
}
|
|
438
484
|
|
|
439
485
|
# Fetch the current branch, sync with its remote counterpart and push.
|
|
@@ -691,6 +737,12 @@ gb_web_url() {
|
|
|
691
737
|
echo "$scheme://$host/$path"
|
|
692
738
|
}
|
|
693
739
|
|
|
740
|
+
# A remote URL for messages, without user name and password or token
|
|
741
|
+
# (https://user:token@host/path -> https://host/path)
|
|
742
|
+
gb_redact_url() {
|
|
743
|
+
printf '%s' "$1" | sed -E 's#^([A-Za-z][A-Za-z0-9+.-]*://)[^/@]*@#\1#'
|
|
744
|
+
}
|
|
745
|
+
|
|
694
746
|
# Open a URL in the browser, or print it if no opener is available
|
|
695
747
|
gb_open_url() {
|
|
696
748
|
local url="$1"
|
package/commands/cleanstash.sh
CHANGED
|
@@ -44,7 +44,7 @@ EOF
|
|
|
44
44
|
while [[ $# -gt 0 ]]; do
|
|
45
45
|
case "$1" in
|
|
46
46
|
-h|--help) cleanstash --help; return 0 ;;
|
|
47
|
-
-y|--yes)
|
|
47
|
+
-y|--yes) GITBASH_ASSUME_YES=1 ;;
|
|
48
48
|
*)
|
|
49
49
|
print_error "Unknown argument: $1"
|
|
50
50
|
echo "Usage: cleanstash [-y|--yes]" >&2
|
package/commands/cleanup.sh
CHANGED
|
@@ -106,7 +106,7 @@ EOF
|
|
|
106
106
|
shift
|
|
107
107
|
;;
|
|
108
108
|
-y|--yes)
|
|
109
|
-
|
|
109
|
+
GITBASH_ASSUME_YES=1
|
|
110
110
|
shift
|
|
111
111
|
;;
|
|
112
112
|
*)
|
|
@@ -183,7 +183,7 @@ EOF
|
|
|
183
183
|
fi
|
|
184
184
|
entries+=("$preselect|$label|$ts|$branch|$rel|$email|$unpushed|$name")
|
|
185
185
|
done < <(git for-each-ref \
|
|
186
|
-
--format="%(refname:
|
|
186
|
+
--format="%(refname:lstrip=2)%1f%(committerdate:unix)%1f%(committerdate:relative)%1f%(authoremail)%1f%(upstream:short)%1f%(upstream:track)%1f%(authorname)" \
|
|
187
187
|
refs/heads 2>/dev/null)
|
|
188
188
|
|
|
189
189
|
# Pre-selected first, then newest first
|
|
@@ -235,7 +235,7 @@ EOF
|
|
|
235
235
|
fi
|
|
236
236
|
if [[ "$dry_run" == true ]]; then
|
|
237
237
|
echo "Would delete ${#selected[@]} branch(es):"
|
|
238
|
-
printf ' - %s\n' "${selected[@]}"
|
|
238
|
+
printf ' - %s\n' "${selected[@]}" | gb_sanitize
|
|
239
239
|
return 0
|
|
240
240
|
fi
|
|
241
241
|
else
|
|
@@ -322,7 +322,7 @@ ${#sorted[@]} branches, $preselect_count pre-selected" \
|
|
|
322
322
|
label_of=""
|
|
323
323
|
[[ "$branch" == "$current_branch" ]] && { need_switch=true; label_of=" (current branch)"; }
|
|
324
324
|
[[ "$merged_list" == *" $branch "* ]] || only_merged=false
|
|
325
|
-
echo " - $branch$label_of"
|
|
325
|
+
echo " - $branch$label_of" | gb_sanitize
|
|
326
326
|
done
|
|
327
327
|
if [[ "$need_switch" == true ]]; then
|
|
328
328
|
echo "Will switch to '$base_branch' first."
|
|
@@ -363,8 +363,8 @@ ${#sorted[@]} branches, $preselect_count pre-selected" \
|
|
|
363
363
|
echo
|
|
364
364
|
print_warning "These branches have commits that are not merged. Deleting them loses those commits:"
|
|
365
365
|
for branch in "${refused[@]}"; do
|
|
366
|
-
echo " $branch:"
|
|
367
|
-
git log --oneline -n 5 "refs/heads/$branch" --not "$base_ref" --remotes 2>/dev/null | sed 's/^/ /'
|
|
366
|
+
echo " $branch:" | gb_sanitize
|
|
367
|
+
git log --oneline -n 5 "refs/heads/$branch" --not "$base_ref" --remotes 2>/dev/null | gb_sanitize | sed 's/^/ /'
|
|
368
368
|
done
|
|
369
369
|
if gb_confirm --strict "Force-delete these ${#refused[@]} branch(es)?" n; then
|
|
370
370
|
for branch in "${refused[@]}"; do
|
package/commands/commit.sh
CHANGED
package/commands/commits.sh
CHANGED
|
@@ -60,7 +60,7 @@ EOF
|
|
|
60
60
|
while [[ $# -gt 0 ]]; do
|
|
61
61
|
case "$1" in
|
|
62
62
|
-a|--all) all=true ;;
|
|
63
|
-
-y|--yes)
|
|
63
|
+
-y|--yes) GITBASH_ASSUME_YES=1 ;;
|
|
64
64
|
-*) print_error "Unknown option '$1'. See 'commits --help'."; return 1 ;;
|
|
65
65
|
*)
|
|
66
66
|
if [[ -n "$count" ]]; then
|
|
@@ -98,12 +98,14 @@ EOF
|
|
|
98
98
|
elif git rev-parse --verify --quiet "$base_ref^{commit}" >/dev/null; then
|
|
99
99
|
log_args=(--not "$base_ref")
|
|
100
100
|
# Also exclude the local base branch when it is ahead of the remote one
|
|
101
|
-
if [[ "$base_ref" != "
|
|
101
|
+
if [[ "$base_ref" != "refs/heads/$base_branch" ]] &&
|
|
102
102
|
git show-ref --verify --quiet "refs/heads/$base_branch"; then
|
|
103
|
-
log_args+=("
|
|
103
|
+
log_args+=("refs/heads/$base_branch")
|
|
104
104
|
fi
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
local base_name
|
|
106
|
+
base_name=$(gb_ref_short "$base_ref")
|
|
107
|
+
scope="commits not in '$base_name' (up to $count)"
|
|
108
|
+
empty_hint="No commits on '$current_branch' that are not in '$base_name'. Use 'commits --all' to show all recent commits."
|
|
107
109
|
fi
|
|
108
110
|
fi
|
|
109
111
|
|
|
@@ -184,7 +186,7 @@ Showing $scope" \
|
|
|
184
186
|
echo
|
|
185
187
|
echo "Commits to revert (newest first):"
|
|
186
188
|
for hash in "${to_revert[@]}"; do
|
|
187
|
-
echo " - $(git log -1 --format='%h %s' "$hash")"
|
|
189
|
+
echo " - $(git log -1 --format='%h %s' "$hash" | gb_sanitize)"
|
|
188
190
|
done
|
|
189
191
|
echo
|
|
190
192
|
|
package/commands/create.sh
CHANGED
|
@@ -103,7 +103,7 @@ EOF
|
|
|
103
103
|
--hotfix) branch_type="hotfix/"; shift ;;
|
|
104
104
|
--release) branch_type="release/"; shift ;;
|
|
105
105
|
-p|--push) auto_push="yes"; shift ;;
|
|
106
|
-
-y|--yes)
|
|
106
|
+
-y|--yes) GITBASH_ASSUME_YES=1; shift ;;
|
|
107
107
|
--no-push) auto_push="no"; shift ;;
|
|
108
108
|
--)
|
|
109
109
|
shift
|
|
@@ -244,10 +244,10 @@ release/ - Release preparation branches"
|
|
|
244
244
|
if base_branch=$(gb_base_branch); then
|
|
245
245
|
print_info "Fetching latest '$base_branch' from '$remote'..."
|
|
246
246
|
if git fetch --quiet "$remote" "+refs/heads/$base_branch:refs/remotes/$remote/$base_branch" 2>/dev/null; then
|
|
247
|
-
start_point="
|
|
247
|
+
start_point="refs/remotes/$remote/$base_branch"
|
|
248
248
|
elif git show-ref --verify --quiet "refs/heads/$base_branch"; then
|
|
249
249
|
print_warning "Could not fetch '$base_branch'. Creating the branch from local '$base_branch'."
|
|
250
|
-
start_point="
|
|
250
|
+
start_point="refs/heads/$base_branch"
|
|
251
251
|
fi
|
|
252
252
|
fi
|
|
253
253
|
if [[ -z "$start_point" ]]; then
|
|
@@ -257,7 +257,7 @@ release/ - Release preparation branches"
|
|
|
257
257
|
print_info "Creating branch..."
|
|
258
258
|
if [[ -n "$start_point" ]]; then
|
|
259
259
|
git switch --quiet --no-track -c "$branch_name" "$start_point" || { print_error "Failed to create branch."; return 1; }
|
|
260
|
-
print_success "Created and switched to '$branch_name' (from $start_point)"
|
|
260
|
+
print_success "Created and switched to '$branch_name' (from $(gb_ref_short "$start_point"))"
|
|
261
261
|
else
|
|
262
262
|
git switch --quiet -c "$branch_name" || { print_error "Failed to create branch."; return 1; }
|
|
263
263
|
print_success "Created and switched to '$branch_name'"
|
package/commands/pr.sh
CHANGED
|
@@ -78,7 +78,7 @@ EOF
|
|
|
78
78
|
shift
|
|
79
79
|
;;
|
|
80
80
|
-y|--yes)
|
|
81
|
-
|
|
81
|
+
GITBASH_ASSUME_YES=1
|
|
82
82
|
shift
|
|
83
83
|
;;
|
|
84
84
|
*)
|
|
@@ -102,7 +102,7 @@ EOF
|
|
|
102
102
|
return 1
|
|
103
103
|
fi
|
|
104
104
|
if ! base=$(gb_web_url "$remote_url"); then
|
|
105
|
-
print_error "Don't know how to open '$remote_url' in a browser."
|
|
105
|
+
print_error "Don't know how to open '$(gb_redact_url "$remote_url")' in a browser."
|
|
106
106
|
return 1
|
|
107
107
|
fi
|
|
108
108
|
|
package/commands/repo.sh
CHANGED
package/commands/reset-repo.sh
CHANGED
|
@@ -68,7 +68,7 @@ EOF_HELP
|
|
|
68
68
|
shift
|
|
69
69
|
;;
|
|
70
70
|
-y|--yes)
|
|
71
|
-
|
|
71
|
+
GITBASH_ASSUME_YES=1
|
|
72
72
|
shift
|
|
73
73
|
;;
|
|
74
74
|
*)
|
|
@@ -100,7 +100,7 @@ EOF_HELP
|
|
|
100
100
|
return 1
|
|
101
101
|
fi
|
|
102
102
|
|
|
103
|
-
local remote target
|
|
103
|
+
local remote target target_ref
|
|
104
104
|
remote=$(gb_remote)
|
|
105
105
|
if ! git remote get-url "$remote" >/dev/null 2>&1; then
|
|
106
106
|
print_error "No remote '$remote' configured."
|
|
@@ -111,10 +111,13 @@ EOF_HELP
|
|
|
111
111
|
print_error "Failed to fetch '$remote'."
|
|
112
112
|
return 1
|
|
113
113
|
fi
|
|
114
|
+
# Full ref names for git (a tag called <remote>/<branch> must not win), short ones for messages
|
|
114
115
|
if git show-ref --verify --quiet "refs/remotes/$remote/$branch"; then
|
|
115
116
|
target="$remote/$branch"
|
|
117
|
+
target_ref="refs/remotes/$remote/$branch"
|
|
116
118
|
else
|
|
117
119
|
target="$branch"
|
|
120
|
+
target_ref="refs/heads/$branch"
|
|
118
121
|
print_warning "'$branch' is not on '$remote': keeping its commits, only local changes are discarded."
|
|
119
122
|
fi
|
|
120
123
|
|
|
@@ -130,14 +133,14 @@ EOF_HELP
|
|
|
130
133
|
|
|
131
134
|
# What would be lost
|
|
132
135
|
local lost_commits changes untracked
|
|
133
|
-
lost_commits=$(git log --oneline "$
|
|
136
|
+
lost_commits=$(git log --oneline "$target_ref..refs/heads/$branch")
|
|
134
137
|
changes=$(git status --porcelain --untracked-files=no)
|
|
135
138
|
untracked=$(git clean -n "${clean_args[@]}")
|
|
136
139
|
|
|
137
140
|
local head_commit
|
|
138
141
|
head_commit=$(git rev-parse "refs/heads/$branch")
|
|
139
142
|
if [[ -z "$lost_commits" && -z "$changes" && -z "$untracked" &&
|
|
140
|
-
"$(git rev-parse HEAD)" == "$(git rev-parse "$
|
|
143
|
+
"$(git rev-parse HEAD)" == "$(git rev-parse "$target_ref")" ]]; then
|
|
141
144
|
_reset_repo_abort_operations
|
|
142
145
|
print_success "'$branch' is already like a fresh clone of '$target'."
|
|
143
146
|
return 0
|
|
@@ -147,17 +150,17 @@ EOF_HELP
|
|
|
147
150
|
if [[ -n "$lost_commits" ]]; then
|
|
148
151
|
echo
|
|
149
152
|
print_warning "$(printf '%s\n' "$lost_commits" | wc -l | tr -d ' ') unpushed commit(s) will be removed from '$branch':"
|
|
150
|
-
printf '%s\n' "$lost_commits" | sed 's/^/ /'
|
|
153
|
+
printf '%s\n' "$lost_commits" | gb_sanitize | sed 's/^/ /'
|
|
151
154
|
fi
|
|
152
155
|
if [[ -n "$changes" ]]; then
|
|
153
156
|
echo
|
|
154
157
|
echo "Local changes to discard:"
|
|
155
|
-
printf '%s\n' "$changes" | sed 's/^/ /'
|
|
158
|
+
printf '%s\n' "$changes" | gb_sanitize | sed 's/^/ /'
|
|
156
159
|
fi
|
|
157
160
|
if [[ -n "$untracked" ]]; then
|
|
158
161
|
echo
|
|
159
162
|
echo "Untracked and ignored files to delete:"
|
|
160
|
-
printf '%s\n' "$untracked" | sed 's/^Would remove / /'
|
|
163
|
+
printf '%s\n' "$untracked" | gb_sanitize | sed 's/^Would remove / /'
|
|
161
164
|
fi
|
|
162
165
|
if [[ ${#keep[@]} -gt 1 ]]; then
|
|
163
166
|
echo
|
|
@@ -180,7 +183,7 @@ EOF_HELP
|
|
|
180
183
|
print_error "Failed to check out '$branch'."
|
|
181
184
|
return 1
|
|
182
185
|
fi
|
|
183
|
-
if ! git reset --quiet --hard "$
|
|
186
|
+
if ! git reset --quiet --hard "$target_ref"; then
|
|
184
187
|
print_error "Failed to reset '$branch' to '$target'."
|
|
185
188
|
return 1
|
|
186
189
|
fi
|
|
@@ -189,7 +192,9 @@ EOF_HELP
|
|
|
189
192
|
return 1
|
|
190
193
|
fi
|
|
191
194
|
if [[ -f .gitmodules ]]; then
|
|
192
|
-
|
|
195
|
+
# Only submodules that were initialized: --init would clone every URL in
|
|
196
|
+
# the repository's .gitmodules, which a fresh clone doesn't do either
|
|
197
|
+
git submodule update --recursive --force --quiet ||
|
|
193
198
|
print_warning "Failed to update submodules."
|
|
194
199
|
fi
|
|
195
200
|
|
package/commands/stale.sh
CHANGED
|
@@ -172,7 +172,7 @@ EOF
|
|
|
172
172
|
stale_list+="$row"
|
|
173
173
|
fi
|
|
174
174
|
done < <(git for-each-ref --sort=committerdate \
|
|
175
|
-
--format="%(refname:
|
|
175
|
+
--format="%(refname:lstrip=2)%1f%(committerdate:unix)%1f%(committerdate:relative)%1f%(authoremail)%1f%(authorname)" \
|
|
176
176
|
"refs/remotes/$remote" 2>/dev/null)
|
|
177
177
|
|
|
178
178
|
if [[ "$json_mode" == true ]]; then
|
|
@@ -268,7 +268,7 @@ TOGGLE_EOF
|
|
|
268
268
|
|
|
269
269
|
echo
|
|
270
270
|
echo "Selected branches to delete from '$remote':"
|
|
271
|
-
printf ' - %s\n' "${branches_to_delete[@]}"
|
|
271
|
+
printf ' - %s\n' "${branches_to_delete[@]}" | gb_sanitize
|
|
272
272
|
echo
|
|
273
273
|
|
|
274
274
|
if ! gb_confirm --strict "Delete these ${#branches_to_delete[@]} branch(es) from '$remote'? This cannot be undone from here." n; then
|
package/commands/status.sh
CHANGED
|
@@ -177,7 +177,7 @@ EOF
|
|
|
177
177
|
print_warning "$path has a merge conflict - resolve it in your editor, then stage it."
|
|
178
178
|
;;
|
|
179
179
|
?" ")
|
|
180
|
-
echo "Unstaging: $path"
|
|
180
|
+
echo "Unstaging: $path" | gb_sanitize
|
|
181
181
|
if [[ -n "$orig" ]]; then
|
|
182
182
|
_status_unstage "$path" "$orig" || print_error "Failed to unstage $path"
|
|
183
183
|
else
|
|
@@ -185,7 +185,7 @@ EOF
|
|
|
185
185
|
fi
|
|
186
186
|
;;
|
|
187
187
|
*)
|
|
188
|
-
echo "Staging: $path"
|
|
188
|
+
echo "Staging: $path" | gb_sanitize
|
|
189
189
|
git add -A -- "$path" || print_error "Failed to stage $path"
|
|
190
190
|
;;
|
|
191
191
|
esac
|
|
@@ -210,7 +210,7 @@ _status_discard() {
|
|
|
210
210
|
|
|
211
211
|
if [[ ${#untracked[@]} -gt 0 ]]; then
|
|
212
212
|
echo "Untracked files/directories to delete:"
|
|
213
|
-
printf ' - %s\n' "${untracked[@]}"
|
|
213
|
+
printf ' - %s\n' "${untracked[@]}" | gb_sanitize
|
|
214
214
|
if gb_confirm --strict "Delete these ${#untracked[@]} item(s)? This cannot be undone." n; then
|
|
215
215
|
for path in "${untracked[@]}"; do
|
|
216
216
|
if rm -rf -- "$path"; then
|
|
@@ -224,7 +224,7 @@ _status_discard() {
|
|
|
224
224
|
|
|
225
225
|
if [[ ${#added[@]} -gt 0 ]]; then
|
|
226
226
|
echo "Newly added files to unstage:"
|
|
227
|
-
printf ' - %s\n' "${added[@]}"
|
|
227
|
+
printf ' - %s\n' "${added[@]}" | gb_sanitize
|
|
228
228
|
if gb_confirm --strict "Unstage these ${#added[@]} file(s)?" n; then
|
|
229
229
|
local delete_too=false
|
|
230
230
|
gb_confirm --strict "Also delete them from disk?" n && delete_too=true
|
|
@@ -248,7 +248,7 @@ _status_discard() {
|
|
|
248
248
|
return 0
|
|
249
249
|
fi
|
|
250
250
|
echo "Files to restore to the last commit:"
|
|
251
|
-
printf ' - %s\n' "${tracked[@]}"
|
|
251
|
+
printf ' - %s\n' "${tracked[@]}" | gb_sanitize
|
|
252
252
|
if gb_confirm --strict "Discard all changes in these ${#tracked[@]} file(s)? This cannot be undone." n; then
|
|
253
253
|
for path in "${tracked[@]}"; do
|
|
254
254
|
if git restore --source=HEAD --staged --worktree -- "$path" 2>/dev/null ||
|
package/commands/switch.sh
CHANGED
|
@@ -11,10 +11,10 @@ _switch_list_branches() {
|
|
|
11
11
|
current=$(gb_current_branch) || current=""
|
|
12
12
|
if base=$(gb_base_branch 2>/dev/null); then
|
|
13
13
|
base_ref=$(gb_base_ref "$base")
|
|
14
|
-
merged=$(git for-each-ref --merged "$base_ref" --format='%(refname:
|
|
14
|
+
merged=$(git for-each-ref --merged "$base_ref" --format='%(refname:lstrip=2)' refs/heads 2>/dev/null)
|
|
15
15
|
fi
|
|
16
16
|
|
|
17
|
-
local_names=$(git for-each-ref --format='%(refname:
|
|
17
|
+
local_names=$(git for-each-ref --format='%(refname:lstrip=2)' refs/heads)
|
|
18
18
|
while IFS= read -r name; do
|
|
19
19
|
[[ -z "$name" ]] && continue
|
|
20
20
|
if [[ "$name" != "$current" && "$name" != "$base" ]] && printf '%s\n' "$merged" | grep -qxF -- "$name"; then
|
|
@@ -36,7 +36,7 @@ _switch_list_branches() {
|
|
|
36
36
|
printed_spacer=true
|
|
37
37
|
fi
|
|
38
38
|
printf 'remote: %s\tremote\t%s\n' "$name" "$name"
|
|
39
|
-
done < <(git for-each-ref --format='%(refname:
|
|
39
|
+
done < <(git for-each-ref --format='%(refname:lstrip=2)' refs/remotes)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
# Delete the local branch of a list line (called from fzf via 'switch --delete-branch')
|
|
@@ -70,9 +70,11 @@ _switch_delete_branch() {
|
|
|
70
70
|
return 0
|
|
71
71
|
fi
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
local base_ref=HEAD base
|
|
74
|
+
base=$(gb_base_branch 2>/dev/null) && base_ref=$(gb_base_ref "$base")
|
|
75
|
+
count=$(git rev-list --count "refs/heads/$name" --not --remotes "$base_ref" 2>/dev/null || echo "?")
|
|
74
76
|
print_warning "'$name' has $count commit(s) that are not merged or pushed:"
|
|
75
|
-
git log --oneline -n 10 "refs/heads/$name" --not --remotes 2>/dev/null | sed 's/^/ /'
|
|
77
|
+
git log --oneline -n 10 "refs/heads/$name" --not --remotes 2>/dev/null | gb_sanitize | sed 's/^/ /'
|
|
76
78
|
if gb_confirm --strict "Force-delete '$name' and lose these commits?" n; then
|
|
77
79
|
if git branch -D "$name" >/dev/null 2>&1; then
|
|
78
80
|
print_success "Force-deleted $name"
|
|
@@ -246,7 +248,7 @@ EOF
|
|
|
246
248
|
git switch "$local_branch" || { print_error "Failed to switch branch."; return 1; }
|
|
247
249
|
else
|
|
248
250
|
echo "Creating local branch '$local_branch' tracking '$branch_name'"
|
|
249
|
-
git switch -c "$local_branch" --track "
|
|
251
|
+
git switch -c "$local_branch" --track "refs/remotes/$branch_name" || { print_error "Failed to switch branch."; return 1; }
|
|
250
252
|
fi
|
|
251
253
|
fi
|
|
252
254
|
print_success "Switched to branch: $(gb_current_branch)"
|
package/commands/unstash.sh
CHANGED
package/commands/update.sh
CHANGED
|
@@ -57,7 +57,7 @@ EOF
|
|
|
57
57
|
shift
|
|
58
58
|
;;
|
|
59
59
|
-y|--yes)
|
|
60
|
-
|
|
60
|
+
GITBASH_ASSUME_YES=1
|
|
61
61
|
shift
|
|
62
62
|
;;
|
|
63
63
|
*)
|
|
@@ -86,7 +86,7 @@ EOF
|
|
|
86
86
|
# -----------------------------
|
|
87
87
|
if [[ "$current_branch" == "$base_branch" ]]; then
|
|
88
88
|
print_info "Pulling latest '$base_branch' from '$remote'..."
|
|
89
|
-
if git pull --ff-only "$remote" "
|
|
89
|
+
if git pull --ff-only "$remote" "refs/heads/$base_branch"; then
|
|
90
90
|
print_success "'$base_branch' is up to date."
|
|
91
91
|
return 0
|
|
92
92
|
fi
|
|
@@ -143,7 +143,7 @@ EOF
|
|
|
143
143
|
fi
|
|
144
144
|
|
|
145
145
|
print_info "Merging '$remote/$base_branch' into '$current_branch'..."
|
|
146
|
-
if git merge --no-edit "
|
|
146
|
+
if git merge --no-edit "refs/remotes/$remote/$base_branch"; then
|
|
147
147
|
print_success "'$current_branch' is up to date with '$base_branch'."
|
|
148
148
|
else
|
|
149
149
|
local conflicts
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -124,7 +124,7 @@ shellcheck -x bin/gitbash commands/*.sh
|
|
|
124
124
|
./bin/gitbash stale --all
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
Tests live in `test/*.bats`. Each test gets its own temporary repository with a local bare remote (`test/helpers/setup.bash`). fzf is replaced by a stub that records its arguments and answers from a plan (`test/helpers/bin/fzf`); `test/fzf_real.bats` drives the real fzf in a pseudo-terminal. Add a test for every bug fix.
|
|
127
|
+
Tests live in `test/*.bats`. Each test gets its own temporary repository with a local bare remote (`test/helpers/setup.bash`). fzf is replaced by a stub that records its arguments and answers from a plan (`test/helpers/bin/fzf`); `test/fzf_real.bats` drives the real fzf in a pseudo-terminal. Add a test for every bug fix. Write `[[ ... ]]` checks that are not the last line of a test as `[[ ... ]] || false`: bats running on bash 3.2 ignores a failing `[[ ]]` otherwise.
|
|
128
128
|
|
|
129
129
|
CI runs shellcheck and the tests on Ubuntu (bash 5), macOS (bash 3.2 and 5) and Windows (Git Bash). Scripts must keep LF line endings (`.gitattributes` enforces it).
|
|
130
130
|
|
package/docs/SECURITY.md
CHANGED
|
@@ -18,7 +18,9 @@ Versions before 2.0 executed repository `.gitbashrc` files as shell code, so ope
|
|
|
18
18
|
## Trust Model
|
|
19
19
|
|
|
20
20
|
- Configuration files (`~/.gitbashrc`, a repository's `.gitbashrc` and `.gitbashrc-user`) are **parsed, never executed**. Only plain `GITBASH_*="value"` lines from a fixed list of settings are read; values containing `$`, backticks, backslashes or quotes are rejected.
|
|
21
|
-
- A repository's committed `.gitbashrc` is treated as untrusted: it cannot set `GITBASH_MERGE_COMMAND` (the only setting that names a program to run). That setting is only read from `~/.gitbashrc` and `.gitbashrc-user`.
|
|
21
|
+
- A repository's committed `.gitbashrc` is treated as untrusted: it cannot set `GITBASH_MERGE_COMMAND` (the only setting that names a program to run). That setting is only read from `~/.gitbashrc` and an uncommitted `.gitbashrc-user`. A `.gitbashrc-user` that is tracked by git came with the repository and is treated like `.gitbashrc` (before 3.1.0 it was trusted, so a repository could choose the merge tool that `update` runs on conflicts).
|
|
22
|
+
- A repository's `.gitbashrc` can add to `GITBASH_PROTECTED_BRANCHES`, but not remove branches that your own settings (or the default) protect.
|
|
23
|
+
- A repository also cannot turn off update checks: `GITBASH_NO_UPDATE_CHECKS` is ignored in the committed `.gitbashrc` (and in a committed `.gitbashrc-user`).
|
|
22
24
|
- Settings that reach git as arguments are validated: `GITBASH_BASE_BRANCH` (and a base branch taken from the remote's `HEAD`) must be a valid branch name that does not start with `-`, and `GITBASH_REMOTE` must not start with `-`. Before 3.0.0, a committed `.gitbashrc` could set `GITBASH_BASE_BRANCH="--output=<file>"` and make `switch` or `cleanup` overwrite that file.
|
|
23
25
|
- `gitbash --init` prints wrapper functions that call the installed `gitbash` binary; command code is not sourced into your shell.
|
|
24
26
|
- `pr` and `repo` never pass credentials from the remote URL to the browser.
|
|
@@ -28,8 +30,8 @@ Versions before 2.0 executed repository `.gitbashrc` files as shell code, so ope
|
|
|
28
30
|
## Releases
|
|
29
31
|
|
|
30
32
|
- Releases are staged only by the Release workflow in GitHub Actions, with npm trusted publishing (OIDC, no long-lived npm token) and [provenance](https://docs.npmjs.com/generating-provenance-statements). A version goes live only after a maintainer approves it with 2FA ([staged publishing](https://docs.npmjs.com/staged-publishing/)); neither the workflow nor a leaked GitHub token can publish on its own. Check an installed copy with `npm audit signatures`.
|
|
31
|
-
- The install script (`install.sh
|
|
32
|
-
-
|
|
33
|
+
- The install script (`install.sh`) downloads that same npm package and installs it only if it matches the sha512 checksum npm records for the version. It keeps a copy of itself in the installation, and `gitbash --update` runs that copy, so a git tag cannot change what gets installed or run. (Before 3.1.0, `--update` downloaded `install.sh` from the release's git tag and ran it: whoever could create a GitHub release could run code on machines that updated a script install.)
|
|
34
|
+
- `gitbash --update`, the update check and the install script take the latest version from npm's `latest` tag, which only moves when a maintainer approves a staged version. GitHub releases are not used for updates.
|
|
33
35
|
- Third-party actions are pinned to commit SHAs and kept up to date by Dependabot; each workflow job gets only the permissions it needs.
|
|
34
36
|
|
|
35
37
|
---
|
package/install.sh
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# gitbash installer - installs gitbash without npm or node.
|
|
3
|
+
#
|
|
4
|
+
# Downloads the package published on npm (released only from GitHub Actions,
|
|
5
|
+
# with provenance, after approval with 2FA) and checks it against the sha512
|
|
6
|
+
# integrity npm records for that version before installing anything.
|
|
7
|
+
#
|
|
8
|
+
# curl -fsSL https://raw.githubusercontent.com/jaggli/gitbash/main/install.sh | sh
|
|
9
|
+
# wget -qO- https://raw.githubusercontent.com/jaggli/gitbash/main/install.sh | sh
|
|
10
|
+
#
|
|
11
|
+
# Environment:
|
|
12
|
+
# GITBASH_VERSION version to install, e.g. 3.0.0 or v3.0.0 (default: latest)
|
|
13
|
+
# GITBASH_INSTALL_DIR where the files go (default: ${XDG_DATA_HOME:-~/.local/share}/gitbash)
|
|
14
|
+
# GITBASH_BIN_DIR where the gitbash symlink goes (default: ~/.local/bin)
|
|
15
|
+
#
|
|
16
|
+
# Works in Git Bash on Windows, where a small launcher script replaces the symlink.
|
|
17
|
+
#
|
|
18
|
+
# Run it again, or 'gitbash --update' (which runs the installed copy of this
|
|
19
|
+
# script), to upgrade.
|
|
20
|
+
set -eu
|
|
21
|
+
|
|
22
|
+
REGISTRY="https://registry.npmjs.org/gitbash"
|
|
23
|
+
INSTALL_DIR="${GITBASH_INSTALL_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/gitbash}"
|
|
24
|
+
BIN_DIR="${GITBASH_BIN_DIR:-$HOME/.local/bin}"
|
|
25
|
+
|
|
26
|
+
say() { printf '%s\n' "$*"; }
|
|
27
|
+
die() { printf 'gitbash install: %s\n' "$*" >&2; exit 1; }
|
|
28
|
+
|
|
29
|
+
if command -v curl >/dev/null 2>&1; then
|
|
30
|
+
fetch() { curl -fsSL "$1"; }
|
|
31
|
+
fetch_to() { curl -fsSL -o "$2" "$1"; }
|
|
32
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
33
|
+
fetch() { wget -qO- "$1"; }
|
|
34
|
+
fetch_to() { wget -qO "$2" "$1"; }
|
|
35
|
+
else
|
|
36
|
+
die "curl or wget is required"
|
|
37
|
+
fi
|
|
38
|
+
command -v tar >/dev/null 2>&1 || die "tar is required"
|
|
39
|
+
command -v bash >/dev/null 2>&1 || die "bash is required to run gitbash"
|
|
40
|
+
command -v git >/dev/null 2>&1 || say "Warning: git is not installed; gitbash needs git >= 2.23."
|
|
41
|
+
|
|
42
|
+
# SHA-512 of a file, in hex
|
|
43
|
+
sha512_hex() {
|
|
44
|
+
if command -v sha512sum >/dev/null 2>&1; then
|
|
45
|
+
sha512sum "$1" | cut -d ' ' -f 1
|
|
46
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
47
|
+
shasum -a 512 "$1" | cut -d ' ' -f 1
|
|
48
|
+
elif command -v openssl >/dev/null 2>&1; then
|
|
49
|
+
openssl dgst -sha512 "$1" | sed 's/.*= *//'
|
|
50
|
+
else
|
|
51
|
+
return 1
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Base64 (npm's integrity format) to hex
|
|
56
|
+
base64_to_hex() {
|
|
57
|
+
{ printf '%s' "$1" | base64 -d 2>/dev/null ||
|
|
58
|
+
printf '%s' "$1" | base64 -D 2>/dev/null ||
|
|
59
|
+
printf '%s' "$1" | openssl base64 -d -A 2>/dev/null; } | od -An -v -tx1 | tr -d ' \n'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# A string field of npm's (single-line) version document
|
|
63
|
+
json_field() {
|
|
64
|
+
# shellcheck disable=SC2020 # each of , { } becomes a newline
|
|
65
|
+
printf '%s' "$1" | tr ',{}' '\n\n\n' | sed -n "s/^\"$2\":\"\([^\"]*\)\"\$/\1/p" | head -n 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Returns 0 for a plain version number like 3.0.0 (digits and exactly two dots)
|
|
69
|
+
valid_version() {
|
|
70
|
+
case "$1" in
|
|
71
|
+
*[!0-9.]*|.*|*.|*..*|*.*.*.*) return 1 ;;
|
|
72
|
+
*.*.*) return 0 ;;
|
|
73
|
+
esac
|
|
74
|
+
return 1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Resolve the version, then read its tarball URL and checksum from npm
|
|
78
|
+
version="${GITBASH_VERSION:-latest}"
|
|
79
|
+
version="${version#v}"
|
|
80
|
+
if [ "$version" != latest ] && ! valid_version "$version"; then
|
|
81
|
+
die "invalid GITBASH_VERSION '$version' (use e.g. 3.0.0)"
|
|
82
|
+
fi
|
|
83
|
+
if [ "$version" = latest ]; then
|
|
84
|
+
# Not the version document's "version": its npm scripts have one too
|
|
85
|
+
tags="$(fetch "https://registry.npmjs.org/-/package/gitbash/dist-tags")" ||
|
|
86
|
+
die "could not look up the latest version on npm (set GITBASH_VERSION to pick one)"
|
|
87
|
+
version="$(json_field "$tags" latest)"
|
|
88
|
+
valid_version "$version" ||
|
|
89
|
+
die "could not look up the latest version on npm (set GITBASH_VERSION to pick one)"
|
|
90
|
+
fi
|
|
91
|
+
meta="$(fetch "$REGISTRY/$version")" || die "could not find gitbash $version on npm"
|
|
92
|
+
tarball="$(json_field "$meta" tarball)"
|
|
93
|
+
integrity="$(json_field "$meta" integrity)"
|
|
94
|
+
case "$tarball" in
|
|
95
|
+
"$REGISTRY/-/gitbash-$version.tgz") ;;
|
|
96
|
+
*) die "unexpected package location '$tarball'" ;;
|
|
97
|
+
esac
|
|
98
|
+
case "$integrity" in
|
|
99
|
+
sha512-?*) ;;
|
|
100
|
+
*) die "npm has no sha512 checksum for gitbash $version" ;;
|
|
101
|
+
esac
|
|
102
|
+
|
|
103
|
+
# Refuse to replace a directory that is not a gitbash install
|
|
104
|
+
if [ -e "$INSTALL_DIR" ] && [ ! -f "$INSTALL_DIR/bin/gitbash" ]; then
|
|
105
|
+
die "$INSTALL_DIR exists and is not a gitbash install; set GITBASH_INSTALL_DIR to another location"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
tmp="$(mktemp -d 2>/dev/null || mktemp -d -t gitbash)"
|
|
109
|
+
trap 'rm -rf "$tmp"' EXIT INT TERM
|
|
110
|
+
|
|
111
|
+
say "Downloading gitbash $version..."
|
|
112
|
+
fetch_to "$tarball" "$tmp/gitbash.tgz" || die "download failed"
|
|
113
|
+
|
|
114
|
+
# Nothing is unpacked before the checksum matches
|
|
115
|
+
expected="$(base64_to_hex "${integrity#sha512-}")"
|
|
116
|
+
actual="$(sha512_hex "$tmp/gitbash.tgz")" || die "sha512sum, shasum or openssl is required to verify the download"
|
|
117
|
+
if [ -z "$expected" ] || [ "$actual" != "$expected" ]; then
|
|
118
|
+
die "checksum mismatch for gitbash $version: the download was not installed"
|
|
119
|
+
fi
|
|
120
|
+
say "Verified sha512 checksum."
|
|
121
|
+
|
|
122
|
+
mkdir "$tmp/src"
|
|
123
|
+
tar -xzf "$tmp/gitbash.tgz" -C "$tmp/src"
|
|
124
|
+
src="$tmp/src/package"
|
|
125
|
+
[ -f "$src/bin/gitbash" ] || die "unexpected package layout"
|
|
126
|
+
|
|
127
|
+
# Copy only what gitbash needs at runtime
|
|
128
|
+
mkdir "$tmp/install"
|
|
129
|
+
cp -R "$src/bin" "$src/commands" "$src/package.json" "$tmp/install/"
|
|
130
|
+
[ -f "$src/LICENSE" ] && cp "$src/LICENSE" "$tmp/install/"
|
|
131
|
+
# This version's verified installer: 'gitbash --update' runs it from here
|
|
132
|
+
[ -f "$src/install.sh" ] && cp "$src/install.sh" "$tmp/install/"
|
|
133
|
+
chmod +x "$tmp/install/bin/gitbash"
|
|
134
|
+
# Tells 'gitbash --update' how gitbash was installed
|
|
135
|
+
printf 'method=script\nbin_dir=%s\n' "$BIN_DIR" > "$tmp/install/.gitbash-install"
|
|
136
|
+
|
|
137
|
+
mkdir -p "$(dirname "$INSTALL_DIR")" "$BIN_DIR"
|
|
138
|
+
rm -rf "$INSTALL_DIR"
|
|
139
|
+
mv "$tmp/install" "$INSTALL_DIR"
|
|
140
|
+
rm -f "$BIN_DIR/gitbash"
|
|
141
|
+
if ln -s "$INSTALL_DIR/bin/gitbash" "$BIN_DIR/gitbash" 2>/dev/null && [ -L "$BIN_DIR/gitbash" ]; then
|
|
142
|
+
linked="Linked $BIN_DIR/gitbash"
|
|
143
|
+
else
|
|
144
|
+
# No symlinks (Git Bash on Windows copies the file instead): a launcher script
|
|
145
|
+
rm -f "$BIN_DIR/gitbash"
|
|
146
|
+
target="$(printf '%s' "$INSTALL_DIR/bin/gitbash" | sed "s/'/'\\\\''/g")"
|
|
147
|
+
printf '#!/bin/sh\nexec bash '"'"'%s'"'"' "$@"\n' "$target" > "$BIN_DIR/gitbash"
|
|
148
|
+
chmod +x "$BIN_DIR/gitbash"
|
|
149
|
+
linked="Created launcher $BIN_DIR/gitbash"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
say "Installed gitbash $version to $INSTALL_DIR"
|
|
153
|
+
say "$linked"
|
|
154
|
+
|
|
155
|
+
case ":$PATH:" in
|
|
156
|
+
*":$BIN_DIR:"*)
|
|
157
|
+
found="$(command -v gitbash 2>/dev/null || true)"
|
|
158
|
+
if [ -n "$found" ] && [ "$found" != "$BIN_DIR/gitbash" ]; then
|
|
159
|
+
say ""
|
|
160
|
+
say "Note: 'gitbash' currently resolves to $found, which comes before $BIN_DIR in PATH."
|
|
161
|
+
fi
|
|
162
|
+
;;
|
|
163
|
+
*)
|
|
164
|
+
say ""
|
|
165
|
+
say "$BIN_DIR is not in your PATH. Add this to your .zshrc or .bashrc:"
|
|
166
|
+
say " export PATH=\"$BIN_DIR:\$PATH\""
|
|
167
|
+
;;
|
|
168
|
+
esac
|
|
169
|
+
|
|
170
|
+
say ""
|
|
171
|
+
say "Next: run 'gitbash --config', and see the README for shell integration."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitbash",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Opinionated git utilities for bash with fzf-powered menus",
|
|
5
5
|
"author": "jaggli",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"bin/",
|
|
31
31
|
"commands/",
|
|
32
32
|
"docs/",
|
|
33
|
+
"install.sh",
|
|
33
34
|
"README.md",
|
|
34
35
|
"LICENSE"
|
|
35
36
|
],
|