bimagic 1.4.2
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/LICENSE +21 -0
- package/README.md +656 -0
- package/SECURITY.md +25 -0
- package/bimagic +1310 -0
- package/package.json +34 -0
package/bimagic
ADDED
|
@@ -0,0 +1,1310 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
VERSION="v1.4.2"
|
|
4
|
+
|
|
5
|
+
if [[ "$1" == "--version" || "$1" == "-v" ]]; then
|
|
6
|
+
echo "Bimagic Git Wizard $VERSION"
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
# Configuration and Theme
|
|
11
|
+
CONFIG_DIR="$HOME/.config/bimagic"
|
|
12
|
+
THEME_FILE="$CONFIG_DIR/theme.wz"
|
|
13
|
+
|
|
14
|
+
# Default colors (ANSI 0-255 or HEX)
|
|
15
|
+
BIMAGIC_PRIMARY="212"
|
|
16
|
+
BIMAGIC_SECONDARY="51"
|
|
17
|
+
BIMAGIC_SUCCESS="46"
|
|
18
|
+
BIMAGIC_ERROR="196"
|
|
19
|
+
BIMAGIC_WARNING="214"
|
|
20
|
+
BIMAGIC_INFO="39"
|
|
21
|
+
BIMAGIC_MUTED="240"
|
|
22
|
+
|
|
23
|
+
# Banner Colors
|
|
24
|
+
BANNER_COLOR_1="51"
|
|
25
|
+
BANNER_COLOR_2="45"
|
|
26
|
+
BANNER_COLOR_3="39"
|
|
27
|
+
BANNER_COLOR_4="99"
|
|
28
|
+
BANNER_COLOR_5="135"
|
|
29
|
+
|
|
30
|
+
# Load theme if it exists
|
|
31
|
+
if [[ -f "$THEME_FILE" ]]; then
|
|
32
|
+
source "$THEME_FILE"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Function to get ANSI escape sequence for echo
|
|
36
|
+
get_ansi_esc() {
|
|
37
|
+
local color=$1
|
|
38
|
+
if [[ "$color" =~ ^# ]]; then
|
|
39
|
+
local r=$(printf "%d" "0x${color:1:2}")
|
|
40
|
+
local g=$(printf "%d" "0x${color:3:2}")
|
|
41
|
+
local b=$(printf "%d" "0x${color:5:2}")
|
|
42
|
+
echo -e "\033[38;2;${r};${g};${b}m"
|
|
43
|
+
else
|
|
44
|
+
echo -e "\033[38;5;${color}m"
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Colors for output (using theme)
|
|
49
|
+
GREEN=$(get_ansi_esc "$BIMAGIC_SUCCESS")
|
|
50
|
+
RED=$(get_ansi_esc "$BIMAGIC_ERROR")
|
|
51
|
+
YELLOW=$(get_ansi_esc "$BIMAGIC_WARNING")
|
|
52
|
+
BLUE=$(get_ansi_esc "$BIMAGIC_SECONDARY")
|
|
53
|
+
PURPLE=$(get_ansi_esc "$BIMAGIC_PRIMARY")
|
|
54
|
+
CYAN=$(get_ansi_esc "$BIMAGIC_INFO")
|
|
55
|
+
NC='\033[0m' # No Color
|
|
56
|
+
|
|
57
|
+
# Ensure gum is installed
|
|
58
|
+
if ! command -v gum &>/dev/null; then
|
|
59
|
+
echo "Error: gum is not installed."
|
|
60
|
+
echo "Please install it: https://github.com/charmbracelet/gum"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
echo "Welcome to the Git Wizard! Let's work some magic..."
|
|
65
|
+
echo
|
|
66
|
+
|
|
67
|
+
# Sound functions
|
|
68
|
+
play_sound() {
|
|
69
|
+
local sound_type=$1
|
|
70
|
+
case "$sound_type" in
|
|
71
|
+
"success")
|
|
72
|
+
# Pleasant bell sound
|
|
73
|
+
echo -e "\a" # System bell
|
|
74
|
+
# Alternative: play a beep sequence
|
|
75
|
+
for i in {1..2}; do
|
|
76
|
+
printf "\a"
|
|
77
|
+
sleep 0.1
|
|
78
|
+
done
|
|
79
|
+
;;
|
|
80
|
+
"error")
|
|
81
|
+
# Harsher error sound
|
|
82
|
+
for i in {1..3}; do
|
|
83
|
+
printf "\a"
|
|
84
|
+
sleep 0.05
|
|
85
|
+
done
|
|
86
|
+
;;
|
|
87
|
+
"warning")
|
|
88
|
+
# Single warning beep
|
|
89
|
+
printf "\a"
|
|
90
|
+
;;
|
|
91
|
+
"magic")
|
|
92
|
+
# Magical sequence for special operations
|
|
93
|
+
for i in {1..3}; do
|
|
94
|
+
printf "\a"
|
|
95
|
+
sleep 0.2
|
|
96
|
+
done
|
|
97
|
+
;;
|
|
98
|
+
"progress")
|
|
99
|
+
# Subtle progress indicator
|
|
100
|
+
printf "\a"
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
}
|
|
104
|
+
print_status() {
|
|
105
|
+
gum style --foreground "$BIMAGIC_PRIMARY" "$1"
|
|
106
|
+
play_sound "success"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
print_error() {
|
|
110
|
+
gum style --foreground "$BIMAGIC_ERROR" "$1"
|
|
111
|
+
play_sound "error"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
print_warning() {
|
|
115
|
+
gum style --foreground "$BIMAGIC_WARNING" "$1"
|
|
116
|
+
play_sound "warning"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Function to get current branch with color
|
|
120
|
+
get_current_branch() {
|
|
121
|
+
git branch --show-current 2>/dev/null || echo "main"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Function to display branches with gum
|
|
125
|
+
show_branches() {
|
|
126
|
+
local current_branch=$(get_current_branch)
|
|
127
|
+
|
|
128
|
+
# Get all branches and highlight current one
|
|
129
|
+
git branch -a --format='%(refname:short)' |
|
|
130
|
+
while read -r branch; do
|
|
131
|
+
if [[ "$branch" == "$current_branch" ]]; then
|
|
132
|
+
echo -e "${GREEN}➤ $branch${NC} (current)"
|
|
133
|
+
else
|
|
134
|
+
echo " $branch"
|
|
135
|
+
fi
|
|
136
|
+
done | sort -u
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Function to setup or update a remote
|
|
140
|
+
setup_remote() {
|
|
141
|
+
local remote_name=${1:-"origin"}
|
|
142
|
+
|
|
143
|
+
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
144
|
+
print_error "Not a git repository! Initialize it first."
|
|
145
|
+
return 1
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Ask for protocol
|
|
149
|
+
local protocol=$(gum choose --header "Select protocol for '$remote_name':" "HTTPS (Token)" "SSH")
|
|
150
|
+
|
|
151
|
+
local remote_url=""
|
|
152
|
+
if [[ "$protocol" == "HTTPS (Token)" ]]; then
|
|
153
|
+
if [[ -z "$GITHUB_USER" ]] || [[ -z "$GITHUB_TOKEN" ]]; then
|
|
154
|
+
print_error "GITHUB_USER and GITHUB_TOKEN required for HTTPS!"
|
|
155
|
+
return 1
|
|
156
|
+
fi
|
|
157
|
+
local reponame=$(gum input --placeholder "Enter repo name (example: my-repo)")
|
|
158
|
+
[[ -z "$reponame" ]] && return 1
|
|
159
|
+
# Ensure it ends with .git consistently
|
|
160
|
+
reponame="${reponame%.git}.git"
|
|
161
|
+
remote_url="https://${GITHUB_TOKEN}@github.com/${GITHUB_USER}/${reponame}"
|
|
162
|
+
elif [[ "$protocol" == "SSH" ]]; then
|
|
163
|
+
remote_url=$(gum input --placeholder "Enter SSH URL (e.g., git@github.com:user/repo.git)")
|
|
164
|
+
[[ -z "$remote_url" ]] && return 1
|
|
165
|
+
else
|
|
166
|
+
print_warning "No protocol selected."
|
|
167
|
+
return 1
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
if gum confirm "Set remote '$remote_name' to $remote_url?"; then
|
|
171
|
+
git remote remove "$remote_name" 2>/dev/null
|
|
172
|
+
git remote add "$remote_name" "$remote_url"
|
|
173
|
+
print_status " Remote '$remote_name' set to $remote_url"
|
|
174
|
+
return 0
|
|
175
|
+
else
|
|
176
|
+
print_status "Operation cancelled."
|
|
177
|
+
return 1
|
|
178
|
+
fi
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
show_repo_status() {
|
|
182
|
+
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
183
|
+
print_warning "Not inside a git repository!"
|
|
184
|
+
return
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
local branch=$(get_current_branch)
|
|
188
|
+
|
|
189
|
+
# Ahead/behind info
|
|
190
|
+
local upstream=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)
|
|
191
|
+
local ahead behind
|
|
192
|
+
if [[ -n "$upstream" ]]; then
|
|
193
|
+
ahead=$(git rev-list --count "$upstream"..HEAD 2>/dev/null)
|
|
194
|
+
behind=$(git rev-list --count HEAD.."$upstream" 2>/dev/null)
|
|
195
|
+
else
|
|
196
|
+
ahead=0
|
|
197
|
+
behind=0
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# Working tree status
|
|
201
|
+
local status
|
|
202
|
+
if git diff --quiet 2>/dev/null && git diff --cached --quiet 2>/dev/null; then
|
|
203
|
+
if git ls-files -u | grep -q .; then
|
|
204
|
+
status="🔴 conflicts"
|
|
205
|
+
color="$BIMAGIC_ERROR"
|
|
206
|
+
else
|
|
207
|
+
status="🟢 clean"
|
|
208
|
+
color="$BIMAGIC_SUCCESS"
|
|
209
|
+
fi
|
|
210
|
+
else
|
|
211
|
+
status="🟡 uncommitted"
|
|
212
|
+
color="$BIMAGIC_WARNING"
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
local display_user=${GITHUB_USER:-"SSH/Local"}
|
|
216
|
+
local content="GITHUB USER: $display_user
|
|
217
|
+
BRANCH: $branch
|
|
218
|
+
AHEAD: $ahead | BEHIND: $behind
|
|
219
|
+
STATUS: $status"
|
|
220
|
+
|
|
221
|
+
echo
|
|
222
|
+
gum style \
|
|
223
|
+
--border rounded \
|
|
224
|
+
--margin "1 0" \
|
|
225
|
+
--padding "1 2" \
|
|
226
|
+
--border-foreground "$color" \
|
|
227
|
+
"$content"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
generate_bar() {
|
|
231
|
+
local percentage=$1
|
|
232
|
+
local bar=""
|
|
233
|
+
# Convert float to integer for bar calculation
|
|
234
|
+
local int_percentage=${percentage%.*}
|
|
235
|
+
local bars=$((int_percentage / 2)) # Each █ represents 2%
|
|
236
|
+
|
|
237
|
+
for ((i = 0; i < bars; i++)); do
|
|
238
|
+
bar+="█"
|
|
239
|
+
done
|
|
240
|
+
echo "$bar"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# Function to get contributor statistics
|
|
244
|
+
show_contributor_stats() {
|
|
245
|
+
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
246
|
+
print_error "Not a git repository!"
|
|
247
|
+
return 1
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
# Time range selection
|
|
251
|
+
local time_range=$(gum choose --header "Select time range" \
|
|
252
|
+
"Last 7 days" \
|
|
253
|
+
"Last 30 days" \
|
|
254
|
+
"Last 90 days" \
|
|
255
|
+
"Last year" \
|
|
256
|
+
"All time")
|
|
257
|
+
|
|
258
|
+
local since=""
|
|
259
|
+
case "$time_range" in
|
|
260
|
+
"Last 7 days") since="--since='7 days ago'" ;;
|
|
261
|
+
"Last 30 days") since="--since='30 days ago'" ;;
|
|
262
|
+
"Last 90 days") since="--since='3 months ago'" ;;
|
|
263
|
+
"Last year") since="--since='1 year ago'" ;;
|
|
264
|
+
"All time") since="" ;;
|
|
265
|
+
*)
|
|
266
|
+
print_warning "No time range selected."
|
|
267
|
+
return 1
|
|
268
|
+
;;
|
|
269
|
+
esac
|
|
270
|
+
|
|
271
|
+
print_status "Analyzing contributions ($time_range)..."
|
|
272
|
+
echo
|
|
273
|
+
|
|
274
|
+
# Temporary file for processing
|
|
275
|
+
local temp_file=$(mktemp)
|
|
276
|
+
|
|
277
|
+
# Get git log with numstat and format for processing
|
|
278
|
+
if [[ -n "$since" ]]; then
|
|
279
|
+
gum spin --title "Analyzing log..." -- bash -c "git log --pretty=format:'COMMIT|%aN' --numstat $since > $temp_file 2>/dev/null"
|
|
280
|
+
else
|
|
281
|
+
gum spin --title "Analyzing log..." -- bash -c "git log --pretty=format:'COMMIT|%aN' --numstat > $temp_file 2>/dev/null"
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
# Check if we got any data
|
|
285
|
+
if [[ ! -s "$temp_file" ]]; then
|
|
286
|
+
print_error "No contribution data found for the selected period."
|
|
287
|
+
rm -f "$temp_file"
|
|
288
|
+
return 1
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# Process the data with awk
|
|
292
|
+
local stats=$(awk '
|
|
293
|
+
BEGIN {
|
|
294
|
+
total_lines = 0
|
|
295
|
+
}
|
|
296
|
+
/^COMMIT\|/ {
|
|
297
|
+
# Extract author name (everything after COMMIT|)
|
|
298
|
+
split($0, a, "|")
|
|
299
|
+
author = a[2]
|
|
300
|
+
author_commits[author]++
|
|
301
|
+
last_author = author
|
|
302
|
+
}
|
|
303
|
+
/^[0-9-]/ {
|
|
304
|
+
added = $1
|
|
305
|
+
deleted = $2
|
|
306
|
+
# Use 0 if added/deleted is "-" (binary files)
|
|
307
|
+
add_num = (added == "-" ? 0 : added)
|
|
308
|
+
del_num = (deleted == "-" ? 0 : deleted)
|
|
309
|
+
|
|
310
|
+
author = last_author
|
|
311
|
+
if (author != "") {
|
|
312
|
+
author_lines[author] += add_num + del_num
|
|
313
|
+
total_lines += add_num + del_num
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
END {
|
|
317
|
+
for (author in author_commits) {
|
|
318
|
+
if (total_lines > 0) {
|
|
319
|
+
percentage = (author_lines[author] / total_lines) * 100
|
|
320
|
+
} else {
|
|
321
|
+
percentage = 0
|
|
322
|
+
}
|
|
323
|
+
printf "%s|%d|%d|%.1f\n", author, author_lines[author], author_commits[author], percentage
|
|
324
|
+
}
|
|
325
|
+
}' "$temp_file" | sort -t'|' -k2 -nr)
|
|
326
|
+
|
|
327
|
+
rm -f "$temp_file"
|
|
328
|
+
|
|
329
|
+
if [[ -z "$stats" ]]; then
|
|
330
|
+
print_error "No contribution data found for the selected period."
|
|
331
|
+
return 1
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
# Calculate additional metrics
|
|
335
|
+
local most_active_author=""
|
|
336
|
+
local most_commits=0
|
|
337
|
+
local most_productive_author=""
|
|
338
|
+
local highest_avg=0
|
|
339
|
+
|
|
340
|
+
# Display header
|
|
341
|
+
echo -e "${PURPLE}Contribution Report ($time_range)${NC}"
|
|
342
|
+
echo "$(printf '─%.0s' $(seq 1 45))"
|
|
343
|
+
|
|
344
|
+
# Display each contributor
|
|
345
|
+
while IFS='|' read -r author lines commits percentage; do
|
|
346
|
+
# Clean up author name
|
|
347
|
+
author=$(echo "$author" | sed 's/^ *//;s/ *$//')
|
|
348
|
+
|
|
349
|
+
# Generate bar (max 50 characters for 100%)
|
|
350
|
+
local bar=$(generate_bar "$percentage")
|
|
351
|
+
|
|
352
|
+
printf "% -15s %-25s %5.1f%% (%d lines)\n" \
|
|
353
|
+
"$author" "$bar" "$percentage" "$lines"
|
|
354
|
+
|
|
355
|
+
# Track most active (most commits)
|
|
356
|
+
if [[ $commits -gt $most_commits ]]; then
|
|
357
|
+
most_commits=$commits
|
|
358
|
+
most_active_author="$author"
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
# Track most productive (highest average lines per commit)
|
|
362
|
+
if [[ $commits -gt 0 ]]; then
|
|
363
|
+
local avg_lines=$((lines / commits))
|
|
364
|
+
if [[ $avg_lines -gt $highest_avg ]]; then
|
|
365
|
+
highest_avg=$avg_lines
|
|
366
|
+
most_productive_author="$author"
|
|
367
|
+
fi
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
done <<<"$stats"
|
|
371
|
+
|
|
372
|
+
echo
|
|
373
|
+
echo -e "${CYAN}Highlights:${NC}"
|
|
374
|
+
if [[ -n "$most_active_author" ]]; then
|
|
375
|
+
echo -e "${BLUE}Most Active:${NC} $most_active_author ($most_commits commits)"
|
|
376
|
+
fi
|
|
377
|
+
if [[ -n "$most_productive_author" ]]; then
|
|
378
|
+
echo -e "${BLUE}Most Productive:${NC} $most_productive_author ($highest_avg lines/commit)"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
local total_contributors=$(echo "$stats" | wc -l)
|
|
382
|
+
echo -e "${BLUE}Total Contributors:${NC} $total_contributors"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# Function to clone a repository (Standard or Interactive)
|
|
386
|
+
clone_repo() {
|
|
387
|
+
local url=$1
|
|
388
|
+
local interactive=$2
|
|
389
|
+
|
|
390
|
+
# Extract repo name from URL (basename, remove .git)
|
|
391
|
+
local repo_name=$(basename "$url" .git)
|
|
392
|
+
|
|
393
|
+
if [[ -d "$repo_name" ]]; then
|
|
394
|
+
print_error "Directory '$repo_name' already exists."
|
|
395
|
+
return 1
|
|
396
|
+
fi
|
|
397
|
+
|
|
398
|
+
if [[ "$interactive" == "true" ]]; then
|
|
399
|
+
print_status "Initializing interactive clone for $repo_name..."
|
|
400
|
+
|
|
401
|
+
# 1. Clone with --no-checkout and --filter=blob:none (downloads commits/trees, no file contents)
|
|
402
|
+
if ! gum spin --title "Cloning structure..." -- git clone --filter=blob:none --no-checkout "$url" "$repo_name"; then
|
|
403
|
+
print_error "Clone failed."
|
|
404
|
+
return 1
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
pushd "$repo_name" >/dev/null || return 1
|
|
408
|
+
|
|
409
|
+
# 2. List all files (from HEAD) and let user select
|
|
410
|
+
print_status "Fetching file list..."
|
|
411
|
+
local selected_paths
|
|
412
|
+
# git ls-tree -r --name-only HEAD lists all files recursively
|
|
413
|
+
selected_paths=$(git ls-tree -r --name-only HEAD | gum filter --no-limit --placeholder "Select files/folders to download (Space to select)")
|
|
414
|
+
|
|
415
|
+
if [[ -z "$selected_paths" ]]; then
|
|
416
|
+
print_warning "No files selected. Aborting checkout."
|
|
417
|
+
popd >/dev/null
|
|
418
|
+
rm -rf "$repo_name"
|
|
419
|
+
return 1
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
# 3. Configure sparse-checkout
|
|
423
|
+
# FIX: Add --no-cone so Git accepts individual file paths
|
|
424
|
+
gum spin --title "Configuring sparse checkout..." -- git sparse-checkout init --no-cone
|
|
425
|
+
|
|
426
|
+
# We use 'set' with the selected paths.
|
|
427
|
+
# Note: --cone mode works best with directories, but for specific files we might rely on the fact
|
|
428
|
+
# that sparse-checkout patterns handle full paths.
|
|
429
|
+
# However, 'git sparse-checkout set' treats arguments as patterns.
|
|
430
|
+
# We turn off cone for precise file selection transparency if needed, but standard 'set' often works.
|
|
431
|
+
# To be safe for exact file paths, we simply pass them.
|
|
432
|
+
# If the list is huge, this might fail on command line length.
|
|
433
|
+
# Ideally we pipe to 'git sparse-checkout set --stdin', but gum returns newline separated.
|
|
434
|
+
|
|
435
|
+
gum spin --title "Downloading selected files..." -- git checkout HEAD
|
|
436
|
+
|
|
437
|
+
popd >/dev/null
|
|
438
|
+
print_status "Successfully cloned selected files into '$repo_name'!"
|
|
439
|
+
|
|
440
|
+
else
|
|
441
|
+
# Standard clone
|
|
442
|
+
if gum spin --title "Cloning repository..." -- git clone "$url"; then
|
|
443
|
+
print_status "Successfully cloned '$url' into '$repo_name'!"
|
|
444
|
+
else
|
|
445
|
+
print_error "Clone failed."
|
|
446
|
+
return 1
|
|
447
|
+
fi
|
|
448
|
+
fi
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
# a .gitignore generator to your tool
|
|
452
|
+
summon_gitignore() {
|
|
453
|
+
# Check for curl
|
|
454
|
+
if ! command -v curl &>/dev/null; then
|
|
455
|
+
print_error "Error: curl is not installed. Required to summon the Architect."
|
|
456
|
+
return 1
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
print_status "📜 Summoning the Architect..."
|
|
460
|
+
|
|
461
|
+
# Check if .gitignore already exists
|
|
462
|
+
if [[ -f ".gitignore" ]]; then
|
|
463
|
+
print_warning "A .gitignore file already exists in this directory."
|
|
464
|
+
if ! gum confirm "Do you want to overwrite it?"; then
|
|
465
|
+
print_status "Operation cancelled."
|
|
466
|
+
return 0
|
|
467
|
+
fi
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
# List of popular templates (curated from github/gitignore)
|
|
471
|
+
local templates=(
|
|
472
|
+
"Actionscript" "Ada" "Android" "Angular" "AppEngine" "ArchLinuxPackages" "Autotools"
|
|
473
|
+
"C++" "C" "CMake" "CUDA" "CakePHP" "ChefCookbook" "Clojure" "CodeIgniter" "Composer"
|
|
474
|
+
"Dart" "Delphi" "Dotnet" "Drupal" "Elixir" "Elm" "Erlang" "Flutter" "Fortran"
|
|
475
|
+
"Go" "Godot" "Gradle" "Grails" "Haskell" "Haxe" "Java" "Jekyll" "Joomla" "Julia"
|
|
476
|
+
"Kotlin" "Laravel" "Lua" "Magento" "Maven" "Nextjs" "Nim" "Nix" "Node" "Objective-C"
|
|
477
|
+
"Opa" "Perl" "Phalcon" "PlayFramework" "Prestashop" "Processing" "Python" "Qt"
|
|
478
|
+
"R" "ROS" "Rails" "Ruby" "Rust" "Scala" "Scheme" "Smalltalk" "Swift" "Symfony"
|
|
479
|
+
"Terraform" "TeX" "Unity" "UnrealEngine" "VisualStudio" "WordPress" "Zig"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# Use gum filter for a better selection experience
|
|
483
|
+
local template=$(printf "%s\n" "${templates[@]}" | gum filter --placeholder "Search for a blueprint (e.g., Python, Node, Rust)...")
|
|
484
|
+
|
|
485
|
+
if [[ -z "$template" ]]; then
|
|
486
|
+
print_status "Cancelled."
|
|
487
|
+
return 0
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
# Fetch the template directly from GitHub's official repository
|
|
491
|
+
print_status "Drawing the magic circle for $template..."
|
|
492
|
+
|
|
493
|
+
local url="https://raw.githubusercontent.com/github/gitignore/main/${template}.gitignore"
|
|
494
|
+
|
|
495
|
+
if gum spin --title "Fetching template..." -- curl -sL "$url" -o .gitignore; then
|
|
496
|
+
# Verify the file is not empty (curl might return 404 text if URL is wrong)
|
|
497
|
+
if grep -q "404: Not Found" .gitignore; then
|
|
498
|
+
print_error "Failed to summon template: 404 Not Found at $url"
|
|
499
|
+
rm .gitignore
|
|
500
|
+
return 1
|
|
501
|
+
fi
|
|
502
|
+
print_status "✨ .gitignore for $template created successfully!"
|
|
503
|
+
else
|
|
504
|
+
print_error "Failed to summon template. Check your internet connection."
|
|
505
|
+
return 1
|
|
506
|
+
fi
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
# Function for Conventional Commits
|
|
510
|
+
commit_wizard() {
|
|
511
|
+
echo -e "${PURPLE}=== The Alchemist's Commit ===${NC}"
|
|
512
|
+
|
|
513
|
+
# 1. Select Type
|
|
514
|
+
local type=$(gum choose --header "Select change type:" \
|
|
515
|
+
"feat: A new feature" \
|
|
516
|
+
"fix: A bug fix" \
|
|
517
|
+
"docs: Documentation only changes" \
|
|
518
|
+
"style: Changes that do not affect the meaning of the code" \
|
|
519
|
+
"refactor: A code change that neither fixes a bug nor adds a feature" \
|
|
520
|
+
"perf: A code change that improves performance" \
|
|
521
|
+
"test: Adding missing tests or correcting existing tests" \
|
|
522
|
+
"chore: Changes to the build process or auxiliary tools")
|
|
523
|
+
|
|
524
|
+
# Extract just the type (e.g., "feat") from the selection
|
|
525
|
+
type=$(echo "$type" | cut -d: -f1)
|
|
526
|
+
[[ -z "$type" ]] && return 1
|
|
527
|
+
|
|
528
|
+
# 2. Scope (Optional)
|
|
529
|
+
local scope=$(gum input --placeholder "Scope (optional, e.g., 'login', 'ui'). Press Enter to skip.")
|
|
530
|
+
|
|
531
|
+
# 3. Short Description
|
|
532
|
+
local summary=$(gum input --placeholder "Short description (imperative mood, e.g., 'add generic login')")
|
|
533
|
+
[[ -z "$summary" ]] && print_warning "Summary is required!" && return 1
|
|
534
|
+
|
|
535
|
+
# 4. Long Description (Optional)
|
|
536
|
+
local body=""
|
|
537
|
+
if gum confirm "Add a longer description (body)?"; then
|
|
538
|
+
body=$(gum write --placeholder "Enter detailed description...")
|
|
539
|
+
fi
|
|
540
|
+
|
|
541
|
+
# 5. Breaking Change?
|
|
542
|
+
local breaking=""
|
|
543
|
+
if gum confirm "Is this a BREAKING CHANGE?"; then
|
|
544
|
+
breaking="!"
|
|
545
|
+
fi
|
|
546
|
+
|
|
547
|
+
# Construct the message
|
|
548
|
+
local commit_msg=""
|
|
549
|
+
if [[ -n "$scope" ]]; then
|
|
550
|
+
commit_msg="${type}(${scope})${breaking}: ${summary}"
|
|
551
|
+
else
|
|
552
|
+
commit_msg="${type}${breaking}: ${summary}"
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
# Append body if exists
|
|
556
|
+
if [[ -n "$body" ]]; then
|
|
557
|
+
commit_msg="${commit_msg}
|
|
558
|
+
|
|
559
|
+
${body}"
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
# Preview and Confirm
|
|
563
|
+
echo
|
|
564
|
+
gum style --border rounded --border-foreground "$BIMAGIC_PRIMARY" --padding "1 2" \
|
|
565
|
+
"PREVIEW:" "$commit_msg"
|
|
566
|
+
echo
|
|
567
|
+
|
|
568
|
+
if gum confirm "Commit with this message?"; then
|
|
569
|
+
git commit -m "$commit_msg"
|
|
570
|
+
print_status " Mischief managed! (Commit successful)"
|
|
571
|
+
else
|
|
572
|
+
print_warning "Commit cancelled."
|
|
573
|
+
fi
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#function to display git graph
|
|
577
|
+
pretty_git_log() {
|
|
578
|
+
git log --graph \
|
|
579
|
+
--abbrev-commit \
|
|
580
|
+
--decorate \
|
|
581
|
+
--date=short \
|
|
582
|
+
--format="%C(auto)%h%Creset %C(blue)%ad%Creset %C(green)%an%Creset %C(yellow)%d%Creset %Creset%s" \
|
|
583
|
+
--all
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
# Check if environment variables are set (optional for SSH)
|
|
587
|
+
if [[ -z "$GITHUB_USER" ]] || [[ -z "$GITHUB_TOKEN" ]]; then
|
|
588
|
+
print_warning "GITHUB_USER or GITHUB_TOKEN not set. Defaulting to SSH/Local mode."
|
|
589
|
+
fi
|
|
590
|
+
|
|
591
|
+
# Parse CLI arguments
|
|
592
|
+
CLI_MODE=""
|
|
593
|
+
CLI_URL=""
|
|
594
|
+
CLI_MSG=""
|
|
595
|
+
CLI_INTERACTIVE="false"
|
|
596
|
+
|
|
597
|
+
# Preserve args in a loop
|
|
598
|
+
while [[ $# -gt 0 ]]; do
|
|
599
|
+
case "$1" in
|
|
600
|
+
-d)
|
|
601
|
+
CLI_MODE="clone"
|
|
602
|
+
shift
|
|
603
|
+
;;
|
|
604
|
+
-i)
|
|
605
|
+
CLI_INTERACTIVE="true"
|
|
606
|
+
shift
|
|
607
|
+
;;
|
|
608
|
+
-z)
|
|
609
|
+
CLI_MODE="lazy"
|
|
610
|
+
shift
|
|
611
|
+
;;
|
|
612
|
+
-s)
|
|
613
|
+
CLI_MODE="status"
|
|
614
|
+
shift
|
|
615
|
+
;;
|
|
616
|
+
-u)
|
|
617
|
+
CLI_MODE="undo"
|
|
618
|
+
shift
|
|
619
|
+
;;
|
|
620
|
+
-g)
|
|
621
|
+
CLI_MODE="graph"
|
|
622
|
+
shift
|
|
623
|
+
;;
|
|
624
|
+
-a | --architect)
|
|
625
|
+
CLI_MODE="architect"
|
|
626
|
+
shift
|
|
627
|
+
;;
|
|
628
|
+
*)
|
|
629
|
+
# Argument handling based on current mode
|
|
630
|
+
if [[ "$CLI_MODE" == "clone" && -z "$CLI_URL" ]]; then
|
|
631
|
+
CLI_URL="$1"
|
|
632
|
+
elif [[ "$CLI_MODE" == "lazy" && -z "$CLI_MSG" ]]; then
|
|
633
|
+
CLI_MSG="$1"
|
|
634
|
+
fi
|
|
635
|
+
shift
|
|
636
|
+
;;
|
|
637
|
+
esac
|
|
638
|
+
done
|
|
639
|
+
|
|
640
|
+
# --- Mode Handlers ---
|
|
641
|
+
|
|
642
|
+
if [[ "$CLI_MODE" == "clone" ]]; then
|
|
643
|
+
if [[ -z "$CLI_URL" ]]; then
|
|
644
|
+
print_error "Error: Repository URL required with -d"
|
|
645
|
+
exit 1
|
|
646
|
+
fi
|
|
647
|
+
clone_repo "$CLI_URL" "$CLI_INTERACTIVE"
|
|
648
|
+
exit $?
|
|
649
|
+
fi
|
|
650
|
+
|
|
651
|
+
if [[ "$CLI_MODE" == "status" ]]; then
|
|
652
|
+
show_repo_status
|
|
653
|
+
exit 0
|
|
654
|
+
fi
|
|
655
|
+
|
|
656
|
+
if [[ "$CLI_MODE" == "graph" ]]; then
|
|
657
|
+
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
658
|
+
print_error "Not a git repository!"
|
|
659
|
+
exit 1
|
|
660
|
+
fi
|
|
661
|
+
pretty_git_log
|
|
662
|
+
exit 0
|
|
663
|
+
fi
|
|
664
|
+
|
|
665
|
+
if [[ "$CLI_MODE" == "architect" ]]; then
|
|
666
|
+
summon_gitignore
|
|
667
|
+
exit 0
|
|
668
|
+
fi
|
|
669
|
+
|
|
670
|
+
# Mode: Time Turner (Undo)
|
|
671
|
+
if [[ "$CLI_MODE" == "undo" ]]; then
|
|
672
|
+
print_status " Spinning the Time Turner..."
|
|
673
|
+
|
|
674
|
+
# Check if there is a commit to undo
|
|
675
|
+
if ! git rev-parse HEAD >/dev/null 2>&1; then
|
|
676
|
+
print_error "No commits to undo! This repo is empty."
|
|
677
|
+
exit 1
|
|
678
|
+
fi
|
|
679
|
+
|
|
680
|
+
# Check if we are on the initial commit (no parent)
|
|
681
|
+
is_initial_commit=false
|
|
682
|
+
if ! git rev-parse HEAD~1 >/dev/null 2>&1; then
|
|
683
|
+
is_initial_commit=true
|
|
684
|
+
fi
|
|
685
|
+
|
|
686
|
+
# Ask user for the type of undo
|
|
687
|
+
undo_type=$(gum choose --header "Select Undo Level:" \
|
|
688
|
+
"Soft (Undo commit, keep changes staged - Best for fixing typos)" \
|
|
689
|
+
"Mixed (Undo commit, keep changes unstaged - Best for splitting work)" \
|
|
690
|
+
"Hard (DESTROY changes - Revert to previous state)" \
|
|
691
|
+
"Cancel")
|
|
692
|
+
|
|
693
|
+
case "$undo_type" in
|
|
694
|
+
"Soft"*)
|
|
695
|
+
if [[ "$is_initial_commit" == "true" ]]; then
|
|
696
|
+
git update-ref -d HEAD
|
|
697
|
+
else
|
|
698
|
+
git reset --soft HEAD~1
|
|
699
|
+
fi
|
|
700
|
+
print_status "✨ Success! I undid the commit, but kept your files ready to commit again."
|
|
701
|
+
;;
|
|
702
|
+
"Mixed"*)
|
|
703
|
+
if [[ "$is_initial_commit" == "true" ]]; then
|
|
704
|
+
git update-ref -d HEAD
|
|
705
|
+
git rm --cached -r -q .
|
|
706
|
+
else
|
|
707
|
+
git reset HEAD~1
|
|
708
|
+
fi
|
|
709
|
+
print_status " Success! I undid the commit and unstaged the files."
|
|
710
|
+
;;
|
|
711
|
+
"Hard"*)
|
|
712
|
+
if gum confirm " DANGER: This deletes your work forever. Are you sure?"; then
|
|
713
|
+
if [[ "$is_initial_commit" == "true" ]]; then
|
|
714
|
+
# Unstage everything first, then clean to ensure files are deleted
|
|
715
|
+
git update-ref -d HEAD
|
|
716
|
+
git rm --cached -r -q . >/dev/null 2>&1
|
|
717
|
+
git clean -fd
|
|
718
|
+
else
|
|
719
|
+
git reset --hard HEAD~1
|
|
720
|
+
fi
|
|
721
|
+
print_status " Obliviate! The last commit and its changes are destroyed."
|
|
722
|
+
else
|
|
723
|
+
print_status "Operation cancelled."
|
|
724
|
+
fi
|
|
725
|
+
;;
|
|
726
|
+
*)
|
|
727
|
+
print_status "Mischief managed (Cancelled)."
|
|
728
|
+
;;
|
|
729
|
+
esac
|
|
730
|
+
exit 0
|
|
731
|
+
fi
|
|
732
|
+
|
|
733
|
+
if [[ "$CLI_MODE" == "lazy" ]]; then
|
|
734
|
+
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
735
|
+
print_error "Not a git repository!"
|
|
736
|
+
exit 1
|
|
737
|
+
fi
|
|
738
|
+
|
|
739
|
+
if [[ -z "$CLI_MSG" ]]; then
|
|
740
|
+
print_error "Error: Commit message required for Lazy Wizard (-z)"
|
|
741
|
+
echo "Usage: bimagic -z \"commit message\""
|
|
742
|
+
exit 1
|
|
743
|
+
fi
|
|
744
|
+
|
|
745
|
+
print_status " Lazy Wizard invoked!"
|
|
746
|
+
|
|
747
|
+
# 1. Add all changes
|
|
748
|
+
if gum spin --title "Adding files..." -- git add .; then
|
|
749
|
+
print_status "Files added."
|
|
750
|
+
else
|
|
751
|
+
print_error "Failed to add files."
|
|
752
|
+
exit 1
|
|
753
|
+
fi
|
|
754
|
+
|
|
755
|
+
# 2. Commit
|
|
756
|
+
if git commit -m "$CLI_MSG"; then
|
|
757
|
+
print_status "Committed: $CLI_MSG"
|
|
758
|
+
else
|
|
759
|
+
print_error "Commit failed (nothing to commit?)"
|
|
760
|
+
exit 1
|
|
761
|
+
fi
|
|
762
|
+
|
|
763
|
+
# 3. Push
|
|
764
|
+
branch=$(get_current_branch)
|
|
765
|
+
print_status "Pushing to $branch..."
|
|
766
|
+
|
|
767
|
+
if gum spin --title "Pushing..." -- git push; then
|
|
768
|
+
print_status " Magic complete!"
|
|
769
|
+
else
|
|
770
|
+
# Try setting upstream if standard push failed
|
|
771
|
+
print_warning "Standard push failed. Trying to set upstream..."
|
|
772
|
+
if gum spin --title "Pushing (upstream)..." -- git push -u origin "$branch"; then
|
|
773
|
+
print_status " Magic complete (upstream set)!"
|
|
774
|
+
else
|
|
775
|
+
print_error "Push failed."
|
|
776
|
+
exit 1
|
|
777
|
+
fi
|
|
778
|
+
fi
|
|
779
|
+
exit 0
|
|
780
|
+
fi
|
|
781
|
+
|
|
782
|
+
CONFIG_DIR="$HOME/.config/bimagic"
|
|
783
|
+
VERSION_FILE="$CONFIG_DIR/version"
|
|
784
|
+
|
|
785
|
+
show_welcome_banner() {
|
|
786
|
+
clear
|
|
787
|
+
echo -e "$(get_ansi_esc "$BANNER_COLOR_1")▗▖ ▄ ▄▄▄▄ ▗▄▖ ▗▄▄▖▄ ▗▄▄▖\033[0m"
|
|
788
|
+
echo -e "$(get_ansi_esc "$BANNER_COLOR_2")▐▌ ▄ █ █ █ ▐▌ ▐▌▐▌ ▄ ▐▌ \033[0m"
|
|
789
|
+
echo -e "$(get_ansi_esc "$BANNER_COLOR_3")▐▛▀▚▖█ █ █ ▐▛▀▜▌▐▌▝▜▌█ ▐▌ \033[0m"
|
|
790
|
+
echo -e "$(get_ansi_esc "$BANNER_COLOR_4")▐▙▄▞▘█ ▐▌ ▐▌▝▚▄▞▘█ ▝▚▄▄▖\033[0m"
|
|
791
|
+
echo -e "$(get_ansi_esc "$BANNER_COLOR_5") \033[0m"
|
|
792
|
+
|
|
793
|
+
echo
|
|
794
|
+
gum style --foreground "$BIMAGIC_PRIMARY" --bold "✨ Welcome to Bimagic Git Wizard $VERSION ✨"
|
|
795
|
+
echo
|
|
796
|
+
|
|
797
|
+
if [[ ! -f "$VERSION_FILE" ]]; then
|
|
798
|
+
gum style --foreground "$BIMAGIC_SUCCESS" "It looks like this is your first time using Bimagic! Let's cast some spells."
|
|
799
|
+
else
|
|
800
|
+
gum style --foreground "$BIMAGIC_SUCCESS" "Bimagic has been updated to $VERSION! Enjoy the new magic."
|
|
801
|
+
fi
|
|
802
|
+
echo
|
|
803
|
+
|
|
804
|
+
# Save the current version so this doesn't show again until the next update
|
|
805
|
+
mkdir -p "$CONFIG_DIR"
|
|
806
|
+
echo "$VERSION" >"$VERSION_FILE"
|
|
807
|
+
|
|
808
|
+
gum style --foreground "$BIMAGIC_MUTED" "Press Enter to open the spellbook..."
|
|
809
|
+
read -r </dev/tty
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if [[ -z "$CLI_MODE" ]]; then
|
|
813
|
+
# Check if the version file doesn't exist, OR if it doesn't match the current version
|
|
814
|
+
if [[ ! -f "$VERSION_FILE" ]] || [[ "$(cat "$VERSION_FILE")" != "$VERSION" ]]; then
|
|
815
|
+
show_welcome_banner
|
|
816
|
+
fi
|
|
817
|
+
fi
|
|
818
|
+
|
|
819
|
+
while true; do
|
|
820
|
+
clear
|
|
821
|
+
show_repo_status # <-- shows the status dashboard at the top
|
|
822
|
+
# gum menu
|
|
823
|
+
choice=$(
|
|
824
|
+
gum choose --header " Choose your spell: (j/k to navigate)" \
|
|
825
|
+
--cursor " " --cursor.foreground "$BIMAGIC_PRIMARY" \
|
|
826
|
+
" Clone repository" \
|
|
827
|
+
" Init new repo" \
|
|
828
|
+
" Add files" \
|
|
829
|
+
" Commit changes" \
|
|
830
|
+
" Push to remote" \
|
|
831
|
+
" Pull latest changes" \
|
|
832
|
+
" Create/switch branch" \
|
|
833
|
+
" Set remote" \
|
|
834
|
+
" Show status" \
|
|
835
|
+
" Contributor Statistics" \
|
|
836
|
+
" Git graph" \
|
|
837
|
+
" Summon the Architect (.gitignore)" \
|
|
838
|
+
" Remove files/folders (rm)" \
|
|
839
|
+
" Merge branches" \
|
|
840
|
+
" Uninitialize repo" \
|
|
841
|
+
" Revert commit(s)" \
|
|
842
|
+
" Stash operations" \
|
|
843
|
+
" Exit"
|
|
844
|
+
)
|
|
845
|
+
echo
|
|
846
|
+
|
|
847
|
+
case "$choice" in
|
|
848
|
+
" Clone repository")
|
|
849
|
+
repo_url=$(gum input --placeholder "Enter repository URL")
|
|
850
|
+
if [[ -z "$repo_url" ]]; then continue; fi
|
|
851
|
+
|
|
852
|
+
clone_mode=$(gum choose "Standard Clone" "Interactive (Select files)")
|
|
853
|
+
|
|
854
|
+
if [[ "$clone_mode" == "Interactive (Select files)" ]]; then
|
|
855
|
+
clone_repo "$repo_url" "true"
|
|
856
|
+
else
|
|
857
|
+
clone_repo "$repo_url" "false"
|
|
858
|
+
fi
|
|
859
|
+
;;
|
|
860
|
+
" Stash operations")
|
|
861
|
+
while true; do
|
|
862
|
+
stash_choice=$(gum choose --header "Stash Operations" \
|
|
863
|
+
" Push (Save) changes" \
|
|
864
|
+
" Pop latest stash" \
|
|
865
|
+
" List stashes" \
|
|
866
|
+
" Apply specific stash" \
|
|
867
|
+
" Drop specific stash" \
|
|
868
|
+
" Clear all stashes" \
|
|
869
|
+
" Back")
|
|
870
|
+
|
|
871
|
+
case "$stash_choice" in
|
|
872
|
+
" Push (Save) changes")
|
|
873
|
+
msg=$(gum input --placeholder "Optional stash message")
|
|
874
|
+
# Ask about untracked files
|
|
875
|
+
if gum confirm "Include untracked files?"; then
|
|
876
|
+
include_untracked="-u"
|
|
877
|
+
else
|
|
878
|
+
include_untracked=""
|
|
879
|
+
fi
|
|
880
|
+
|
|
881
|
+
if git stash push $include_untracked -m "$msg"; then
|
|
882
|
+
print_status "Changes stashed successfully!"
|
|
883
|
+
else
|
|
884
|
+
print_error "Failed to stash changes."
|
|
885
|
+
fi
|
|
886
|
+
;;
|
|
887
|
+
" Pop latest stash")
|
|
888
|
+
if git stash pop; then
|
|
889
|
+
print_status "Stash popped successfully!"
|
|
890
|
+
else
|
|
891
|
+
print_error "Failed to pop stash (possible conflicts)."
|
|
892
|
+
fi
|
|
893
|
+
;;
|
|
894
|
+
" List stashes")
|
|
895
|
+
if [[ -z "$(git stash list)" ]]; then
|
|
896
|
+
print_warning "No stashes found."
|
|
897
|
+
else
|
|
898
|
+
git stash list | gum style --border normal --padding "0 1"
|
|
899
|
+
fi
|
|
900
|
+
;;
|
|
901
|
+
" Apply specific stash")
|
|
902
|
+
if [[ -z "$(git stash list)" ]]; then
|
|
903
|
+
print_warning "No stashes found."
|
|
904
|
+
continue
|
|
905
|
+
fi
|
|
906
|
+
|
|
907
|
+
stash_entry=$(git stash list | gum filter --placeholder "Select stash to apply")
|
|
908
|
+
if [[ -n "$stash_entry" ]]; then
|
|
909
|
+
stash_id=$(echo "$stash_entry" | cut -d: -f1)
|
|
910
|
+
if git stash apply "$stash_id"; then
|
|
911
|
+
print_status "Applied $stash_id"
|
|
912
|
+
else
|
|
913
|
+
print_error "Failed to apply $stash_id"
|
|
914
|
+
fi
|
|
915
|
+
fi
|
|
916
|
+
;;
|
|
917
|
+
" Drop specific stash")
|
|
918
|
+
if [[ -z "$(git stash list)" ]]; then
|
|
919
|
+
print_warning "No stashes found."
|
|
920
|
+
continue
|
|
921
|
+
fi
|
|
922
|
+
|
|
923
|
+
stash_entry=$(git stash list | gum filter --placeholder "Select stash to drop")
|
|
924
|
+
if [[ -n "$stash_entry" ]]; then
|
|
925
|
+
stash_id=$(echo "$stash_entry" | cut -d: -f1)
|
|
926
|
+
if gum confirm "Are you sure you want to drop $stash_id?"; then
|
|
927
|
+
if git stash drop "$stash_id"; then
|
|
928
|
+
print_status "Dropped $stash_id"
|
|
929
|
+
else
|
|
930
|
+
print_error "Failed to drop $stash_id"
|
|
931
|
+
fi
|
|
932
|
+
fi
|
|
933
|
+
fi
|
|
934
|
+
;;
|
|
935
|
+
" Clear all stashes")
|
|
936
|
+
if [[ -z "$(git stash list)" ]]; then
|
|
937
|
+
print_warning "No stashes found."
|
|
938
|
+
continue
|
|
939
|
+
fi
|
|
940
|
+
|
|
941
|
+
if gum confirm "DANGER: This will delete ALL stashes. Continue?"; then
|
|
942
|
+
if git stash clear; then
|
|
943
|
+
print_status "All stashes cleared."
|
|
944
|
+
else
|
|
945
|
+
print_error "Failed to clear stashes."
|
|
946
|
+
fi
|
|
947
|
+
else
|
|
948
|
+
print_status "Operation cancelled."
|
|
949
|
+
fi
|
|
950
|
+
;;
|
|
951
|
+
" Back")
|
|
952
|
+
break
|
|
953
|
+
;;
|
|
954
|
+
esac
|
|
955
|
+
echo
|
|
956
|
+
gum style --foreground "$BIMAGIC_MUTED" "Press Enter to continue..."
|
|
957
|
+
read -r </dev/tty
|
|
958
|
+
done
|
|
959
|
+
;;
|
|
960
|
+
" Init new repo")
|
|
961
|
+
dirname=$(gum input --placeholder "Enter repo directory name (or '.' for current dir)")
|
|
962
|
+
if [[ -z "$dirname" ]]; then
|
|
963
|
+
print_warning "Operation cancelled."
|
|
964
|
+
continue
|
|
965
|
+
fi
|
|
966
|
+
|
|
967
|
+
if [[ "$dirname" == "." ]]; then
|
|
968
|
+
git init
|
|
969
|
+
print_status "Repo initialized in current directory: $(pwd)"
|
|
970
|
+
else
|
|
971
|
+
mkdir -p "$dirname"
|
|
972
|
+
# Run init and rename in a subshell to avoid directory tracking issues
|
|
973
|
+
(
|
|
974
|
+
cd "$dirname" || exit 1
|
|
975
|
+
git init
|
|
976
|
+
current_branch=$(git symbolic-ref --short HEAD 2>/dev/null)
|
|
977
|
+
if [[ "$current_branch" == "master" ]]; then
|
|
978
|
+
git branch -M main
|
|
979
|
+
echo "Default branch renamed from 'master' to 'main' in $dirname"
|
|
980
|
+
fi
|
|
981
|
+
)
|
|
982
|
+
print_status "Repo initialized in new directory: $dirname"
|
|
983
|
+
fi
|
|
984
|
+
;;
|
|
985
|
+
" Add files")
|
|
986
|
+
# Show untracked + modified files, plus an [ALL] option
|
|
987
|
+
files=$( (
|
|
988
|
+
echo "[ALL]"
|
|
989
|
+
git ls-files --others --modified --exclude-standard
|
|
990
|
+
) | gum filter --no-limit --placeholder "Select files to add")
|
|
991
|
+
|
|
992
|
+
if [[ -z "$files" ]]; then
|
|
993
|
+
print_warning "No files selected."
|
|
994
|
+
else
|
|
995
|
+
# gum filter returns selected items separated by newlines.
|
|
996
|
+
# We need to handle the case where [ALL] is selected along with other files.
|
|
997
|
+
if echo "$files" | grep -q "\[ALL\]"; then
|
|
998
|
+
git add .
|
|
999
|
+
print_status "All files staged."
|
|
1000
|
+
else
|
|
1001
|
+
# Correctly handle filenames with spaces by reading line by line
|
|
1002
|
+
echo "$files" | while read -r f; do
|
|
1003
|
+
[[ -n "$f" ]] && git add "$f"
|
|
1004
|
+
done
|
|
1005
|
+
print_status "Selected files staged."
|
|
1006
|
+
# Optionally, list the files that were staged
|
|
1007
|
+
echo "$files"
|
|
1008
|
+
fi
|
|
1009
|
+
fi
|
|
1010
|
+
;;
|
|
1011
|
+
" Commit changes")
|
|
1012
|
+
commit_mode=$(gum choose " Magic Commit (Builder)" " Quick Commit (One-line)")
|
|
1013
|
+
|
|
1014
|
+
if [[ "$commit_mode" == " Magic Commit (Builder)" ]]; then
|
|
1015
|
+
commit_wizard
|
|
1016
|
+
else
|
|
1017
|
+
msg=$(gum input --placeholder "Enter commit message")
|
|
1018
|
+
if [[ -z "$msg" ]]; then
|
|
1019
|
+
print_warning "No commit message provided. Cancelled."
|
|
1020
|
+
continue
|
|
1021
|
+
fi
|
|
1022
|
+
|
|
1023
|
+
if gum confirm "Commit changes?"; then
|
|
1024
|
+
git commit -m "$msg"
|
|
1025
|
+
print_status "Commit done!"
|
|
1026
|
+
else
|
|
1027
|
+
print_status "Commit cancelled."
|
|
1028
|
+
fi
|
|
1029
|
+
fi
|
|
1030
|
+
;;
|
|
1031
|
+
" Push to remote")
|
|
1032
|
+
branch=$(git symbolic-ref --short HEAD 2>/dev/null)
|
|
1033
|
+
branch=${branch:-main}
|
|
1034
|
+
|
|
1035
|
+
remotes=$(git remote)
|
|
1036
|
+
|
|
1037
|
+
if [[ -z "$remotes" ]]; then
|
|
1038
|
+
print_error "No remote set!"
|
|
1039
|
+
if setup_remote "origin"; then
|
|
1040
|
+
remote="origin"
|
|
1041
|
+
else
|
|
1042
|
+
continue
|
|
1043
|
+
fi
|
|
1044
|
+
else
|
|
1045
|
+
# Select remote if multiple exist
|
|
1046
|
+
if [[ $(echo "$remotes" | wc -l) -eq 1 ]]; then
|
|
1047
|
+
remote="$remotes"
|
|
1048
|
+
else
|
|
1049
|
+
remote=$(echo "$remotes" | gum filter --placeholder "Select remote to push to")
|
|
1050
|
+
fi
|
|
1051
|
+
fi
|
|
1052
|
+
|
|
1053
|
+
[[ -z "$remote" ]] && continue
|
|
1054
|
+
|
|
1055
|
+
if gum confirm "Push branch '$branch' to '$remote'?"; then
|
|
1056
|
+
echo "Pushing branch '$branch' to '$remote'..."
|
|
1057
|
+
gum spin --title "Pushing..." -- git push -u "$remote" "$branch"
|
|
1058
|
+
else
|
|
1059
|
+
print_status "Push cancelled."
|
|
1060
|
+
fi
|
|
1061
|
+
;;
|
|
1062
|
+
" Pull latest changes")
|
|
1063
|
+
# Auto-fetch before pulling
|
|
1064
|
+
if gum spin --title "Fetching updates..." -- git fetch --all; then
|
|
1065
|
+
print_status "Fetch complete."
|
|
1066
|
+
else
|
|
1067
|
+
print_warning "Fetch encountered issues."
|
|
1068
|
+
fi
|
|
1069
|
+
|
|
1070
|
+
pull_choice=$(gum choose --header "Select pull mode" \
|
|
1071
|
+
"Pull specific branch" \
|
|
1072
|
+
"Pull all")
|
|
1073
|
+
|
|
1074
|
+
case "$pull_choice" in
|
|
1075
|
+
"Pull all")
|
|
1076
|
+
if gum confirm "Run 'git pull --all'?"; then
|
|
1077
|
+
gum spin --title "Pulling all..." -- git pull --all
|
|
1078
|
+
print_status "Pull all complete."
|
|
1079
|
+
else
|
|
1080
|
+
print_status "Pull cancelled."
|
|
1081
|
+
fi
|
|
1082
|
+
;;
|
|
1083
|
+
"Pull specific branch")
|
|
1084
|
+
branch=$(gum input --value "main" --placeholder "Enter branch to pull")
|
|
1085
|
+
branch=${branch:-main}
|
|
1086
|
+
|
|
1087
|
+
remotes=$(git remote 2>/dev/null)
|
|
1088
|
+
if [[ -z "$remotes" ]]; then
|
|
1089
|
+
print_error "No remote set! Cannot pull."
|
|
1090
|
+
else
|
|
1091
|
+
# Select remote if multiple exist
|
|
1092
|
+
if [[ $(echo "$remotes" | wc -l) -eq 1 ]]; then
|
|
1093
|
+
remote="$remotes"
|
|
1094
|
+
else
|
|
1095
|
+
remote=$(echo "$remotes" | gum filter --placeholder "Select remote to pull from")
|
|
1096
|
+
fi
|
|
1097
|
+
|
|
1098
|
+
if [[ -n "$remote" ]]; then
|
|
1099
|
+
if gum confirm "Pull branch '$branch' from '$remote'?"; then
|
|
1100
|
+
gum spin --title "Pulling..." -- git pull "$remote" "$branch"
|
|
1101
|
+
else
|
|
1102
|
+
print_status "Pull cancelled."
|
|
1103
|
+
fi
|
|
1104
|
+
else
|
|
1105
|
+
print_warning "No remote selected."
|
|
1106
|
+
fi
|
|
1107
|
+
fi
|
|
1108
|
+
;;
|
|
1109
|
+
esac
|
|
1110
|
+
;;
|
|
1111
|
+
" Create/switch branch")
|
|
1112
|
+
# Show current branch and all available branches
|
|
1113
|
+
current_branch=$(get_current_branch)
|
|
1114
|
+
print_status "Current branch: $current_branch"
|
|
1115
|
+
echo
|
|
1116
|
+
print_status "Available branches:"
|
|
1117
|
+
show_branches
|
|
1118
|
+
echo
|
|
1119
|
+
|
|
1120
|
+
# Let user choose between existing branches or new branch
|
|
1121
|
+
branch_option=$(gum choose "Switch to existing branch" "Create new branch")
|
|
1122
|
+
|
|
1123
|
+
case "$branch_option" in
|
|
1124
|
+
"Switch to existing branch")
|
|
1125
|
+
# Use gum filter to select from existing branches
|
|
1126
|
+
existing_branch=$(git branch --format='%(refname:short)' | gum filter --placeholder "Select branch to switch to")
|
|
1127
|
+
if [[ -n "$existing_branch" ]]; then
|
|
1128
|
+
git checkout "$existing_branch"
|
|
1129
|
+
print_status "Switched to branch: $existing_branch"
|
|
1130
|
+
else
|
|
1131
|
+
print_warning "No branch selected."
|
|
1132
|
+
fi
|
|
1133
|
+
;;
|
|
1134
|
+
"Create new branch")
|
|
1135
|
+
new_branch=$(gum input --placeholder "Enter new branch name")
|
|
1136
|
+
if [[ -n "$new_branch" ]]; then
|
|
1137
|
+
git checkout -b "$new_branch"
|
|
1138
|
+
print_status "Created and switched to new branch: $new_branch"
|
|
1139
|
+
else
|
|
1140
|
+
print_error "No branch name provided."
|
|
1141
|
+
fi
|
|
1142
|
+
;;
|
|
1143
|
+
*)
|
|
1144
|
+
print_warning "Operation cancelled."
|
|
1145
|
+
;;
|
|
1146
|
+
esac
|
|
1147
|
+
;;
|
|
1148
|
+
" Set remote")
|
|
1149
|
+
setup_remote "origin"
|
|
1150
|
+
;;
|
|
1151
|
+
" Show status")
|
|
1152
|
+
git status
|
|
1153
|
+
;;
|
|
1154
|
+
" Remove files/folders (rm)")
|
|
1155
|
+
# List tracked + untracked files for removal
|
|
1156
|
+
files=$(git ls-files --cached --others --exclude-standard |
|
|
1157
|
+
gum filter --no-limit --placeholder "Select files/folders to remove")
|
|
1158
|
+
|
|
1159
|
+
if [[ -z "$files" ]]; then
|
|
1160
|
+
print_warning "No files selected."
|
|
1161
|
+
continue # Use continue to go back to the main menu
|
|
1162
|
+
fi
|
|
1163
|
+
|
|
1164
|
+
echo "Files selected for removal:"
|
|
1165
|
+
# Use 'tput' for better visual separation and color
|
|
1166
|
+
tput setaf 3
|
|
1167
|
+
echo "$files"
|
|
1168
|
+
tput sgr0
|
|
1169
|
+
echo
|
|
1170
|
+
|
|
1171
|
+
if gum confirm "Confirm removal? This cannot be undone."; then
|
|
1172
|
+
# Use a 'while read' loop to correctly handle filenames with spaces
|
|
1173
|
+
echo "$files" | while read -r f; do
|
|
1174
|
+
if [[ -z "$f" ]]; then continue; fi # Skip empty lines
|
|
1175
|
+
|
|
1176
|
+
# Check if the file is tracked by git
|
|
1177
|
+
if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
|
|
1178
|
+
# It's a tracked file, use 'git rm'
|
|
1179
|
+
git rm -rf "$f"
|
|
1180
|
+
else
|
|
1181
|
+
# It's an untracked file, use 'rm'
|
|
1182
|
+
rm -rf "$f"
|
|
1183
|
+
fi
|
|
1184
|
+
done
|
|
1185
|
+
print_status "Selected files/folders have been removed."
|
|
1186
|
+
else
|
|
1187
|
+
print_status "Operation cancelled."
|
|
1188
|
+
fi
|
|
1189
|
+
;;
|
|
1190
|
+
" Uninitialize repo")
|
|
1191
|
+
print_warning "This will completely uninitialize the Git repository in this folder."
|
|
1192
|
+
echo "This action will delete the .git directory and cannot be undone!"
|
|
1193
|
+
echo
|
|
1194
|
+
|
|
1195
|
+
if gum confirm "Are you sure you want to continue?"; then
|
|
1196
|
+
if [ -d ".git" ]; then
|
|
1197
|
+
rm -rf .git
|
|
1198
|
+
print_status "Git repository has been uninitialized."
|
|
1199
|
+
else
|
|
1200
|
+
print_error "No .git directory found here. Nothing to do."
|
|
1201
|
+
fi
|
|
1202
|
+
else
|
|
1203
|
+
print_status "Operation cancelled."
|
|
1204
|
+
fi
|
|
1205
|
+
;;
|
|
1206
|
+
" Exit")
|
|
1207
|
+
if gum confirm "Are you sure you want to exit?"; then
|
|
1208
|
+
echo "Git Wizard vanishes in a puff of smoke..."
|
|
1209
|
+
exit 0
|
|
1210
|
+
else
|
|
1211
|
+
continue
|
|
1212
|
+
fi
|
|
1213
|
+
;;
|
|
1214
|
+
" Merge branches")
|
|
1215
|
+
current_branch=$(get_current_branch)
|
|
1216
|
+
print_status "You are on branch: $current_branch"
|
|
1217
|
+
echo
|
|
1218
|
+
|
|
1219
|
+
# Pick branch to merge from
|
|
1220
|
+
merge_branch=$(git branch --format='%(refname:short)' |
|
|
1221
|
+
grep -v "^$current_branch$" |
|
|
1222
|
+
gum filter --placeholder "Select branch to merge into $current_branch")
|
|
1223
|
+
|
|
1224
|
+
if [[ -z "$merge_branch" ]]; then
|
|
1225
|
+
print_warning "No branch selected. Merge cancelled."
|
|
1226
|
+
else
|
|
1227
|
+
if gum confirm "Merge branch '$merge_branch' into '$current_branch'?"; then
|
|
1228
|
+
echo "Merging branch '$merge_branch' into '$current_branch'..."
|
|
1229
|
+
if gum spin --title "Merging..." -- git merge "$merge_branch"; then
|
|
1230
|
+
print_status "Merge successful!"
|
|
1231
|
+
else
|
|
1232
|
+
print_error "Merge had conflicts! Resolve them manually."
|
|
1233
|
+
fi
|
|
1234
|
+
else
|
|
1235
|
+
print_status "Merge cancelled."
|
|
1236
|
+
fi
|
|
1237
|
+
fi
|
|
1238
|
+
;;
|
|
1239
|
+
" Contributor Statistics")
|
|
1240
|
+
show_contributor_stats
|
|
1241
|
+
;;
|
|
1242
|
+
|
|
1243
|
+
" Summon the Architect (.gitignore)")
|
|
1244
|
+
summon_gitignore
|
|
1245
|
+
;;
|
|
1246
|
+
|
|
1247
|
+
" Revert commit(s)")
|
|
1248
|
+
print_status "Fetching commit history..."
|
|
1249
|
+
echo
|
|
1250
|
+
|
|
1251
|
+
# Show commits as "<hash> <message>" but keep hash separately
|
|
1252
|
+
commits=$(git log --oneline --decorate |
|
|
1253
|
+
gum filter --no-limit --placeholder "Select commit(s) to revert" |
|
|
1254
|
+
awk '{print $1}')
|
|
1255
|
+
|
|
1256
|
+
if [[ -z "$commits" ]]; then
|
|
1257
|
+
print_warning "No commit selected. Revert cancelled."
|
|
1258
|
+
continue
|
|
1259
|
+
fi
|
|
1260
|
+
|
|
1261
|
+
echo "You selected:"
|
|
1262
|
+
echo "$commits"
|
|
1263
|
+
echo
|
|
1264
|
+
|
|
1265
|
+
if gum confirm "Confirm revert?"; then
|
|
1266
|
+
for c in $commits; do
|
|
1267
|
+
echo "Reverting commit $c..."
|
|
1268
|
+
if git revert --no-edit "$c"; then
|
|
1269
|
+
print_status "Commit $c reverted."
|
|
1270
|
+
else
|
|
1271
|
+
print_error "Conflict occurred while reverting $c!"
|
|
1272
|
+
echo "Please resolve conflicts, then run:"
|
|
1273
|
+
echo " git revert --continue"
|
|
1274
|
+
break
|
|
1275
|
+
fi
|
|
1276
|
+
done
|
|
1277
|
+
else
|
|
1278
|
+
print_status "Revert cancelled."
|
|
1279
|
+
fi
|
|
1280
|
+
;;
|
|
1281
|
+
|
|
1282
|
+
" Git graph")
|
|
1283
|
+
color=${YELLOW}
|
|
1284
|
+
line1="Git graph"
|
|
1285
|
+
line2="[INFO] press 'q' to exit"
|
|
1286
|
+
|
|
1287
|
+
echo -e "${color}╭$(printf '─%.0s' $(seq 1 30))╮${NC}"
|
|
1288
|
+
printf "${color}│${NC} %-*s ${color}│${NC}\n" 28 "$line1"
|
|
1289
|
+
printf "${color}│${NC} %-*s ${color}│${NC}\n" 28 "$line2"
|
|
1290
|
+
echo -e "${color}╰$(printf '─%.0s' $(seq 1 30))╯${NC}"
|
|
1291
|
+
|
|
1292
|
+
# Use gum spin while loading the graph.
|
|
1293
|
+
# Since git log can be long, we pipe it to less if it's too big,
|
|
1294
|
+
# but gum spin doesn't work well with interactive pagers.
|
|
1295
|
+
# However, for a simple "pretty log", we can just run it.
|
|
1296
|
+
gum spin --title "Drawing git graph..." -- sleep 2
|
|
1297
|
+
pretty_git_log
|
|
1298
|
+
;;
|
|
1299
|
+
|
|
1300
|
+
*)
|
|
1301
|
+
print_error "Invalid choice! Try again."
|
|
1302
|
+
echo "Git Wizard vanishes in a puff of smoke..."
|
|
1303
|
+
break
|
|
1304
|
+
;;
|
|
1305
|
+
esac
|
|
1306
|
+
|
|
1307
|
+
echo
|
|
1308
|
+
gum style --foreground "$BIMAGIC_MUTED" "Press Enter to continue..."
|
|
1309
|
+
read -r </dev/tty
|
|
1310
|
+
done
|