bimagic 1.4.2 → 1.4.4

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