Give the forge bridge a merge commit's trees in one call, and a pull request's head branch (issue 1234)

gitea_commit_trees reads the commit's recursive tree listing and answers the root tree and the
tree of every directory holding a module.json; a cut listing fails loudly. gitea_open_pulls
entries gain head. The query keys recursive and per_page join the allow-list, read flags only.
mesh-delivery decodes open pulls with json.Unmarshal into a struct (cmd/mesh-delivery/ports.go:405-410),
which ignores the added key.
This commit is contained in:
2026-10-19 14:50:19 +02:00
parent 7bb588ca1b
commit ea83746297
5 changed files with 197 additions and 2 deletions
+42 -1
View File
@@ -92,6 +92,8 @@ export interface OpenPull {
repo: string;
number: number;
base: string;
/** The pull request's head branch: what a delivery group is named by (novox/hq ADR 0249). */
head?: string;
head_sha: string;
html_url?: string;
/** The contexts the base branch's protection requires; none when it requires none, or cannot be read. */
@@ -374,7 +376,7 @@ export class GiteaClient {
const rule = rules.get(base);
const checks = await this.headChecks(found.owner, found.name, pull.head_sha);
const labels = found.labels ?? [];
out.push({ owner: found.owner, repo: found.name, number: pull.number, base: pull.base ?? "", head_sha: pull.head_sha,
out.push({ owner: found.owner, repo: found.name, number: pull.number, base: pull.base ?? "", head: pull.head, head_sha: pull.head_sha,
html_url: pull.html_url, required: rule?.enable_status_check ? (rule.status_check_contexts ?? []) : [],
statuses: Object.keys(checks), checks, labels: labels.map((l) => l.name) });
}
@@ -510,6 +512,45 @@ export class GiteaClient {
return out;
}
/** **A commit's trees: its root, and the tree of every directory that holds a `module.json`** (novox/hq issue 1234,
* ADR 0437 §4 and §5), from the forge's one recursive listing of the commit, read page by page. A directory is
* spelled as the repository spells it, no slash at either end, the root as `""`.
*
* **A listing the forge cut is no listing** (it says `truncated` on a page with nothing more to read, or the pages
* run past a bound): the call fails naming the repository and the commit, never answering the directories it did
* see, since a directory left out would be taken for one without a module. Reads only. */
async commitTrees(owner: string, repo: string, sha: string): Promise<{ root: string; trees: Record<string, string> }> {
const base = `${repoPath(owner, repo)}/git/trees/${commitSegment(sha)}?recursive=true`;
const cut = (why: string) => new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} was cut (${why}); no trees are answered from a part`);
let root = "";
const entries: any[] = [];
for (let page = 1; ; page++) {
if (page > 200) throw cut("more than 200 pages");
const got = await this.request<any>(`${base}&page=${page}&per_page=1000`);
if (page === 1) {
root = String(got?.sha ?? "");
if (!/^[0-9a-f]{40,64}$/i.test(root)) throw new Error(`the forge named no root tree for ${owner}/${repo} at ${sha}`);
}
if (!Array.isArray(got?.tree)) throw new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} has no tree`);
entries.push(...got.tree);
if (got.truncated !== true) break;
// Truncated with nothing more to ask for: the forge itself could not give the rest.
if (got.tree.length === 0 || entries.length >= Number(got.total_count ?? 0)) throw cut("truncated");
}
const trees: Record<string, string> = {};
const dirOf = new Map<string, string>();
for (const e of entries) if (e?.type === "tree" && typeof e.path === "string") dirOf.set(e.path, String(e.sha));
for (const e of entries) {
if (e?.type !== "blob" || typeof e.path !== "string") continue;
if (e.path !== "module.json" && !e.path.endsWith("/module.json")) continue;
const dir = e.path === "module.json" ? "" : e.path.slice(0, -"/module.json".length);
const tree = dir === "" ? root : dirOf.get(dir);
if (!tree) throw new Error(`the forge's tree listing of ${owner}/${repo} at ${sha} holds ${e.path} but not the tree of ${dir}`);
trees[dir] = tree;
}
return { root, trees };
}
/** Whether a repository holds a file at a commit: true, false for a 404, thrown when the forge cannot say. */
async holdsFile(owner: string, repo: string, sha: string, file: string): Promise<boolean> {
return this.exists(`${repoPath(owner, repo)}/contents/${encodePath(file)}?ref=${encodeURIComponent(sha)}`);