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 BASE 2727dd3ec. 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:
2026-10-14 14:52:07 -05:00
co-authored by Claude Opus 5.5
parent a076f28c00
commit c2525dd335
3 changed files with 79 additions and 1 deletions
+7 -1
View File
@@ -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, 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 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 `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 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 one above it. Each git call gets SIGTERM after 100 s and SIGKILL 5 s
later, as below. later, as below.
+5
View File
@@ -67,6 +67,11 @@ const SAFE = [
"-c", "protocol.allow=never", "-c", "protocol.allow=never",
"-c", "protocol.file.allow=always", "-c", "protocol.file.allow=always",
"-c", "submodule.recurse=false", "-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. // The environment git runs with: the host's PATH and nothing else of it.
+67
View File
@@ -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 { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { deflateSync } from "node:zlib";
import { CONFIG_MAX_BYTES, CheckoutError, ensureCheckouts, gitEnv } from "../src/checkout.mjs"; import { CONFIG_MAX_BYTES, CheckoutError, ensureCheckouts, gitEnv } from "../src/checkout.mjs";
function tmp(t) { 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")); 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 // 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 // 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. // clone's config file and runs no git in it, so the kept result has no commit.