gitbash 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,21 +5,28 @@ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  # shellcheck source=_utils.sh
6
6
  source "$SOURCE_DIR/_utils.sh"
7
7
 
8
- # List recent commits and optionally revert selected ones
8
+ # List the current branch's own commits and optionally revert selected ones
9
9
  commits() {
10
10
  if [[ "${1:-}" == "-v" || "${1:-}" == "--version" ]]; then
11
11
  echo "gitbash ${FUNCNAME[0]} v$VERSION"
12
12
  return 0
13
13
  fi
14
14
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
15
- cat << 'EOF'
16
- Usage: commits [COUNT]
15
+ gb_help << 'EOF'
16
+ Usage: commits [-a|--all] [COUNT]
17
17
 
18
- List recent commits in the current branch with option to revert selected ones.
18
+ List the commits made on the current branch with option to revert selected ones.
19
+
20
+ On a feature branch only the commits that are not in the base branch are
21
+ shown. On the base branch its own history is shown (--first-parent), so
22
+ merged branches appear as their merge commit.
19
23
 
20
24
  Arguments:
21
25
  COUNT Number of commits to show (default: 20)
22
26
 
27
+ Options:
28
+ -a, --all Show all recent commits reachable from HEAD
29
+
23
30
  Navigation:
24
31
  ↑/↓ Navigate through commits
25
32
  TAB Select/deselect commit for revert
@@ -27,6 +34,7 @@ Navigation:
27
34
  ESC/Ctrl-C Exit without action
28
35
 
29
36
  Notes:
37
+ - If the upstream has new commits, offers to fast-forward first
30
38
  - Preview shows the full commit diff
31
39
  - Selected commits are reverted newest first, whatever order you picked them in
32
40
  - Merge commits are reverted against their first parent (the branch they were merged into)
@@ -35,6 +43,7 @@ Notes:
35
43
  Examples:
36
44
  $ commits
37
45
  $ commits 50
46
+ $ commits --all
38
47
 
39
48
  Requirements:
40
49
  - Must be in a git repository
@@ -46,16 +55,56 @@ EOF
46
55
 
47
56
  require_git_repo || return 1
48
57
 
49
- local count="${1:-20}"
58
+ local all=false count=""
59
+ while [[ $# -gt 0 ]]; do
60
+ case "$1" in
61
+ -a|--all) all=true ;;
62
+ -*) print_error "Unknown option '$1'. See 'commits --help'."; return 1 ;;
63
+ *)
64
+ if [[ -n "$count" ]]; then
65
+ print_error "Unexpected argument '$1'. See 'commits --help'."
66
+ return 1
67
+ fi
68
+ count="$1"
69
+ ;;
70
+ esac
71
+ shift
72
+ done
73
+ count="${count:-20}"
50
74
  if ! [[ "$count" =~ ^[1-9][0-9]*$ ]]; then
51
75
  print_error "COUNT must be a positive number (got '$count')."
52
76
  return 1
53
77
  fi
54
78
  require_fzf || return 1
55
79
 
80
+ # Offer to pull remote commits first, so they show up (and can be reverted)
81
+ gb_offer_fast_forward || true
82
+
56
83
  local current_branch
57
84
  current_branch=$(gb_current_branch) || current_branch="(detached HEAD)"
58
85
 
86
+ # -----------------------------
87
+ # Which commits: the branch's own (not in the base branch) or, on the
88
+ # base branch, its first-parent history
89
+ # -----------------------------
90
+ local log_args=() scope="last $count commits" empty_hint="" base_branch base_ref
91
+ if [[ "$all" == false ]] && base_branch=$(gb_base_branch 2>/dev/null); then
92
+ base_ref=$(gb_base_ref "$base_branch")
93
+ if [[ "$current_branch" == "$base_branch" ]]; then
94
+ log_args=(--first-parent)
95
+ scope="last $count commits on '$base_branch' (merged branches as merge commits)"
96
+ elif git rev-parse --verify --quiet "$base_ref^{commit}" >/dev/null; then
97
+ log_args=(--not "$base_ref")
98
+ # Also exclude the local base branch when it is ahead of the remote one
99
+ if [[ "$base_ref" != "$base_branch" ]] &&
100
+ git show-ref --verify --quiet "refs/heads/$base_branch"; then
101
+ log_args+=("$base_branch")
102
+ fi
103
+ scope="commits not in '$base_ref' (up to $count)"
104
+ empty_hint="No commits on '$current_branch' that are not in '$base_ref'. Use 'commits --all' to show all recent commits."
105
+ fi
106
+ fi
107
+
59
108
  # -----------------------------
60
109
  # Build commit list (fields separated by \x1f, subject last)
61
110
  # -----------------------------
@@ -67,10 +116,10 @@ EOF
67
116
  fi
68
117
  commit_list+=$(printf "%-10s %-60s %-15s %s" "$hash" "$subject" "$date" "$author")
69
118
  commit_list+=$'\t'"$hash"$'\n'
70
- done < <(git log -n "$count" --format='%h%x1f%cr%x1f%an%x1f%s' 2>/dev/null)
119
+ done < <(git log -n "$count" --format='%h%x1f%cr%x1f%an%x1f%s' HEAD ${log_args[@]+"${log_args[@]}"} 2>/dev/null)
71
120
 
72
121
  if [[ -z "$commit_list" ]]; then
73
- echo "No commits found."
122
+ echo "${empty_hint:-No commits found.}"
74
123
  return 0
75
124
  fi
76
125
 
@@ -78,11 +127,11 @@ EOF
78
127
  pager=$(gb_diff_pager)
79
128
  preview='
80
129
  hash=$(printf "%s" {} | cut -f2)
81
- git show --stat --color=always "$hash" 2>/dev/null
130
+ git show --stat --color=$GB_COLOR "$hash" 2>/dev/null
82
131
  echo
83
132
  echo "─────────────────────────────────────────────────────"
84
133
  echo
85
- git show --color=always --format= "$hash" 2>/dev/null | head -300 | __PAGER__'
134
+ git show --color=$GB_COLOR --format= "$hash" 2>/dev/null | head -300 | __PAGER__'
86
135
  preview="${preview//__PAGER__/$pager}"
87
136
 
88
137
  local selection
@@ -92,7 +141,7 @@ EOF
92
141
  --reverse \
93
142
  --border \
94
143
  --header="[TAB] select for revert | [Enter] revert selected | [ESC] exit
95
- Showing last $count commits" \
144
+ Showing $scope" \
96
145
  --multi \
97
146
  --delimiter=$'\t' \
98
147
  --with-nth=1 \
@@ -51,7 +51,7 @@ create() {
51
51
  return 0
52
52
  ;;
53
53
  -h|--help)
54
- cat << 'EOF'
54
+ gb_help << 'EOF'
55
55
  Usage: create [OPTIONS] [JIRA_LINK] [TITLE...]
56
56
 
57
57
  Create a new git branch from the latest base branch, with optional Jira issue parsing.
package/commands/pr.sh CHANGED
@@ -5,52 +5,6 @@ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  # shellcheck source=_utils.sh
6
6
  source "$SOURCE_DIR/_utils.sh"
7
7
 
8
- # Convert a remote URL to the repository's web URL (credentials and .git removed).
9
- # Handles https://[user[:token]@]host/path, ssh://[user@]host[:port]/path and user@host:path.
10
- _pr_web_base() {
11
- local url="$1" scheme="https" rest host path
12
- case "$url" in
13
- http://*|https://*)
14
- scheme="${url%%://*}"
15
- rest="${url#*://}"
16
- host="${rest%%/*}"
17
- host="${host##*@}"
18
- path="${rest#*/}"
19
- ;;
20
- ssh://*)
21
- rest="${url#ssh://}"
22
- host="${rest%%/*}"
23
- host="${host##*@}"
24
- host="${host%%:*}"
25
- path="${rest#*/}"
26
- ;;
27
- *@*:*)
28
- host="${url%%:*}"
29
- host="${host##*@}"
30
- path="${url#*:}"
31
- ;;
32
- *)
33
- return 1
34
- ;;
35
- esac
36
- path="${path%/}"
37
- path="${path%.git}"
38
- if [[ -z "$host" || -z "$path" || ( -n "$rest" && "$path" == "$rest" ) ]]; then
39
- return 1
40
- fi
41
-
42
- # Azure DevOps SSH: ssh.dev.azure.com:v3/org/project/repo
43
- if [[ "$host" == "ssh.dev.azure.com" && "$path" == v3/*/*/* ]]; then
44
- local org project repo
45
- path="${path#v3/}"
46
- org="${path%%/*}"; path="${path#*/}"
47
- project="${path%%/*}"; repo="${path#*/}"
48
- echo "https://dev.azure.com/$org/$project/_git/$repo"
49
- return 0
50
- fi
51
- echo "$scheme://$host/$path"
52
- }
53
-
54
8
  # Web URL for creating a pull/merge request for a branch
55
9
  _pr_create_url() {
56
10
  local base="$1" branch="$2" encoded
@@ -83,7 +37,7 @@ pr() {
83
37
  return 0
84
38
  ;;
85
39
  -h|--help)
86
- cat << 'EOF'
40
+ gb_help << 'EOF'
87
41
  Usage: pr [OPTIONS]
88
42
 
89
43
  Open the current branch's pull request in your browser.
@@ -105,7 +59,7 @@ Behavior:
105
59
  Bitbucket <repo>/pull-requests/new?source=<branch>
106
60
  Azure DevOps <repo>/pullrequestcreate?sourceRef=<branch>
107
61
  - Works with SSH and HTTPS remotes; credentials in the remote URL are never used
108
- - Opens the browser with 'open' (macOS) or 'xdg-open' (Linux), otherwise prints the URL
62
+ - Opens the browser (macOS, Linux, WSL, Git Bash on Windows), otherwise prints the URL
109
63
 
110
64
  Examples:
111
65
  $ pr
@@ -147,7 +101,7 @@ EOF
147
101
  print_error "Detached HEAD: check out a branch first."
148
102
  return 1
149
103
  fi
150
- if ! base=$(_pr_web_base "$remote_url"); then
104
+ if ! base=$(gb_web_url "$remote_url"); then
151
105
  print_error "Don't know how to open '$remote_url' in a browser."
152
106
  return 1
153
107
  fi
@@ -0,0 +1,82 @@
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
+ # Open the repository's web page
9
+ repo() {
10
+ local print_only=false
11
+
12
+ while [[ $# -gt 0 ]]; do
13
+ case "$1" in
14
+ -v|--version)
15
+ echo "gitbash ${FUNCNAME[0]} v$VERSION"
16
+ return 0
17
+ ;;
18
+ -h|--help)
19
+ gb_help << 'EOF_HELP'
20
+ Usage: repo [OPTIONS]
21
+
22
+ Open the repository's web page in your browser.
23
+
24
+ Options:
25
+ --print Print the URL instead of opening it
26
+ -h, --help Show this help message
27
+
28
+ Behavior:
29
+ - Uses the URL of the configured remote (GITBASH_REMOTE, default: origin)
30
+ - With the GitHub CLI (gh) installed and logged in, opens it with 'gh repo view --web'
31
+ - Otherwise opens the URL derived from the remote
32
+ (GitHub, GitHub Enterprise, GitLab, Bitbucket, Azure DevOps)
33
+ - Works with SSH and HTTPS remotes; credentials in the remote URL are never used
34
+ - Opens the browser (macOS, Linux, WSL, Git Bash on Windows), otherwise prints the URL
35
+
36
+ Examples:
37
+ $ repo
38
+ $ repo --print
39
+
40
+ EOF_HELP
41
+ return 0
42
+ ;;
43
+ --print)
44
+ print_only=true
45
+ shift
46
+ ;;
47
+ *)
48
+ print_error "Unknown argument: $1"
49
+ echo "Usage: repo [--print]" >&2
50
+ return 1
51
+ ;;
52
+ esac
53
+ done
54
+
55
+ require_git_repo || return 1
56
+
57
+ local remote remote_url url
58
+ remote=$(gb_remote)
59
+ if ! remote_url=$(git config --get "remote.$remote.url"); then
60
+ print_error "No remote '$remote' found."
61
+ return 1
62
+ fi
63
+ if ! url=$(gb_web_url "$remote_url"); then
64
+ print_error "Don't know how to open '$remote_url' in a browser."
65
+ return 1
66
+ fi
67
+
68
+ if [[ "$print_only" == true ]]; then
69
+ echo "$url"
70
+ return 0
71
+ fi
72
+
73
+ if [[ "$url" != *gitlab* && "$url" != *bitbucket.org/* && "$url" != *dev.azure.com/* && "$url" != *visualstudio.com/* ]] &&
74
+ command -v gh >/dev/null 2>&1; then
75
+ if GH_PROMPT_DISABLED=1 gh repo view "$url" --web >/dev/null 2>&1; then
76
+ print_success "Opened $url"
77
+ return 0
78
+ fi
79
+ fi
80
+
81
+ gb_open_url "$url"
82
+ }
@@ -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
@@ -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
  }