seat: fetch a role clone that is on another branch or dirty (#1611)
Row 117. At a launch, update() kept a clone that was on another branch
or dirty and ran no git in it, so its refs/remotes/origin/* stayed as
old as the last clean launch. Now those two reasons get the clean
path's fetch, with the same arguments and environment, and no merge.
The result is a new outcome, fetched, with the reason, the clone's
branch and HEAD, and origin { branch, commit } after the fetch. Every
earlier check still keeps the clone with no fetch. The launcher writes
a checkout-fetched launch-log line, and the bundle prompt names a
fetched clone's branch, commit and origin. Nothing in unsafe(),
allowed(), gitEnv() or SAFE changed.
Candidate manifest 9fa839edfb2bd04771bbac1913b142cb16aef227a1265120e0d318fb64d6a502
from Darkwing's round 1 packet (17d801f48), on BASE 559ef2287. Dewey
approved in 28443. Sage reran the full gate at the landing target with
the candidate applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
// clone that fails is removed and refuses the launch
|
||||
// a clean clone on fetched from its origin and fast-forwarded
|
||||
// the root's branch
|
||||
// a clone on another fetched from its origin and not merged: only
|
||||
// branch, or dirty refs/remotes moves, with the reason (#1611)
|
||||
// anything else left exactly as it is, with the reason
|
||||
//
|
||||
// The host never resets, cleans, deletes or force-updates a clone, and
|
||||
@@ -252,13 +254,25 @@ async function update(root, dest, g) {
|
||||
const at = await rootBranch(root, g);
|
||||
if (at.error) return { outcome: "kept", reason: "root-unreadable" };
|
||||
if (!at.branch) return { outcome: "kept", reason: "root-detached" };
|
||||
// Row 117 (#1611): a clone on another branch, or dirty, still gets the
|
||||
// fetch, so origin/<branch> is the root's at every launch. Only the merge
|
||||
// is left out. symbolic-ref -q fails on a detached HEAD: branch null.
|
||||
const head = await g([...inClone(dest), "symbolic-ref", "-q", "--short", "HEAD"]);
|
||||
if (!head.ok || head.out.trim() !== at.branch) return { outcome: "kept", reason: "other-branch" };
|
||||
const status = await g(["--no-optional-locks", ...inClone(dest), "status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=all"]);
|
||||
if (!status.ok) return { outcome: "kept", reason: "status-failed" };
|
||||
if (status.out !== "") return { outcome: "kept", reason: "dirty" };
|
||||
const before = (await g([...inClone(dest), "rev-parse", "HEAD"])).out.trim();
|
||||
const branch = head.ok ? head.out.trim() : null;
|
||||
let reason = null;
|
||||
if (branch !== at.branch) reason = "other-branch";
|
||||
else {
|
||||
const status = await g(["--no-optional-locks", ...inClone(dest), "status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=all"]);
|
||||
if (!status.ok) return { outcome: "kept", reason: "status-failed" };
|
||||
if (status.out !== "") reason = "dirty";
|
||||
}
|
||||
const first = await g([...inClone(dest), "rev-parse", "HEAD"]);
|
||||
const before = first.ok ? first.out.trim() : null;
|
||||
if (!(await g([...inClone(dest), "fetch", "-q", "--no-tags", "--no-recurse-submodules", "origin"])).ok) return { outcome: "kept", reason: "fetch-failed" };
|
||||
if (reason) {
|
||||
const origin = await g([...inClone(dest), "rev-parse", "--verify", "-q", `refs/remotes/origin/${at.branch}`]);
|
||||
return { outcome: "fetched", reason, branch, commit: before, origin: { branch: at.branch, commit: origin.ok ? origin.out.trim() : null } };
|
||||
}
|
||||
if (!(await g([...inClone(dest), "merge", "-q", "--ff-only", "--no-edit", `refs/remotes/origin/${at.branch}`])).ok) return { outcome: "kept", reason: "not-fast-forward" };
|
||||
const read = await g([...inClone(dest), "rev-parse", "HEAD"]);
|
||||
const after = read.out.trim();
|
||||
@@ -267,12 +281,15 @@ async function update(root, dest, g) {
|
||||
|
||||
// await ensureCheckouts({ workspace, projects, env, limits }) for the
|
||||
// business file's projects ({ <id>: { root } }). Returns one { project,
|
||||
// path, outcome, reason?, branch?, commit? } per project, outcome cloned,
|
||||
// updated, current or kept. branch and commit are the clone's after this
|
||||
// launch (commit null when git couldn't read it); a kept clone has neither,
|
||||
// since git may not be safe to run there. Rejects with CheckoutError when a
|
||||
// missing clone can't be made. `limits` ({ timeoutMs, graceMs }) is for the
|
||||
// tests.
|
||||
// path, outcome, reason?, branch?, commit?, origin? } per project, outcome
|
||||
// cloned, updated, current, fetched or kept. branch and commit are the
|
||||
// clone's after this launch (commit null when git couldn't read it). A
|
||||
// fetched clone also has a reason (other-branch or dirty), branch null when
|
||||
// its HEAD is detached, and origin { branch, commit }: the root's branch and
|
||||
// refs/remotes/origin/<branch> after the fetch, commit null when unreadable.
|
||||
// A kept clone has only a reason, since git may not be safe to run there.
|
||||
// Rejects with CheckoutError when a missing clone can't be made. `limits`
|
||||
// ({ timeoutMs, graceMs }) is for the tests.
|
||||
export async function ensureCheckouts({ workspace, projects, env = process.env, limits = GIT_LIMITS }) {
|
||||
const g = (args, extra) => git(args, env, extra, limits);
|
||||
const out = [];
|
||||
|
||||
Reference in New Issue
Block a user