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.
@@ -1,87 +1,67 @@
1
1
  #!/usr/bin/env bash
2
- # shellcheck disable=SC2155
3
2
 
4
3
  # Source common utilities
5
- SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
4
+ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
5
  # shellcheck source=_utils.sh
7
6
  source "$SOURCE_DIR/_utils.sh"
8
7
 
9
- # Stage all changes and commit with a message.
8
+ # Stage changes and commit with a message.
10
9
  # If no commit message argument is given, prompt the user for one.
11
10
  commit() {
12
11
  local should_push=false
13
12
  local staged_only=false
14
13
  local amend_mode=false
15
14
  local show_prefix_menu=false
15
+ local force_with_lease=false
16
16
  local msg_parts=()
17
17
 
18
18
  # Parse arguments
19
19
  while [[ $# -gt 0 ]]; do
20
- case $1 in
20
+ case "$1" in
21
21
  -v|--version)
22
22
  echo "gitbash ${FUNCNAME[0]} v$VERSION"
23
23
  return 0
24
24
  ;;
25
25
  -h|--help)
26
26
  cat << 'EOF'
27
- Usage: commit [MESSAGE] [OPTIONS]
27
+ Usage: commit [OPTIONS] [MESSAGE...] [-- MESSAGE...]
28
28
 
29
- Stage all changes (git add -A) and commit with a message.
29
+ Commit your changes with a message.
30
30
 
31
31
  Options:
32
- -p, --push Push to origin after successful commit
33
- -s, --staged Commit only staged changes (skip prompt, ignore unstaged)
34
- -a, --amend Amend the last commit instead of creating a new one
35
- -t, --type Show conventional commit type selector (feat, fix, docs, etc.)
36
- -h, --help Show this help message
32
+ -p, --push Push to the remote after a successful commit
33
+ -s, --staged Commit only staged changes
34
+ -a, --amend Amend the last commit (keeps its message if none is given)
35
+ -t, --type Show conventional commit type selector (feat, fix, docs, etc.)
36
+ -y, --yes Don't ask for confirmations (untracked files, staged/all)
37
+ --force-with-lease Allow overwriting the remote branch when pushing (after amend/rebase)
38
+ -- Everything after this is the message (even if it starts with '-')
39
+ -h, --help Show this help message
40
+
41
+ What gets committed:
42
+ - Only staged changes → commits them
43
+ - Only unstaged/untracked → stages everything (lists new files first) and commits
44
+ - Both staged and unstaged → asks: [s]taged only (default) or [a]ll
45
+
46
+ Pushing (-p):
47
+ - New branches are pushed with tracking (-u)
48
+ - If the remote has new commits you don't have, they are fast-forwarded
49
+ - If your branch and the remote diverged, you are asked whether to rebase,
50
+ merge or abort. Nothing is rewritten without asking.
51
+ - After --amend, you are asked before force-pushing (--force-with-lease)
37
52
 
38
53
  Conventional Commit Types (with --type):
39
- feat: A new feature
40
- fix: A bug fix
41
- docs: Documentation only changes
42
- style: Code style changes (formatting, whitespace)
43
- refactor: Code change that neither fixes a bug nor adds a feature
44
- perf: Performance improvement
45
- test: Adding or fixing tests
46
- build: Build system or dependency changes
47
- ci: CI/CD configuration changes
48
- chore: Other changes that don't modify src or test files
49
-
50
- Interactive mode (no message):
51
- $ commit
52
- Commit message: fix login bug
53
- [main 1a2b3c4] fix login bug
54
- 1 file changed, 5 insertions(+), 2 deletions(-)
55
-
56
- With message argument:
57
- $ commit implement new feature
58
- [main 5d6e7f8] implement new feature
59
- 3 files changed, 42 insertions(+), 8 deletions(-)
60
-
61
- With type selector:
62
- $ commit -t fix broken tests
63
- # Shows type menu, then commits as "fix: broken tests"
64
-
65
- Amend last commit:
66
- $ commit --amend fix typo in docs
67
- [main abc123] fix typo in docs
68
- 1 file changed, 1 insertion(+)
69
-
70
- With push option:
71
- $ commit update documentation -p
72
- [main 9a8b7c6] update documentation
73
- 1 file changed, 10 insertions(+), 3 deletions(-)
74
- Pushing 'main' to origin...
75
- ✓ Successfully pushed 'main' to origin.
54
+ feat, fix, docs, style, refactor, perf, test, build, ci, chore
76
55
 
77
56
  Examples:
78
57
  commit # Interactive mode
79
58
  commit add user validation # Quick commit
80
59
  commit refactor auth module --push # Commit and push
81
- commit -p update dependencies # Commit and push (short flag)
82
60
  commit -s -p fix bug # Commit only staged and push
83
61
  commit -t add user endpoint # Conventional commit with type menu
84
- commit --amend fix typo # Amend last commit
62
+ commit --amend # Amend, keep the message
63
+ commit --amend fix typo # Amend with a new message
64
+ commit -- -1 is not a valid index # Message starting with '-'
85
65
 
86
66
  EOF
87
67
  return 0
@@ -102,37 +82,100 @@ EOF
102
82
  show_prefix_menu=true
103
83
  shift
104
84
  ;;
85
+ -y|--yes)
86
+ GITBASH_ASSUME_YES=1
87
+ shift
88
+ ;;
89
+ --force-with-lease)
90
+ force_with_lease=true
91
+ shift
92
+ ;;
93
+ --)
94
+ shift
95
+ msg_parts+=("$@")
96
+ break
97
+ ;;
105
98
  -*)
106
- echo "Unknown option: $1"
107
- echo "Usage: commit [message] [-p|--push] [-s|--staged] [-a|--amend] [-t|--type]"
99
+ print_error "Unknown option: $1"
100
+ echo "Usage: commit [-p] [-s] [-a] [-t] [-y] [--force-with-lease] [message...]" >&2
108
101
  return 1
109
102
  ;;
110
103
  *)
111
- # Collect all non-flag arguments as message parts
112
104
  msg_parts+=("$1")
113
105
  shift
114
106
  ;;
115
107
  esac
116
108
  done
117
109
 
118
- # Join message parts with spaces (handle empty array for set -u)
110
+ require_git_repo || return 1
111
+
119
112
  local msg=""
120
113
  if [[ ${#msg_parts[@]} -gt 0 ]]; then
121
114
  msg="${msg_parts[*]}"
122
115
  fi
123
116
 
124
- # If no commit message was provided...
125
- if [ -z "$msg" ]; then
126
- # Prompt the user for a commit message (no newline, no extra spaces)
117
+ # -----------------------------
118
+ # 1. Decide what to commit (before asking for a message)
119
+ # -----------------------------
120
+ local has_staged has_unstaged untracked
121
+ has_staged=$(git diff --cached --name-only 2>/dev/null)
122
+ has_unstaged=$(git diff --name-only 2>/dev/null)
123
+ # ls-files only looks below the current directory; 'git add -A' stages the whole repository
124
+ untracked=$(git -C "$(git rev-parse --show-toplevel)" ls-files --others --exclude-standard 2>/dev/null)
125
+
126
+ # mode: staged | all | none (amend without new changes)
127
+ local mode
128
+ if [[ "$staged_only" == true ]]; then
129
+ if [[ -z "$has_staged" && "$amend_mode" == false ]]; then
130
+ print_error "No staged changes to commit."
131
+ return 1
132
+ fi
133
+ mode="staged"
134
+ elif [[ -n "$has_staged" && ( -n "$has_unstaged" || -n "$untracked" ) ]]; then
135
+ echo "You have both staged and unstaged changes."
136
+ local choice="s"
137
+ if [[ "${GITBASH_ASSUME_YES:-}" != "1" ]]; then
138
+ gb_choice choice "Commit [s]taged only or [a]ll changes? (S/a):" "sa" "s"
139
+ fi
140
+ if [[ "$choice" == "a" ]]; then mode="all"; else mode="staged"; fi
141
+ elif [[ -n "$has_staged" ]]; then
142
+ mode="staged"
143
+ elif [[ -n "$has_unstaged" || -n "$untracked" ]]; then
144
+ mode="all"
145
+ elif [[ "$amend_mode" == true ]]; then
146
+ mode="none"
147
+ else
148
+ echo "Nothing to commit - working tree clean."
149
+ return 1
150
+ fi
151
+
152
+ # List new files before staging everything, so secrets don't slip in unnoticed
153
+ if [[ "$mode" == "all" && -n "$untracked" ]]; then
154
+ echo "New (untracked) files that will be added:"
155
+ printf '%s\n' "$untracked" | sed 's/^/ + /'
156
+ if ! gb_confirm "Add these files to the commit?" y; then
157
+ echo "Commit cancelled. Stage what you want and use 'commit -s', or add files to .gitignore."
158
+ return 1
159
+ fi
160
+ fi
161
+
162
+ # -----------------------------
163
+ # 2. Commit message
164
+ # -----------------------------
165
+ if [[ -z "$msg" && ( "$amend_mode" == false || "$show_prefix_menu" == true ) ]]; then
127
166
  prompt_read "Commit message: " msg
128
167
  fi
168
+ if [[ -z "$msg" && "$amend_mode" == false ]]; then
169
+ print_error "Empty commit message - commit cancelled."
170
+ return 1
171
+ fi
129
172
 
130
- # Show conventional commit type menu if requested
131
173
  if [[ "$show_prefix_menu" == true ]]; then
132
- if ! command -v fzf >/dev/null 2>&1; then
133
- print_error "fzf is required for type selector. Install it or provide message directly."
174
+ if [[ -z "$msg" ]]; then
175
+ print_error "Empty commit message - commit cancelled."
134
176
  return 1
135
177
  fi
178
+ require_fzf || return 1
136
179
 
137
180
  local type_options="feat - A new feature
138
181
  fix - A bug fix
@@ -146,136 +189,57 @@ ci - CI/CD configuration changes
146
189
  chore - Other changes that don't modify src/test files"
147
190
 
148
191
  local selected_type
149
- selected_type=$(echo "$type_options" | fzf --prompt="Commit type > " \
192
+ selected_type=$(run_fzf --prompt="Commit type > " \
150
193
  -i \
151
194
  --reverse \
152
195
  --border \
153
196
  --header="Select conventional commit type" \
154
197
  --no-multi \
155
- --bind=enter:accept \
156
- ) </dev/tty || true
198
+ <<< "$type_options"
199
+ ) || true
157
200
 
158
201
  if [[ -z "$selected_type" ]]; then
159
- echo "Aborted."
160
- return 1
202
+ echo "Commit cancelled."
203
+ return 0
161
204
  fi
162
205
 
163
- # Extract just the type keyword (first word)
164
- local type_keyword
165
- type_keyword=$(echo "$selected_type" | awk '{print $1}')
166
-
167
- # Prepend to message
168
- msg="${type_keyword}: ${msg}"
206
+ msg="${selected_type%% *}: ${msg}"
169
207
  print_info "Commit message: $msg"
170
208
  fi
171
209
 
172
- # Build commit flags
173
- local commit_flags=()
210
+ # -----------------------------
211
+ # 3. Commit
212
+ # -----------------------------
213
+ local commit_args=()
174
214
  if [[ "$amend_mode" == true ]]; then
175
- commit_flags+=("--amend")
215
+ commit_args+=("--amend")
176
216
  fi
177
-
178
- # Check if there are staged changes
179
- local has_staged
180
- has_staged=$(git diff --cached --name-only)
181
-
182
- local has_unstaged
183
- has_unstaged=$(git diff --name-only)
184
-
185
- # For amend mode, we might not need new changes
186
- if [[ "$amend_mode" == true ]]; then
187
- if [[ -z "$has_staged" && -z "$has_unstaged" ]]; then
188
- print_info "Amending commit message only..."
189
- git commit --amend -m "$msg"
190
- else
191
- # Has changes to add
192
- if [[ "$staged_only" == true ]]; then
193
- if [[ -z "$has_staged" ]]; then
194
- print_info "Amending commit message only (no staged changes)..."
195
- git commit --amend -m "$msg"
196
- else
197
- print_info "Amending with staged changes..."
198
- git commit --amend -m "$msg"
199
- fi
200
- else
201
- print_info "Amending with all changes..."
202
- git add -A && git commit --amend -m "$msg"
203
- fi
204
- fi
205
- # If --staged flag is set, only commit staged changes
206
- elif [[ "$staged_only" == true ]]; then
207
- if [[ -z "$has_staged" ]]; then
208
- echo "No staged changes to commit."
209
- return 1
210
- fi
211
- echo "Committing staged changes only..."
212
- git commit -m "$msg"
213
- # If there are both staged and unstaged changes, ask what to commit
214
- elif [[ -n "$has_staged" && -n "$has_unstaged" ]]; then
215
- echo
216
- echo "You have both staged and unstaged changes."
217
- echo
218
- prompt_read "Commit [s]taged only, or [a]ll changes? (s/a): " commit_choice
219
-
220
- case "$commit_choice" in
221
- [sS])
222
- # Commit only staged changes
223
- echo "Committing staged changes only..."
224
- git commit -m "$msg"
225
- ;;
226
- [aA]|*)
227
- # Stage all and commit
228
- echo "Staging all changes and committing..."
229
- git add -A && git commit -m "$msg"
230
- ;;
231
- esac
232
- elif [[ -n "$has_staged" ]]; then
233
- # Only staged changes exist
234
- echo "Committing staged changes..."
235
- git commit -m "$msg"
217
+ if [[ -n "$msg" ]]; then
218
+ commit_args+=("-m" "$msg")
236
219
  else
237
- # No staged changes, stage all and commit
238
- git add -A && git commit -m "$msg"
220
+ commit_args+=("--no-edit")
239
221
  fi
240
222
 
241
- # Push if requested and commit was successful
242
- if [[ $? -eq 0 && "$should_push" == true ]]; then
243
- current_branch=$(git rev-parse --abbrev-ref HEAD)
244
-
245
- # Check if remote has updates and pull first
246
- git fetch origin "$current_branch" 2>/dev/null
247
- local local_commit=$(git rev-parse HEAD)
248
- local remote_commit=$(git rev-parse "origin/$current_branch" 2>/dev/null)
249
-
250
- if [[ -n "$remote_commit" && "$local_commit" != "$remote_commit" ]]; then
251
- # Check if we're behind the remote
252
- if git merge-base --is-ancestor "$local_commit" "$remote_commit" 2>/dev/null; then
253
- print_info "Remote has updates. Pulling first..."
254
- git pull --rebase origin "$current_branch"
255
- if [[ $? -ne 0 ]]; then
256
- print_warning "Failed to pull remote changes. Please resolve conflicts and try again."
257
- return 1
258
- fi
259
- print_success "Pulled latest changes."
260
- elif ! git merge-base --is-ancestor "$remote_commit" "$local_commit" 2>/dev/null; then
261
- # Branches have diverged
262
- print_info "Remote has diverged. Pulling with rebase..."
263
- git pull --rebase origin "$current_branch"
264
- if [[ $? -ne 0 ]]; then
265
- print_warning "Failed to pull remote changes. Please resolve conflicts and try again."
266
- return 1
267
- fi
268
- print_success "Rebased on latest changes."
269
- fi
270
- fi
271
-
272
- print_info "Pushing '$current_branch' to origin..."
273
- git push origin "$current_branch"
274
- if [[ $? -eq 0 ]]; then
275
- print_success "Successfully pushed '$current_branch' to origin."
276
- else
277
- print_warning "Failed to push '$current_branch' to origin."
223
+ if [[ "$mode" == "all" ]]; then
224
+ if ! git add -A; then
225
+ print_error "Failed to stage changes."
278
226
  return 1
279
227
  fi
280
228
  fi
229
+
230
+ if ! git commit "${commit_args[@]}"; then
231
+ print_error "Commit failed."
232
+ return 1
233
+ fi
234
+
235
+ # -----------------------------
236
+ # 4. Push
237
+ # -----------------------------
238
+ if [[ "$should_push" == true ]]; then
239
+ local push_args=()
240
+ [[ "$amend_mode" == true ]] && push_args+=("--amend")
241
+ [[ "$force_with_lease" == true ]] && push_args+=("--force-with-lease")
242
+ gb_sync_and_push ${push_args[@]+"${push_args[@]}"} || return 1
243
+ fi
244
+ return 0
281
245
  }
@@ -1,16 +1,12 @@
1
1
  #!/usr/bin/env bash
2
- # shellcheck disable=SC2155
3
2
 
4
3
  # Source common utilities
5
- SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
4
+ SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
5
  # shellcheck source=_utils.sh
7
6
  source "$SOURCE_DIR/_utils.sh"
8
7
 
9
8
  # List recent commits and optionally revert selected ones
10
9
  commits() {
11
- # -----------------------------
12
- # 0. Check for help/version flag
13
- # -----------------------------
14
10
  if [[ "${1:-}" == "-v" || "${1:-}" == "--version" ]]; then
15
11
  echo "gitbash ${FUNCNAME[0]} v$VERSION"
16
12
  return 0
@@ -25,24 +21,20 @@ Arguments:
25
21
  COUNT Number of commits to show (default: 20)
26
22
 
27
23
  Navigation:
28
- ↑/↓ or j/k Navigate through commits
24
+ ↑/↓ Navigate through commits
29
25
  TAB Select/deselect commit for revert
30
26
  Enter Revert selected commit(s)
31
27
  ESC/Ctrl-C Exit without action
32
28
 
33
29
  Notes:
34
- - Shows commits from newest to oldest
35
- - Preview shows full commit diff
36
- - Multiple commits can be selected for revert
37
- - Reverts are done in reverse order (oldest first) to avoid conflicts
30
+ - Preview shows the full commit diff
31
+ - Selected commits are reverted newest first, whatever order you picked them in
32
+ - Merge commits are reverted against their first parent (the branch they were merged into)
38
33
  - Each revert creates a new commit
39
34
 
40
35
  Examples:
41
36
  $ commits
42
- # Shows last 20 commits
43
-
44
37
  $ commits 50
45
- # Shows last 50 commits
46
38
 
47
39
  Requirements:
48
40
  - Must be in a git repository
@@ -52,50 +44,49 @@ EOF
52
44
  return 0
53
45
  fi
54
46
 
55
- # -----------------------------
56
- # 1. Check prerequisites
57
- # -----------------------------
58
47
  require_git_repo || return 1
59
- require_fzf || return 1
60
48
 
61
- # -----------------------------
62
- # 2. Get commit count
63
- # -----------------------------
64
49
  local count="${1:-20}"
65
- local abort_label="✖ Abort"
50
+ if ! [[ "$count" =~ ^[1-9][0-9]*$ ]]; then
51
+ print_error "COUNT must be a positive number (got '$count')."
52
+ return 1
53
+ fi
54
+ require_fzf || return 1
55
+
66
56
  local current_branch
67
- current_branch=$(git rev-parse --abbrev-ref HEAD)
57
+ current_branch=$(gb_current_branch) || current_branch="(detached HEAD)"
68
58
 
69
59
  # -----------------------------
70
- # 3. Build commit list
60
+ # Build commit list (fields separated by \x1f, subject last)
71
61
  # -----------------------------
72
- local commit_list
73
- commit_list=$(git log -n "$count" --format="%h|%s|%cr|%an" | while IFS='|' read -r hash subject date author; do
74
- # Truncate subject if too long
62
+ local commit_list="" hash date author subject
63
+ while IFS=$'\x1f' read -r hash date author subject; do
64
+ [[ -z "$hash" ]] && continue
75
65
  if [[ ${#subject} -gt 60 ]]; then
76
66
  subject="${subject:0:57}..."
77
67
  fi
78
- printf "%-8s %-60s %-15s %s\t%s\n" "$hash" "$subject" "$date" "$author" "$hash"
79
- done)
68
+ commit_list+=$(printf "%-10s %-60s %-15s %s" "$hash" "$subject" "$date" "$author")
69
+ commit_list+=$'\t'"$hash"$'\n'
70
+ done < <(git log -n "$count" --format='%h%x1f%cr%x1f%an%x1f%s' 2>/dev/null)
80
71
 
81
72
  if [[ -z "$commit_list" ]]; then
82
73
  echo "No commits found."
83
74
  return 0
84
75
  fi
85
76
 
86
- # Add abort option
87
- commit_list+=$'\n'"$abort_label"$'\t'
77
+ local pager preview
78
+ pager=$(gb_diff_pager)
79
+ preview='
80
+ hash=$(printf "%s" {} | cut -f2)
81
+ git show --stat --color=always "$hash" 2>/dev/null
82
+ echo
83
+ echo "─────────────────────────────────────────────────────"
84
+ echo
85
+ git show --color=always --format= "$hash" 2>/dev/null | head -300 | __PAGER__'
86
+ preview="${preview//__PAGER__/$pager}"
88
87
 
89
- # Create temp file
90
- local commit_file
91
- commit_file=$(mktemp)
92
- echo "$commit_list" > "$commit_file"
93
-
94
- # -----------------------------
95
- # 4. Run fzf picker
96
- # -----------------------------
97
88
  local selection
98
- selection=$(fzf \
89
+ selection=$(run_fzf \
99
90
  --prompt="Commits on '$current_branch' > " \
100
91
  -i \
101
92
  --reverse \
@@ -105,96 +96,72 @@ Showing last $count commits" \
105
96
  --multi \
106
97
  --delimiter=$'\t' \
107
98
  --with-nth=1 \
108
- --bind=enter:accept \
109
- --preview='
110
- line={}
111
- if [[ "$line" == "✖ Abort"* ]]; then
112
- echo "Exit without action";
113
- else
114
- hash=$(echo "$line" | cut -f2)
115
- echo "Commit: $hash";
116
- echo "";
117
- git show --stat --color=always "$hash" 2>/dev/null;
118
- echo "";
119
- echo "─────────────────────────────────────────────────────";
120
- echo "";
121
- git show --color=always "$hash" 2>/dev/null | head -100 | delta --light 2>/dev/null || git show --color=always "$hash" 2>/dev/null | head -100;
122
- fi
123
- ' \
99
+ --preview="$preview" \
124
100
  --preview-window=right:50% \
125
- < "$commit_file"
126
- ) </dev/tty || true
127
-
128
- # Cleanup temp file
129
- rm -f "$commit_file"
101
+ <<< "$commit_list"
102
+ ) || true
130
103
 
131
- # ESC or Ctrl-C
132
104
  if [[ -z "$selection" ]]; then
133
105
  echo "Exited."
134
106
  return 0
135
107
  fi
136
108
 
137
- # Abort option
138
- if echo "$selection" | grep -q "^$abort_label"; then
139
- echo "Aborted."
140
- return 0
141
- fi
142
-
143
109
  # -----------------------------
144
- # 5. Extract commit hashes and confirm revert
110
+ # Order newest first (fzf returns them in the order they were picked)
145
111
  # -----------------------------
146
- local commits_to_revert=()
147
-
112
+ local picked=() line
148
113
  while IFS= read -r line; do
149
- [[ "$line" == "✖ Abort"* ]] && continue
150
- local hash
151
- hash=$(echo "$line" | cut -f2)
152
- if [[ -n "$hash" ]]; then
153
- commits_to_revert+=("$hash")
154
- fi
114
+ hash=$(printf '%s' "$line" | cut -f2)
115
+ [[ -n "$hash" ]] && picked+=("$hash")
155
116
  done <<< "$selection"
156
117
 
157
- if [[ ${#commits_to_revert[@]} -eq 0 ]]; then
118
+ # Walk the history newest first and keep the picked commits in that order
119
+ local to_revert=() full picked_full=" " short
120
+ for short in "${picked[@]}"; do
121
+ picked_full+="$(git rev-parse --verify --quiet "$short^{commit}") "
122
+ done
123
+ while IFS= read -r full; do
124
+ [[ "$picked_full" == *" $full "* ]] && to_revert+=("$full")
125
+ [[ ${#to_revert[@]} -eq ${#picked[@]} ]] && break
126
+ done < <(git rev-list --topo-order HEAD 2>/dev/null)
127
+
128
+ if [[ ${#to_revert[@]} -eq 0 ]]; then
158
129
  echo "No commits selected."
159
130
  return 0
160
131
  fi
161
132
 
162
- echo ""
163
- echo "Selected commits to revert:"
164
- for hash in "${commits_to_revert[@]}"; do
165
- local subject
166
- subject=$(git log -1 --format="%s" "$hash")
167
- echo " - $hash: $subject"
133
+ echo
134
+ echo "Commits to revert (newest first):"
135
+ for hash in "${to_revert[@]}"; do
136
+ echo " - $(git log -1 --format='%h %s' "$hash")"
137
+ done
138
+ echo
139
+
140
+ if ! gb_confirm "Revert these ${#to_revert[@]} commit(s)?" n; then
141
+ echo "Revert cancelled."
142
+ return 0
143
+ fi
144
+
145
+ local parents revert_args
146
+ for hash in "${to_revert[@]}"; do
147
+ revert_args=(--no-edit)
148
+ parents=$(git rev-list --parents -n 1 "$hash" | wc -w | tr -d ' ')
149
+ if [[ "$parents" -gt 2 ]]; then
150
+ print_info "$(git rev-parse --short "$hash") is a merge commit - reverting it against its first parent."
151
+ revert_args+=(-m 1)
152
+ fi
153
+ if git revert "${revert_args[@]}" "$hash"; then
154
+ print_success "Reverted $(git rev-parse --short "$hash")"
155
+ else
156
+ if git rev-parse --verify --quiet REVERT_HEAD >/dev/null; then
157
+ print_error "Reverting $(git rev-parse --short "$hash") stopped with conflicts."
158
+ echo " Resolve them and run 'git revert --continue', or 'git revert --abort' to cancel this revert." >&2
159
+ else
160
+ print_error "Failed to revert $(git rev-parse --short "$hash")."
161
+ fi
162
+ return 1
163
+ fi
168
164
  done
169
- echo ""
170
-
171
- local confirm
172
- prompt_read "Revert these ${#commits_to_revert[@]} commit(s)? (y/N): " confirm
173
- case "$confirm" in
174
- [yY][eE][sS]|[yY])
175
- print_info "Reverting commits..."
176
-
177
- # Reverse the array to revert oldest first (avoids conflicts)
178
- local reversed=()
179
- for ((i=${#commits_to_revert[@]}-1; i>=0; i--)); do
180
- reversed+=("${commits_to_revert[$i]}")
181
- done
182
-
183
- for hash in "${reversed[@]}"; do
184
- echo "Reverting $hash ..."
185
- if git revert --no-edit "$hash"; then
186
- print_success "Reverted $hash"
187
- else
188
- print_error "Failed to revert $hash (conflict?)"
189
- echo " Resolve the conflict and run 'git revert --continue'"
190
- return 1
191
- fi
192
- done
193
- echo ""
194
- print_success "All selected commits reverted."
195
- ;;
196
- *)
197
- echo "Revert cancelled."
198
- ;;
199
- esac
165
+ echo
166
+ print_success "All selected commits reverted."
200
167
  }