seat: the host's git runs no background maintenance in a role clone (row 118, #1612)
Candidate manifest eeb67779c919b7c1f25b4449df7207d85634e5175b16eef315346cf5a284c63c from Darkwing's round 1 packet (5a68df727), on BASE2727dd3ec. No manifest path changed between BASE and the landing target. Dewey approved in 28479, with no blocking or doc-only findings. 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:
@@ -304,7 +304,13 @@ only the `file` transport. A submodule has its own config, which none of
|
||||
those checks reads and which can name a filter that `git status` would run,
|
||||
so the host never recurses into one: a clone with a submodule is kept, and
|
||||
`status` runs with `--ignore-submodules=all` and every call with
|
||||
`submodule.recurse=false`. The root is read with `GIT_CEILING_DIRECTORIES`
|
||||
`submodule.recurse=false`. Every call also carries `maintenance.auto=false`
|
||||
and `gc.auto=0`. Without them a fetch or a fast-forward merge starts
|
||||
`git maintenance run --auto`, which detaches from the call's process group
|
||||
and can repack the clone after the call has returned, where the time limit
|
||||
below no longer reaches it. On git 2.55 either key alone stops that start;
|
||||
`maintenance.auto=false` is the documented switch, and `gc.auto=0` also
|
||||
stops a `gc --auto` that runs anyway. The root is read with `GIT_CEILING_DIRECTORIES`
|
||||
at its parent, so a root that isn't a repository is never taken for the
|
||||
one above it. Each git call gets SIGTERM after 100 s and SIGKILL 5 s
|
||||
later, as below.
|
||||
|
||||
@@ -67,6 +67,11 @@ const SAFE = [
|
||||
"-c", "protocol.allow=never",
|
||||
"-c", "protocol.file.allow=always",
|
||||
"-c", "submodule.recurse=false",
|
||||
// A fetch or a merge would start `git maintenance run --auto`, which
|
||||
// detaches from the call's process group and repacks the clone after the
|
||||
// call has returned. gc.auto=0 also stops a gc --auto that runs anyway.
|
||||
"-c", "maintenance.auto=false",
|
||||
"-c", "gc.auto=0",
|
||||
];
|
||||
|
||||
// The environment git runs with: the host's PATH and nothing else of it.
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createHash } from "node:crypto";
|
||||
import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { deflateSync } from "node:zlib";
|
||||
import { CONFIG_MAX_BYTES, CheckoutError, ensureCheckouts, gitEnv } from "../src/checkout.mjs";
|
||||
|
||||
function tmp(t) {
|
||||
@@ -240,6 +241,72 @@ test("a dirty clone whose fetch fails is kept, fetch-failed, exactly as it was",
|
||||
assert.ok(!l.inClone().includes("merge"));
|
||||
});
|
||||
|
||||
// Row 118 (#1612): every host git call carries maintenance.auto=false and
|
||||
// gc.auto=0, so a fetch or a merge in a clone starts no maintenance.
|
||||
|
||||
// Whether a logged call has `-c <kv>` among its arguments.
|
||||
const carries = (call, kv) => call.split(" ").some((a, i, all) => a === "-c" && all[i + 1] === kv);
|
||||
|
||||
test("every git call the host makes, for a new clone and an existing one, carries maintenance.auto=false and gc.auto=0", async (t) => {
|
||||
const w = world(t);
|
||||
const l = logged(w);
|
||||
const ensure = () => ensureCheckouts({ workspace: w.workspace, projects: { stack: { root: w.root } }, env: l.env });
|
||||
assert.equal((await ensure())[0].outcome, "cloned");
|
||||
const two = commit(w.root, "two\n");
|
||||
assert.deepEqual(await ensure(), [{ project: "stack", path: w.clone, outcome: "updated", branch: "main", commit: two }]);
|
||||
const calls = readFileSync(join(w.dir, "calls"), "utf8").split("\n").filter(Boolean);
|
||||
for (const sub of ["clone", "fetch", "merge"]) assert.ok(calls.some((c) => c.split(" ").includes(sub)), `a ${sub} ran`);
|
||||
for (const c of calls) for (const kv of ["maintenance.auto=false", "gc.auto=0"]) assert.ok(carries(c, kv), `${kv} is on: ${c}`);
|
||||
});
|
||||
|
||||
// A git on PATH that logs every git run, the ones git starts itself
|
||||
// included. git puts its exec path first on PATH for its children, so the
|
||||
// exec path here is a copy of the real one whose git is this logger.
|
||||
function loggedAll(w) {
|
||||
const bin = join(w.dir, "exec");
|
||||
mkdirSync(bin);
|
||||
const real = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
|
||||
const libexec = spawnSync(real, ["--exec-path"], { encoding: "utf8" }).stdout.trim();
|
||||
for (const name of readdirSync(libexec)) if (name !== "git") symlinkSync(join(libexec, name), join(bin, name));
|
||||
const calls = join(w.dir, "calls");
|
||||
writeFileSync(join(bin, "git"), `#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nGIT_EXEC_PATH='${bin}' exec '${real}' "$@"\n`, { mode: 0o755 });
|
||||
return { env: { PATH: `${bin}:${process.env.PATH}` }, calls: () => readFileSync(calls, "utf8").split("\n").filter(Boolean) };
|
||||
}
|
||||
|
||||
// `n` loose blobs in the clone, each with an id starting 17. gc --auto
|
||||
// counts the loose objects in objects/17 only, and more than 27 there is
|
||||
// over its default limit of 6700.
|
||||
function loose(clone, n) {
|
||||
const dir = join(clone, ".git", "objects", "17");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
for (let i = 0, made = 0; made < n; i++) {
|
||||
const body = Buffer.from(`loose ${i}\n`);
|
||||
const raw = Buffer.concat([Buffer.from(`blob ${body.length}\0`), body]);
|
||||
const id = createHash("sha1").update(raw).digest("hex");
|
||||
if (!id.startsWith("17")) continue;
|
||||
writeFileSync(join(dir, id.slice(2)), deflateSync(raw));
|
||||
made++;
|
||||
}
|
||||
return readdirSync(dir).map((name) => join(dir, name));
|
||||
}
|
||||
|
||||
// The clone's config can't turn maintenance on (allowed() keeps such a
|
||||
// clone), so the trigger is the default config plus loose objects enough
|
||||
// for gc --auto. Without both keys the fetch and the merge each start
|
||||
// `maintenance run --auto`, which repacks them.
|
||||
test("a clone with loose objects enough for gc --auto gets no maintenance or gc from the fetch and the merge", async (t) => {
|
||||
const w = world(t);
|
||||
await once(w);
|
||||
const objects = loose(w.clone, 40);
|
||||
const two = commit(w.root, "two\n");
|
||||
const l = loggedAll(w);
|
||||
assert.deepEqual(await ensureCheckouts({ workspace: w.workspace, projects: { stack: { root: w.root } }, env: l.env }), [{ project: "stack", path: w.clone, outcome: "updated", branch: "main", commit: two }]);
|
||||
const calls = l.calls();
|
||||
assert.ok(calls.some((c) => !c.startsWith("-c core.hooksPath=")), "the log shows a git that git started");
|
||||
assert.deepEqual(calls.filter((c) => ["maintenance", "gc", "repack"].some((sub) => c.split(" ").includes(sub))), [], "no maintenance, gc or repack ran");
|
||||
for (const o of objects) assert.ok(existsSync(o), `${o} is still loose`);
|
||||
});
|
||||
|
||||
// Row 104 (#1598): a session that sets its own git identity in the clone's
|
||||
// config gets the clone kept at every later launch. The host only reads the
|
||||
// clone's config file and runs no git in it, so the kept result has no commit.
|
||||
|
||||
Reference in New Issue
Block a user