@@ -219,27 +219,36 @@ pub fn cmdRestart(allocator: std.mem.Allocator, args_iter: *platform_mod.ArgIter
219219 const origin_branch = std .fmt .allocPrint (allocator , "origin/{s}" , .{branch }) catch return error .OutOfMemory ;
220220 defer allocator .free (origin_branch );
221221
222- runSubcommand (allocator , &.{ "fetch" , "origin" }) catch | e | {
223- printErr (allocator , "FAILED: fetch origin\n " , .{});
224- return e ;
222+ const fetch_ok = blk : {
223+ runSubcommand (allocator , &.{ "fetch" , "origin" }) catch {
224+ // Fetch can fail if the remote is empty (no refs yet). That's OK —
225+ // this is the first push to a new repo. Skip merge and succeed.
226+ printErr (allocator , "note: fetch origin failed (remote may be empty), skipping merge\n " , .{});
227+ break :blk false ;
228+ };
229+ break :blk true ;
225230 };
226231
227- // Ensure tracking ref exists (ziggit clone/fetch may not create it)
228- ensureTrackingRef (branch );
229-
230- // Merge instead of rebase. This preserves all local commit SHAs so that
231- // `reset --hard HEAD~1` always goes to the previous local commit, not
232- // some unrelated point in history. For parallel agents this is critical:
233- // rebase rewrites their commits and creates orphaned refs.
234- runSubcommand (allocator , &.{ "merge" , origin_branch , "--no-edit" }) catch | e | {
235- // If merge conflicts, abort to leave the tree clean and let the
236- // caller (agent) know it needs to retry or resolve.
237- printErr (allocator , "FAILED: merge {s} (conflict?) — aborting merge\n " , .{origin_branch });
238- runSubcommand (allocator , &.{ "merge" , "--abort" }) catch {};
239- return e ;
240- };
232+ if (fetch_ok ) {
233+ // Ensure tracking ref exists (ziggit clone/fetch may not create it)
234+ ensureTrackingRef (branch );
241235
242- printErr (allocator , "ok merged {s}\n " , .{origin_branch });
236+ // Merge instead of rebase. This preserves all local commit SHAs so that
237+ // `reset --hard HEAD~1` always goes to the previous local commit, not
238+ // some unrelated point in history. For parallel agents this is critical:
239+ // rebase rewrites their commits and creates orphaned refs.
240+ runSubcommand (allocator , &.{ "merge" , origin_branch , "--no-edit" }) catch | e | {
241+ // If merge conflicts, abort to leave the tree clean and let the
242+ // caller (agent) know it needs to retry or resolve.
243+ printErr (allocator , "FAILED: merge {s} (conflict?) — aborting merge\n " , .{origin_branch });
244+ runSubcommand (allocator , &.{ "merge" , "--abort" }) catch {};
245+ return e ;
246+ };
247+
248+ printErr (allocator , "ok merged {s}\n " , .{origin_branch });
249+ } else {
250+ printErr (allocator , "ok (no remote refs yet)\n " , .{});
251+ }
243252}
244253
245254/// start [BRANCH] — stash work, sync with origin (merge), restore work
@@ -335,25 +344,32 @@ pub fn cmdProgress(allocator: std.mem.Allocator, args_iter: *platform_mod.ArgIte
335344
336345 // Step 2: Fetch + merge to incorporate other agents' work.
337346 // Now safe because local changes are committed.
338- runSubcommand (allocator , &.{ "fetch" , "origin" }) catch | e | {
339- printErr (allocator , "FAILED: fetch origin\n " , .{});
340- return e ;
347+ // Fetch may fail if this is the first push to an empty remote — that's OK,
348+ // just skip the merge and go straight to push.
349+ const fetch_ok = blk : {
350+ runSubcommand (allocator , &.{ "fetch" , "origin" }) catch {
351+ printErr (allocator , "note: fetch origin failed (remote may be empty), skipping merge\n " , .{});
352+ break :blk false ;
353+ };
354+ break :blk true ;
341355 };
342356
343- // Merge origin — this is a no-op if already up to date.
344- runSubcommand (allocator , &.{ "merge" , origin_branch , "--no-edit" }) catch | e | {
345- // Merge conflict — abort to leave the tree clean.
346- // The local commit is safe; push what we have.
347- printErr (allocator , "WARN: merge {s} conflict — aborting merge, pushing local commit\n " , .{origin_branch });
348- runSubcommand (allocator , &.{ "merge" , "--abort" }) catch {};
349- // Still try to push the local commit even without the merge
350- if (! pushBranch (allocator , current_branch , default_branch )) {
351- printErr (allocator , "FAILED: push after merge conflict\n " , .{});
352- return e ;
353- }
354- printErr (allocator , "ok committed+pushed (no merge) ({s})\n " , .{default_branch });
355- return ;
356- };
357+ if (fetch_ok ) {
358+ // Merge origin — this is a no-op if already up to date.
359+ runSubcommand (allocator , &.{ "merge" , origin_branch , "--no-edit" }) catch | e | {
360+ // Merge conflict — abort to leave the tree clean.
361+ // The local commit is safe; push what we have.
362+ printErr (allocator , "WARN: merge {s} conflict — aborting merge, pushing local commit\n " , .{origin_branch });
363+ runSubcommand (allocator , &.{ "merge" , "--abort" }) catch {};
364+ // Still try to push the local commit even without the merge
365+ if (! pushBranch (allocator , current_branch , default_branch )) {
366+ printErr (allocator , "FAILED: push after merge conflict\n " , .{});
367+ return e ;
368+ }
369+ printErr (allocator , "ok committed+pushed (no merge) ({s})\n " , .{default_branch });
370+ return ;
371+ };
372+ }
357373
358374 // Step 3: Push
359375 const push_ok = pushBranch (allocator , current_branch , default_branch );
@@ -422,13 +438,62 @@ pub fn cmdSetup(allocator: std.mem.Allocator, args_iter: *platform_mod.ArgIterat
422438 return error .SubcommandFailed ;
423439 };
424440
425- // Step 1: Clone
441+ // Step 1: Clone (falls back to init+remote for empty repos)
426442 printErr (allocator , "setup: cloning {s} → {s}\n " , .{ url , path });
427- runSubcommand (allocator , &.{ "clone" , url , path }) catch | e | {
428- printErr (allocator , "FAILED: clone {s}\n " , .{url });
429- return e ;
443+ const clone_ok = blk : {
444+ runSubcommand (allocator , &.{ "clone" , url , path }) catch {
445+ break :blk false ;
446+ };
447+ break :blk true ;
430448 };
431449
450+ if (! clone_ok ) {
451+ // Clone failed — the repo may exist but be empty (zero commits).
452+ // Fall back to: mkdir + init + remote add origin <url>
453+ printErr (allocator , "setup: clone failed, trying init for empty repo\n " , .{});
454+
455+ std .fs .cwd ().makePath (path ) catch {
456+ printErr (allocator , "FAILED: mkdir {s}\n " , .{path });
457+ return error .SubcommandFailed ;
458+ };
459+
460+ // Save and restore CWD so init runs inside the target dir
461+ var old_cwd_buf : [std .fs .max_path_bytes ]u8 = undefined ;
462+ const old_cwd = std .fs .cwd ().realpath ("." , & old_cwd_buf ) catch {
463+ printErr (allocator , "FAILED: getcwd\n " , .{});
464+ return error .SubcommandFailed ;
465+ };
466+
467+ std .posix .chdir (path ) catch {
468+ printErr (allocator , "FAILED: chdir {s}\n " , .{path });
469+ return error .SubcommandFailed ;
470+ };
471+ defer std .posix .chdir (old_cwd ) catch {};
472+
473+ runSubcommand (allocator , &.{"init" }) catch {
474+ printErr (allocator , "FAILED: init in {s}\n " , .{path });
475+ return error .SubcommandFailed ;
476+ };
477+
478+ // Inject token into remote URL for push access
479+ const push_cmd = @import ("git/push_cmd.zig" );
480+ const remote_url = push_cmd .httpsWithToken (allocator , url ) orelse
481+ (allocator .dupe (u8 , url ) catch return error .OutOfMemory );
482+ defer allocator .free (remote_url );
483+
484+ runSubcommand (allocator , &.{ "remote" , "add" , "origin" , remote_url }) catch {
485+ printErr (allocator , "FAILED: remote add origin\n " , .{});
486+ return error .SubcommandFailed ;
487+ };
488+
489+ // Switch to main branch (GitHub default)
490+ runSubcommand (allocator , &.{ "checkout" , "-b" , "main" }) catch {
491+ // Might already be on main
492+ };
493+
494+ printErr (allocator , "setup: initialized empty repo with remote origin\n " , .{});
495+ }
496+
432497 // Step 2: cd into the repo and configure identity.
433498 // We can't actually chdir, but we can run config with -C.
434499 // However, ziggit's config doesn't support -C. Instead, we'll
0 commit comments