Skip to content

Commit daabbfc

Browse files
committed
workflow: handle empty remotes in restart, progress, setup
When a remote repo exists but has zero commits, fetch fails with a ref negotiation error. Previously this was fatal, blocking the first push to a new repo. Now: restart/progress treat fetch failure as non-fatal (skip merge, proceed to push). setup falls back to init+remote-add when clone fails on an empty repo.
1 parent 564d7ab commit daabbfc

1 file changed

Lines changed: 104 additions & 39 deletions

File tree

‎src/cmd_workflow.zig‎

Lines changed: 104 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)