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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +656 -0
  3. package/SECURITY.md +25 -0
  4. package/bimagic +1310 -0
  5. 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