From 3b77dde666f291a153d576309a371f70d2adb79e Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 28 Sep 2026 09:17:38 +0200 Subject: [PATCH] A merge says which files it changed Every module built from a repository was rebuilt for a change to any of them: one merge in this repository meant twenty-six builds, which is what exhausted a public registry's pull limit. The forge lists the files a merge changed and the event carries them, from the watcher and from the merge tool alike; a merge that changed more files than were asked for says so, and the mesh then treats the whole repository as changed rather than guessing. --- modules/gitea/client.ts | 11 +++++++++++ modules/gitea/index.ts | 6 ++++++ modules/gitea/tools/index.ts | 5 +++++ 3 files changed, 22 insertions(+) diff --git a/modules/gitea/client.ts b/modules/gitea/client.ts index 5a6e918..c8e5c28 100644 --- a/modules/gitea/client.ts +++ b/modules/gitea/client.ts @@ -229,6 +229,17 @@ export class GiteaClient { return GiteaClient.mapPull(await this.request(`/repos/${owner}/${repo}/pulls/${index}`)); } + /** The files a merged pull request changed, as paths from the repository's root. + * + * `limit` is what is asked for, and a merge that changed more says so rather than being read + * page by page: what the mesh does with a partial list is treat the whole repository as changed, + * so more pages would buy nothing. */ + async listPullFiles(owner: string, repo: string, index: number, limit = 100): Promise<{ paths: string[]; truncated: boolean }> { + const files = await this.request(`/repos/${owner}/${repo}/pulls/${index}/files?limit=${limit}`); + const paths = (files ?? []).map((f) => String(f?.filename ?? "")).filter((p) => p !== ""); + return { paths, truncated: paths.length >= limit }; + } + async createPullRequest( owner: string, repo: string, diff --git a/modules/gitea/index.ts b/modules/gitea/index.ts index 0e99f53..947086f 100644 --- a/modules/gitea/index.ts +++ b/modules/gitea/index.ts @@ -95,6 +95,10 @@ async function pollMerged(client: GiteaClient): Promise { // at once. const fresh = !!pull.merged_at && !!since && pull.merged_at > since; if (primedMerges && fresh) { + // What it changed, asked for only now: a module is rebuilt because a file inside its own + // directory moved, and without this every module built from a repository is rebuilt for a + // change to any of them (novox/hq 04-ISSUES/131). + const changed = await client.listPullFiles(repo.owner, repo.name, pull.number); await emit("pull.merged", { owner: repo.owner, repo: repo.name, @@ -106,6 +110,8 @@ async function pollMerged(client: GiteaClient): Promise { merged_at: pull.merged_at, clone_url: repo.clone_url, html_url: pull.html_url, + paths: changed.paths, + paths_truncated: changed.truncated, }); // Said, because a trigger that fires silently is indistinguishable from one that did not // fire (novox/hq 04-ISSUES/131) — this line is how an operator knows the mesh was told. diff --git a/modules/gitea/tools/index.ts b/modules/gitea/tools/index.ts index 103d4a9..fda1190 100644 --- a/modules/gitea/tools/index.ts +++ b/modules/gitea/tools/index.ts @@ -233,6 +233,9 @@ export function getGiteaTools(gitea: GiteaClient): ToolDefinition[] { await gitea.mergePullRequest(owner, repo, number, method, deleteBranch); // Read it again: the merge commit only exists now, and it is what a build is made from. const merged = await gitea.getPullRequest(owner, repo, number); + // And what it changed, so the mesh rebuilds the modules whose own files moved rather than + // every module built from the repository (novox/hq 04-ISSUES/131). + const changed = await gitea.listPullFiles(owner, repo, number); await emit("pull.merged", { owner, repo, @@ -244,6 +247,8 @@ export function getGiteaTools(gitea: GiteaClient): ToolDefinition[] { merged_at: merged.merged_at, method, html_url: pull.html_url, + paths: changed.paths, + paths_truncated: changed.truncated, }); return { merged: true, number, method, deleted_branch: deleteBranch }; }, -- 2.54.0