gitbash 1.9.1 → 2.0.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 +100 -89
- package/bin/gitbash +345 -723
- package/commands/_utils.sh +485 -157
- package/commands/branch.sh +9 -12
- package/commands/cleanstash.sh +47 -122
- package/commands/cleanup.sh +305 -441
- package/commands/commit.sh +136 -172
- package/commands/commits.sh +81 -114
- package/commands/create.sh +231 -314
- package/commands/pr.sh +158 -146
- package/commands/stale.sh +137 -277
- package/commands/stash.sh +11 -39
- package/commands/stashes.sh +10 -13
- package/commands/status.sh +197 -250
- package/commands/switch.sh +186 -274
- package/commands/unstash.sh +45 -109
- package/commands/update.sh +123 -142
- package/docs/CONTRIBUTING.md +16 -5
- package/docs/SECURITY.md +14 -3
- package/package.json +5 -3
package/commands/commit.sh
CHANGED
|
@@ -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]
|
|
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
|
|
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] [
|
|
27
|
+
Usage: commit [OPTIONS] [MESSAGE...] [-- MESSAGE...]
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Commit your changes with a message.
|
|
30
30
|
|
|
31
31
|
Options:
|
|
32
|
-
-p, --push
|
|
33
|
-
-s, --staged
|
|
34
|
-
-a, --amend
|
|
35
|
-
-t, --type
|
|
36
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
107
|
-
echo "Usage: commit [
|
|
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
|
-
|
|
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
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
133
|
-
print_error "
|
|
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=$(
|
|
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
|
-
|
|
156
|
-
)
|
|
198
|
+
<<< "$type_options"
|
|
199
|
+
) || true
|
|
157
200
|
|
|
158
201
|
if [[ -z "$selected_type" ]]; then
|
|
159
|
-
echo "
|
|
160
|
-
return
|
|
202
|
+
echo "Commit cancelled."
|
|
203
|
+
return 0
|
|
161
204
|
fi
|
|
162
205
|
|
|
163
|
-
|
|
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
|
-
#
|
|
173
|
-
|
|
210
|
+
# -----------------------------
|
|
211
|
+
# 3. Commit
|
|
212
|
+
# -----------------------------
|
|
213
|
+
local commit_args=()
|
|
174
214
|
if [[ "$amend_mode" == true ]]; then
|
|
175
|
-
|
|
215
|
+
commit_args+=("--amend")
|
|
176
216
|
fi
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
238
|
-
git add -A && git commit -m "$msg"
|
|
220
|
+
commit_args+=("--no-edit")
|
|
239
221
|
fi
|
|
240
222
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
}
|
package/commands/commits.sh
CHANGED
|
@@ -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]
|
|
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
|
-
↑/↓
|
|
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
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
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
|
-
|
|
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=$(
|
|
57
|
+
current_branch=$(gb_current_branch) || current_branch="(detached HEAD)"
|
|
68
58
|
|
|
69
59
|
# -----------------------------
|
|
70
|
-
#
|
|
60
|
+
# Build commit list (fields separated by \x1f, subject last)
|
|
71
61
|
# -----------------------------
|
|
72
|
-
local commit_list
|
|
73
|
-
|
|
74
|
-
|
|
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 "%-
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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=$(
|
|
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
|
-
--
|
|
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
|
-
|
|
126
|
-
)
|
|
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
|
-
#
|
|
110
|
+
# Order newest first (fzf returns them in the order they were picked)
|
|
145
111
|
# -----------------------------
|
|
146
|
-
local
|
|
147
|
-
|
|
112
|
+
local picked=() line
|
|
148
113
|
while IFS= read -r line; do
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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 "
|
|
164
|
-
for hash in "${
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
}
|