Skip to content

Commit 442eef2

Browse files
dean0xDean Sharon
andauthored
Fix clone/exec bugs and improve clone UX (#4)
- Fix clone crash on empty array with set -u (unbound variable) - Fix exec arg parsing: --tag flags no longer swallowed into command string - Add SIGPIPE trap for pipe-friendly output (mars status | head) - Rewrite clone with per-repo spinner feedback instead of silent parallel - Spinner no-ops when stdout isn't a terminal to prevent SIGPIPE in pipes - Re-record demo GIF with working clone flow Co-authored-by: Dean Sharon <deanshrn@gmain.com>
1 parent 646dfa3 commit 442eef2

File tree

8 files changed

+120
-225
lines changed

8 files changed

+120
-225
lines changed

build.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ cat > "$OUTPUT_FILE" << 'HEADER'
1919
2020
set -euo pipefail
2121
22+
# Exit cleanly on SIGPIPE (e.g., mars clone | grep, mars status | head)
23+
trap 'exit 0' PIPE
24+
2225
MARS_VERSION="0.1.1"
2326
2427
HEADER

demo.gif

200 KB
Loading

demo.tape

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ Type "mars add https://github.com/dean0x/mars-example-shared.git --tags shared"
4343
Enter
4444
Sleep 1.5s
4545

46-
# Step 3: Clone all repos
46+
# Step 3: Clone all repos (spinners show per-repo progress)
4747
Type "mars clone"
4848
Enter
49-
Sleep 4s
49+
Sleep 8s
5050

5151
# Step 4: Check status
5252
Type "mars status"

dist/mars

Lines changed: 57 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
set -euo pipefail
77

8+
# Exit cleanly on SIGPIPE (e.g., mars clone | grep, mars status | head)
9+
trap 'exit 0' PIPE
10+
811
MARS_VERSION="0.1.1"
912

1013

@@ -289,6 +292,11 @@ ui_spinner_start() {
289292
local message="$1"
290293
_SPINNER_MSG="$message"
291294

295+
# Skip spinner animation when stdout is not a terminal (piped/redirected)
296+
if [[ ! -t 1 ]]; then
297+
return
298+
fi
299+
292300
(
293301
local i=0
294302
local frame_count=${#S_SPINNER_FRAMES[@]}
@@ -314,8 +322,10 @@ ui_spinner_stop() {
314322
fi
315323
_SPINNER_PID=""
316324

317-
# Clear spinner line
318-
printf '\r\033[K'
325+
# Clear spinner line only when connected to a terminal
326+
if [[ -t 1 ]]; then
327+
printf '\r\033[K'
328+
fi
319329

320330
# Show final message if provided
321331
if [[ -n "$final_message" ]]; then
@@ -332,7 +342,9 @@ ui_spinner_error() {
332342
fi
333343
_SPINNER_PID=""
334344

335-
printf '\r\033[K'
345+
if [[ -t 1 ]]; then
346+
printf '\r\033[K'
347+
fi
336348
ui_step_error "$message"
337349
}
338350

@@ -1140,10 +1152,7 @@ EOF
11401152

11411153
# === lib/commands/clone.sh ===
11421154
# Mars CLI - clone command
1143-
# Clone configured repositories with parallel execution
1144-
1145-
# Maximum concurrent clone jobs
1146-
CLONE_PARALLEL_LIMIT=4
1155+
# Clone configured repositories with per-repo progress
11471156

11481157
cmd_clone() {
11491158
local tag=""
@@ -1184,142 +1193,75 @@ cmd_clone() {
11841193
return 1
11851194
fi
11861195

1187-
# Count repos
1196+
# Count total repos
11881197
local total=0
1189-
local to_clone=()
1190-
local already_cloned=()
1191-
11921198
while IFS= read -r repo; do
11931199
[[ -z "$repo" ]] && continue
11941200
total=$((total + 1))
1195-
1196-
local path
1197-
path=$(yaml_get_path "$repo")
1198-
local full_path="$MARS_REPOS_DIR/$path"
1199-
1200-
if [[ -d "$full_path" ]] && [[ $force -eq 0 ]]; then
1201-
already_cloned+=("$repo")
1202-
else
1203-
to_clone+=("$repo")
1204-
fi
12051201
done <<< "$repos"
12061202

1207-
# Report already cloned
1208-
for repo in "${already_cloned[@]}"; do
1209-
local path
1210-
path=$(yaml_get_path "$repo")
1211-
ui_step_done "Already cloned:" "$path"
1212-
done
1213-
1214-
if [[ ${#to_clone[@]} -eq 0 ]]; then
1215-
ui_outro "All repositories already cloned"
1216-
return 0
1217-
fi
1218-
1219-
ui_bar_line
1220-
ui_info "Cloning ${#to_clone[@]} of $total repositories..."
1221-
ui_bar_line
1222-
1223-
# Clone with parallelism
1224-
local pids=()
1225-
local repo_for_pid=()
1203+
local current=0
12261204
local success_count=0
1205+
local skip_count=0
12271206
local fail_count=0
1228-
local failed_repos=()
12291207

1230-
for repo in "${to_clone[@]}"; do
1208+
# Clone each repo with spinner feedback
1209+
while IFS= read -r repo; do
1210+
[[ -z "$repo" ]] && continue
1211+
current=$((current + 1))
1212+
12311213
local url
12321214
url=$(yaml_get_url "$repo")
12331215
local path
12341216
path=$(yaml_get_path "$repo")
12351217
local full_path="$MARS_REPOS_DIR/$path"
12361218

1219+
# Already cloned?
1220+
if [[ -d "$full_path" ]] && [[ $force -eq 0 ]]; then
1221+
ui_step_done "Already cloned:" "$path"
1222+
skip_count=$((skip_count + 1))
1223+
continue
1224+
fi
1225+
12371226
# Remove existing directory if force
12381227
if [[ -d "$full_path" ]] && [[ $force -eq 1 ]]; then
12391228
rm -rf "$full_path"
12401229
fi
12411230

1242-
# Wait if at parallel limit
1243-
while [[ ${#pids[@]} -ge $CLONE_PARALLEL_LIMIT ]]; do
1244-
_clone_wait_one
1245-
done
1231+
# Show spinner while cloning
1232+
ui_spinner_start "Cloning $path... ($current/$total)"
12461233

1247-
# Start clone in background
1248-
(
1249-
if git clone --quiet "$url" "$full_path" 2>/dev/null; then
1250-
exit 0
1251-
else
1252-
exit 1
1234+
local clone_err
1235+
if clone_err=$(git clone --quiet "$url" "$full_path" 2>&1); then
1236+
ui_spinner_stop
1237+
ui_step_done "Cloned:" "$path"
1238+
success_count=$((success_count + 1))
1239+
else
1240+
ui_spinner_error "Failed to clone: $path"
1241+
if [[ -n "$clone_err" ]]; then
1242+
ui_info "$(ui_dim "$clone_err")"
12531243
fi
1254-
) &
1255-
1256-
pids+=($!)
1257-
repo_for_pid+=("$repo")
1258-
done
1259-
1260-
# Wait for remaining jobs
1261-
while [[ ${#pids[@]} -gt 0 ]]; do
1262-
_clone_wait_one
1263-
done
1244+
fail_count=$((fail_count + 1))
1245+
fi
1246+
done <<< "$repos"
12641247

12651248
# Summary
12661249
ui_bar_line
12671250

12681251
if [[ $fail_count -eq 0 ]]; then
1269-
ui_outro "Cloned $success_count repositories successfully"
1252+
local msg="Cloned $success_count repositories successfully"
1253+
if [[ $skip_count -gt 0 ]]; then
1254+
msg="$msg, $skip_count already cloned"
1255+
fi
1256+
ui_outro "$msg"
12701257
else
1271-
for repo in "${failed_repos[@]}"; do
1272-
local path
1273-
path=$(yaml_get_path "$repo")
1274-
ui_step_error "Failed: $path"
1275-
done
12761258
ui_outro_cancel "Cloned $success_count, failed $fail_count"
12771259
return 1
12781260
fi
12791261

12801262
return 0
12811263
}
12821264

1283-
# Helper to wait for one clone job
1284-
_clone_wait_one() {
1285-
if [[ ${#pids[@]} -eq 0 ]]; then
1286-
return
1287-
fi
1288-
1289-
# Wait for any process to complete
1290-
local pid
1291-
for i in "${!pids[@]}"; do
1292-
pid="${pids[$i]}"
1293-
if ! kill -0 "$pid" 2>/dev/null; then
1294-
# Process finished
1295-
wait "$pid"
1296-
local exit_code=$?
1297-
local repo="${repo_for_pid[$i]}"
1298-
local path
1299-
path=$(yaml_get_path "$repo")
1300-
1301-
if [[ $exit_code -eq 0 ]]; then
1302-
ui_step_done "Cloned:" "$path"
1303-
success_count=$((success_count + 1))
1304-
else
1305-
ui_step_error "Failed to clone: $path"
1306-
fail_count=$((fail_count + 1))
1307-
failed_repos+=("$repo")
1308-
fi
1309-
1310-
# Remove from arrays
1311-
unset 'pids[i]'
1312-
unset 'repo_for_pid[i]'
1313-
pids=("${pids[@]}")
1314-
repo_for_pid=("${repo_for_pid[@]}")
1315-
return
1316-
fi
1317-
done
1318-
1319-
# If all still running, sleep briefly
1320-
sleep 0.1
1321-
}
1322-
13231265
# === lib/commands/status.sh ===
13241266
# Mars CLI - status command
13251267
# Show git status across all repositories
@@ -1767,9 +1709,13 @@ cmd_exec() {
17671709
return 1
17681710
;;
17691711
*)
1770-
# Everything else is the command
1771-
command="$*"
1772-
break
1712+
if [[ -z "$command" ]]; then
1713+
command="$1"
1714+
else
1715+
ui_step_error "Unexpected argument: $1"
1716+
return 1
1717+
fi
1718+
shift
17731719
;;
17741720
esac
17751721
done

0 commit comments

Comments
 (0)