gitbash 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Source common utilities
4
+ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ # shellcheck source=_utils.sh
6
+ source "$SOURCE_DIR/_utils.sh"
7
+
8
+ # Abort an unfinished merge, rebase, cherry-pick, revert or am
9
+ _reset_repo_abort_operations() {
10
+ local path
11
+ if [[ -d "$(git rev-parse --git-path rebase-merge)" || -d "$(git rev-parse --git-path rebase-apply)" ]]; then
12
+ path=$(git rev-parse --git-path rebase-apply)
13
+ if [[ -f "$path/applying" ]]; then
14
+ git am --abort >/dev/null 2>&1
15
+ else
16
+ git rebase --abort >/dev/null 2>&1
17
+ fi
18
+ fi
19
+ [[ -f "$(git rev-parse --git-path MERGE_HEAD)" ]] && git merge --abort >/dev/null 2>&1
20
+ [[ -f "$(git rev-parse --git-path CHERRY_PICK_HEAD)" ]] && git cherry-pick --abort >/dev/null 2>&1
21
+ [[ -f "$(git rev-parse --git-path REVERT_HEAD)" ]] && git revert --abort >/dev/null 2>&1
22
+ return 0
23
+ }
24
+
25
+ # Reset the current branch to its state on the remote and delete all
26
+ # untracked and ignored files, like a fresh clone
27
+ reset-repo() {
28
+ local dry_run=false
29
+
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
32
+ -v|--version)
33
+ echo "gitbash ${FUNCNAME[0]} v$VERSION"
34
+ return 0
35
+ ;;
36
+ -h|--help)
37
+ gb_help << 'EOF_HELP'
38
+ Usage: reset-repo [OPTIONS]
39
+
40
+ Reset the current branch like a fresh clone: discard all local changes and
41
+ unpushed commits, and delete untracked and ignored files (build output,
42
+ node_modules, ...).
43
+
44
+ Options:
45
+ -n, --dry-run Only show what would be reset and deleted
46
+ -y, --yes Don't ask for confirmation
47
+ -h, --help Show this help message
48
+
49
+ Behavior:
50
+ - Fetches the configured remote (GITBASH_REMOTE, default: origin) and resets
51
+ the branch to <remote>/<branch>; a branch that is not on the remote keeps
52
+ its commits and only loses the local changes
53
+ - Aborts an unfinished merge, rebase, cherry-pick or revert first
54
+ - Keeps files matching GITBASH_RESET_KEEP (space-separated .gitignore
55
+ patterns, e.g. '.env .idea/'), .gitbashrc-user and nested repositories
56
+ - Stashes and other branches are not touched
57
+ - Commits that are lost can be recovered with 'git reflog' for a while
58
+
59
+ Examples:
60
+ $ reset-repo --dry-run
61
+ $ reset-repo
62
+
63
+ EOF_HELP
64
+ return 0
65
+ ;;
66
+ -n|--dry-run)
67
+ dry_run=true
68
+ shift
69
+ ;;
70
+ -y|--yes)
71
+ export GITBASH_ASSUME_YES=1
72
+ shift
73
+ ;;
74
+ *)
75
+ print_error "Unknown argument: $1"
76
+ echo "Usage: reset-repo [--dry-run] [--yes]" >&2
77
+ return 1
78
+ ;;
79
+ esac
80
+ done
81
+
82
+ require_git_repo || return 1
83
+
84
+ local repo_root
85
+ repo_root=$(git rev-parse --show-toplevel) || return 1
86
+ cd "$repo_root" || return 1
87
+
88
+ # During a rebase HEAD is detached; the branch is the one being rebased
89
+ local branch="" head_name
90
+ for head_name in rebase-merge/head-name rebase-apply/head-name; do
91
+ head_name=$(git rev-parse --git-path "$head_name")
92
+ if [[ -f "$head_name" ]]; then
93
+ branch=$(cat "$head_name")
94
+ branch="${branch#refs/heads/}"
95
+ break
96
+ fi
97
+ done
98
+ if [[ -z "$branch" ]] && ! branch=$(gb_current_branch); then
99
+ print_error "Detached HEAD: check out a branch first."
100
+ return 1
101
+ fi
102
+
103
+ local remote target
104
+ remote=$(gb_remote)
105
+ if ! git remote get-url "$remote" >/dev/null 2>&1; then
106
+ print_error "No remote '$remote' configured."
107
+ return 1
108
+ fi
109
+ print_info "Fetching '$remote'..."
110
+ if ! git fetch --quiet --prune "$remote"; then
111
+ print_error "Failed to fetch '$remote'."
112
+ return 1
113
+ fi
114
+ if git show-ref --verify --quiet "refs/remotes/$remote/$branch"; then
115
+ target="$remote/$branch"
116
+ else
117
+ target="$branch"
118
+ print_warning "'$branch' is not on '$remote': keeping its commits, only local changes are discarded."
119
+ fi
120
+
121
+ # Patterns of files to keep (git clean -e); .gitbashrc-user is always kept
122
+ local -a keep clean_args
123
+ local pattern
124
+ IFS=' ' read -r -a keep <<< "${GITBASH_RESET_KEEP:-}"
125
+ keep+=(".gitbashrc-user")
126
+ clean_args=(-d -x)
127
+ for pattern in "${keep[@]}"; do
128
+ [[ -n "$pattern" ]] && clean_args+=(-e "$pattern")
129
+ done
130
+
131
+ # What would be lost
132
+ local lost_commits changes untracked
133
+ lost_commits=$(git log --oneline "$target..refs/heads/$branch")
134
+ changes=$(git status --porcelain --untracked-files=no)
135
+ untracked=$(git clean -n "${clean_args[@]}")
136
+
137
+ local head_commit
138
+ head_commit=$(git rev-parse "refs/heads/$branch")
139
+ if [[ -z "$lost_commits" && -z "$changes" && -z "$untracked" &&
140
+ "$(git rev-parse HEAD)" == "$(git rev-parse "$target")" ]]; then
141
+ _reset_repo_abort_operations
142
+ print_success "'$branch' is already like a fresh clone of '$target'."
143
+ return 0
144
+ fi
145
+
146
+ echo "Resetting '$branch' to '$target':"
147
+ if [[ -n "$lost_commits" ]]; then
148
+ echo
149
+ 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/^/ /'
151
+ fi
152
+ if [[ -n "$changes" ]]; then
153
+ echo
154
+ echo "Local changes to discard:"
155
+ printf '%s\n' "$changes" | sed 's/^/ /'
156
+ fi
157
+ if [[ -n "$untracked" ]]; then
158
+ echo
159
+ echo "Untracked and ignored files to delete:"
160
+ printf '%s\n' "$untracked" | sed 's/^Would remove / /'
161
+ fi
162
+ if [[ ${#keep[@]} -gt 1 ]]; then
163
+ echo
164
+ echo "Kept (GITBASH_RESET_KEEP): ${GITBASH_RESET_KEEP}"
165
+ fi
166
+ echo
167
+
168
+ if [[ "$dry_run" == true ]]; then
169
+ print_info "Dry run - nothing was changed."
170
+ return 0
171
+ fi
172
+
173
+ if ! gb_confirm "Reset '$branch' and delete these files?" n; then
174
+ print_info "Aborted - nothing was changed."
175
+ return 1
176
+ fi
177
+
178
+ _reset_repo_abort_operations
179
+ if [[ "$(gb_current_branch)" != "$branch" ]] && ! git switch --quiet --force "$branch"; then
180
+ print_error "Failed to check out '$branch'."
181
+ return 1
182
+ fi
183
+ if ! git reset --quiet --hard "$target"; then
184
+ print_error "Failed to reset '$branch' to '$target'."
185
+ return 1
186
+ fi
187
+ if ! git clean -q -f "${clean_args[@]}"; then
188
+ print_error "Failed to delete untracked files."
189
+ return 1
190
+ fi
191
+ if [[ -f .gitmodules ]]; then
192
+ git submodule update --init --recursive --force --quiet ||
193
+ print_warning "Failed to update submodules."
194
+ fi
195
+
196
+ print_success "'$branch' is now like a fresh clone of '$target'."
197
+ if [[ -n "$lost_commits" ]]; then
198
+ print_info "Removed commits can be recovered with 'git reflog' (previous HEAD: ${head_commit:0:12})."
199
+ fi
200
+ }
package/commands/stale.sh CHANGED
@@ -28,7 +28,7 @@ stale() {
28
28
  return 0
29
29
  ;;
30
30
  -h|--help)
31
- cat << 'EOF'
31
+ gb_help << 'EOF'
32
32
  Usage: stale [OPTIONS] [FILTER...]
33
33
 
34
34
  Show remote branches ordered by last commit date (oldest first) and delete
@@ -63,7 +63,7 @@ Examples:
63
63
  $ stale --json
64
64
 
65
65
  Output format:
66
- <branch-name> <relative-date> <last committer>
66
+ <branch-name> <relative-date> <last author>
67
67
 
68
68
  Requirements:
69
69
  - Must be in a git repository
@@ -167,7 +167,7 @@ EOF
167
167
  stale_list+="$row"
168
168
  fi
169
169
  done < <(git for-each-ref --sort=committerdate \
170
- --format="%(refname:short)%1f%(committerdate:unix)%1f%(committerdate:relative)%1f%(committeremail)%1f%(committername)" \
170
+ --format="%(refname:short)%1f%(committerdate:unix)%1f%(committerdate:relative)%1f%(authoremail)%1f%(authorname)" \
171
171
  "refs/remotes/$remote" 2>/dev/null)
172
172
 
173
173
  if [[ "$json_mode" == true ]]; then
@@ -229,7 +229,7 @@ TOGGLE_EOF
229
229
  echo \"Branch: $remote_q/\$branch\"
230
230
  echo
231
231
  echo 'Recent commits:'
232
- git log --oneline --color=always -n 15 \"refs/remotes/$remote_q/\$branch\" 2>/dev/null || echo 'No commits found'
232
+ git log --oneline --color=\$GB_COLOR -n 15 \"refs/remotes/$remote_q/\$branch\" 2>/dev/null || echo 'No commits found'
233
233
  " \
234
234
  --preview-window=right:35% \
235
235
  < "$tmp_dir/$(cat "$tmp_dir/state")"
package/commands/stash.sh CHANGED
@@ -11,7 +11,7 @@ stash() {
11
11
  return 0
12
12
  fi
13
13
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
14
- cat << 'EOF'
14
+ gb_help << 'EOF'
15
15
  Usage: stash [NAME...]
16
16
 
17
17
  Create a git stash with a descriptive name.
@@ -14,7 +14,7 @@ stashes() {
14
14
  return 0
15
15
  fi
16
16
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
17
- cat << 'EOF'
17
+ gb_help << 'EOF'
18
18
  Usage: stashes [OPTIONS]
19
19
 
20
20
  Interactive menu to manage git stashes.
@@ -87,7 +87,7 @@ Examples:
87
87
  Actions:
88
88
  💾 Stash - Create a new stash with all changes
89
89
  📦 Unstash - Apply stash to working directory, optionally drop it
90
- 🗑️ Clean up - Delete stashes without applying (supports multi-select)
90
+ 🗑️ Clean up - Delete stashes without applying (supports multi-select)
91
91
 
92
92
  Requirements:
93
93
  - fzf (fuzzy finder)
@@ -45,7 +45,7 @@ status() {
45
45
  return 0
46
46
  fi
47
47
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
48
- cat << 'EOF'
48
+ gb_help << 'EOF'
49
49
  Usage: status [OPTIONS]
50
50
 
51
51
  Interactive git status: stage, unstage, discard and commit with diff previews.
@@ -92,8 +92,8 @@ EOF
92
92
  echo "=== UNTRACKED ==="; echo
93
93
  if [[ -d "$file" ]]; then ls -la "$file"; else __BAT__ "$file" 2>/dev/null || echo "Cannot preview file"; fi
94
94
  else
95
- staged=$(git diff --cached --color=always -- "$file" 2>/dev/null)
96
- unstaged=$(git diff --color=always -- "$file" 2>/dev/null)
95
+ staged=$(git diff --cached --color=$GB_COLOR -- "$file" 2>/dev/null)
96
+ unstaged=$(git diff --color=$GB_COLOR -- "$file" 2>/dev/null)
97
97
  if [[ -n "$staged" ]]; then echo "=== STAGED CHANGES ==="; echo; printf "%s\n" "$staged" | __PAGER__; fi
98
98
  if [[ -n "$unstaged" ]]; then
99
99
  [[ -n "$staged" ]] && echo
@@ -100,14 +100,15 @@ switch() {
100
100
  return 0
101
101
  ;;
102
102
  -h|--help)
103
- cat << 'EOF'
103
+ gb_help << 'EOF'
104
104
  Usage: switch [FILTER...]
105
105
 
106
106
  Select a git branch using fzf and switch to it.
107
107
 
108
108
  Arguments:
109
109
  FILTER... Optional search words to pre-fill fzf (joined with spaces).
110
- If exactly one branch name contains them, switches directly.
110
+ Switches directly on an exact branch name, or if exactly
111
+ one branch name contains them.
111
112
 
112
113
  Options:
113
114
  -h, --help Show this help message
@@ -152,10 +153,19 @@ EOF
152
153
  fi
153
154
 
154
155
  # -----------------------------
155
- # Single match: switch directly
156
+ # Exact or single match: switch directly
156
157
  # -----------------------------
157
158
  local selected="" query="$filter"
158
159
  if [[ -n "$filter" ]]; then
160
+ # An exact branch name wins even if other branches contain it too
161
+ # (local first, then a remote branch like "origin/<filter>")
162
+ selected=$(printf '%s\n' "$branch_list" | awk -F'\t' -v f="$filter" '$2 == "local" && $3 == f { print; exit }')
163
+ if [[ -z "$selected" ]]; then
164
+ selected=$(printf '%s\n' "$branch_list" | awk -F'\t' -v f="$filter" '$2 == "remote" && ($3 == f || substr($3, index($3, "/") + 1) == f) { print; exit }')
165
+ fi
166
+ [[ -n "$selected" ]] && print_info "Exact match found, switching directly..."
167
+ fi
168
+ if [[ -n "$filter" && -z "$selected" ]]; then
159
169
  local matches match_count
160
170
  matches=$(printf '%s\n' "$branch_list" | awk -F'\t' '$2 != "spacer"' |
161
171
  while IFS= read -r line; do
@@ -192,9 +202,9 @@ EOF
192
202
  remote) ref=\"refs/remotes/\$name\" ;;
193
203
  *) echo 'Spacer - not selectable'; exit 0 ;;
194
204
  esac
195
- git log --color=always -n 1 --format='%C(bold cyan)Author:%C(reset) %an%n%C(bold cyan)Date:%C(reset) %ar (%ad)%n%C(bold cyan)Message:%C(reset) %s%n' --date=format:'%Y-%m-%d %H:%M' \"\$ref\" 2>/dev/null
205
+ git log --color=\$GB_COLOR -n 1 --format='%C(bold cyan)Author:%C(reset) %an%n%C(bold cyan)Date:%C(reset) %ar (%ad)%n%C(bold cyan)Message:%C(reset) %s%n' --date=format:'%Y-%m-%d %H:%M' \"\$ref\" 2>/dev/null
196
206
  echo
197
- git log --oneline --color=always -n 10 \"\$ref\" 2>/dev/null
207
+ git log --oneline --color=\$GB_COLOR -n 10 \"\$ref\" 2>/dev/null
198
208
  " \
199
209
  --preview-window=right:50% \
200
210
  --header="[Enter] switch | [Del] delete local branch | [Esc] exit | Current: $current_branch" \
@@ -232,25 +242,6 @@ EOF
232
242
  fi
233
243
  print_success "Switched to branch: $(gb_current_branch)"
234
244
 
235
- # -----------------------------
236
245
  # Offer to fast-forward if behind the upstream
237
- # -----------------------------
238
- local now upstream_remote upstream_merge behind
239
- now=$(gb_current_branch) || return 0
240
- upstream_remote=$(git config "branch.$now.remote" 2>/dev/null) || return 0
241
- upstream_merge=$(git config "branch.$now.merge" 2>/dev/null) || return 0
242
- git fetch --quiet "$upstream_remote" "$upstream_merge" 2>/dev/null || return 0
243
- behind=$(git rev-list --count "HEAD..@{upstream}" 2>/dev/null) || return 0
244
- if [[ "$behind" -gt 0 ]]; then
245
- print_warning "Branch is $behind commit(s) behind its upstream."
246
- if gb_confirm "Fast-forward to the latest changes?" y; then
247
- if git merge --ff-only --quiet "@{upstream}"; then
248
- print_success "Up to date."
249
- else
250
- print_error "Could not fast-forward (you have local commits or changes). Run 'git pull' to reconcile."
251
- return 1
252
- fi
253
- fi
254
- fi
255
- return 0
246
+ gb_offer_fast_forward
256
247
  }
@@ -12,8 +12,8 @@ _stash_preview() {
12
12
  printf '%s' '
13
13
  sel=$(printf "%s" {} | grep -o "^stash@{[0-9]*}" || true)
14
14
  if [[ -n "$sel" ]]; then
15
- { git stash show --include-untracked -p --color=always "$sel" 2>/dev/null ||
16
- git stash show -p --color=always "$sel" 2>/dev/null; } | '"$pager"'
15
+ { git stash show --include-untracked -p --color=$GB_COLOR "$sel" 2>/dev/null ||
16
+ git stash show -p --color=$GB_COLOR "$sel" 2>/dev/null; } | '"$pager"'
17
17
  else
18
18
  echo "No preview"
19
19
  fi'
@@ -25,7 +25,7 @@ unstash() {
25
25
  return 0
26
26
  fi
27
27
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
28
- cat << 'EOF'
28
+ gb_help << 'EOF'
29
29
  Usage: unstash [OPTIONS]
30
30
 
31
31
  Apply a git stash to your working directory with optional removal.
@@ -84,7 +84,9 @@ EOF
84
84
  fi
85
85
 
86
86
  echo "Applying $stash_id ..."
87
- if ! git stash apply "$stash_id"; then
87
+ # The restored files are good news: list them in green, not git's red
88
+ if ! git -c color.status.changed=green -c color.status.untracked=green \
89
+ stash apply "$stash_id"; then
88
90
  print_error "Apply failed or had conflicts - the stash was kept."
89
91
  return 1
90
92
  fi
@@ -21,7 +21,7 @@ update() {
21
21
  return 0
22
22
  ;;
23
23
  -h|--help)
24
- cat << 'EOF'
24
+ gb_help << 'EOF'
25
25
  Usage: update [OPTIONS]
26
26
 
27
27
  Merge the latest base branch (main/master) from the remote into the current branch.
@@ -104,6 +104,7 @@ When your PR is merged:
104
104
  3. Changesets are consumed and version is bumped
105
105
  4. CHANGELOG.md is automatically updated
106
106
  5. Changes are published with `npm run release`
107
+ 6. Pushing the `vX.Y.Z` tag triggers the Release workflow, which creates the GitHub release with that version's CHANGELOG.md section as its notes (re-run it for an existing tag via "Run workflow")
107
108
 
108
109
  ## Development Guidelines
109
110
 
package/docs/SECURITY.md CHANGED
@@ -20,7 +20,7 @@ Versions before 2.0 executed repository `.gitbashrc` files as shell code, so ope
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
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`.
22
22
  - `gitbash --init` prints wrapper functions that call the installed `gitbash` binary; command code is not sourced into your shell.
23
- - `pr` never passes credentials from the remote URL to the browser.
23
+ - `pr` and `repo` never pass credentials from the remote URL to the browser.
24
24
 
25
25
  ---
26
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitbash",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Opinionated git utilities for bash with fzf-powered menus",
5
5
  "author": "jaggli",
6
6
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "test": "bats test",
45
45
  "changeset": "changeset",
46
46
  "version": "changeset version",
47
- "release": "npm run version && VERSION=$(node -p \"require('./package.json').version\") && ./bin/gitbash commit --yes -p \"chore: release v$VERSION\" && git tag \"v$VERSION\" && git push origin \"v$VERSION\" && npm publish"
47
+ "release": "scripts/release.sh"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@changesets/cli": "^2.29.8",