From 424209c1568989d881aa658f38e9fbe6c6d42b57 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Sun, 26 Jul 2026 01:29:12 -0700 Subject: [PATCH] fix(ci): build conflict trees from main --- test/pr-merge-conflict-fixer.test.ts | 35 +++++++++++++++++++++-- tools/pr-merge-conflict-fixer/publish.mts | 12 +++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/test/pr-merge-conflict-fixer.test.ts b/test/pr-merge-conflict-fixer.test.ts index 064d6b2a92..50896e867d 100644 --- a/test/pr-merge-conflict-fixer.test.ts +++ b/test/pr-merge-conflict-fixer.test.ts @@ -100,7 +100,8 @@ function createConflictFixture(): { git(repository, ["config", "commit.gpgsign", "false"]); write(repository, "conflict.txt", "shared\n"); write(repository, "clean-merge.txt", "first\nkeep-1\nkeep-2\nkeep-3\nkeep-4\nkeep-5\nlast\n"); - git(repository, ["add", "conflict.txt", "clean-merge.txt"]); + write(repository, "pr-deleted.txt", "delete this on the PR branch\n"); + git(repository, ["add", "conflict.txt", "clean-merge.txt", "pr-deleted.txt"]); git(repository, ["commit", "-m", "test: add shared file"]); git(repository, ["checkout", "-b", "pull-request"]); @@ -110,7 +111,8 @@ function createConflictFixture(): { "clean-merge.txt", "pull request\nkeep-1\nkeep-2\nkeep-3\nkeep-4\nkeep-5\nlast\n", ); - git(repository, ["add", "conflict.txt", "clean-merge.txt"]); + fs.rmSync(path.join(repository, "pr-deleted.txt")); + git(repository, ["add", "-A"]); git(repository, ["commit", "-m", "test: change PR side"]); const headSha = git(repository, ["rev-parse", "HEAD"]); @@ -346,8 +348,14 @@ describe("PR merge conflict fixer", () => { ).not.toThrow(); }); - it("creates a verified two-parent commit before the atomic head update (#7542)", async () => { + it("creates a verified commit from a main-relative tree before the atomic head update (#7542)", async () => { const fixture = createConflictFixture(); + for (let index = 0; index < 100; index += 1) { + write(fixture.repository, `stale-main/${index}.txt`, `main ${index}\n`); + } + git(fixture.repository, ["add", "stale-main"]); + git(fixture.repository, ["commit", "-m", "test: advance main beyond the PR head"]); + fixture.baseSha = git(fixture.repository, ["rev-parse", "HEAD"]); const entry = entryFor(fixture); const patchPath = path.join(temporaryDirectory(), "resolution.patch"); const finalTree = createResolutionPatch(fixture, patchPath); @@ -409,6 +417,27 @@ describe("PR merge conflict fixer", () => { tree: finalTree, }); expect(JSON.stringify(commitRequest?.body)).not.toMatch(/author|committer|signature/u); + const treeRequest = required( + requests.find((item) => item.path.endsWith("/git/trees")), + "missing tree request", + ); + const treeBody = treeRequest.body as { + base_tree: string; + tree: Array<{ mode: string; path: string; sha: string | null; type: string }>; + }; + expect(treeBody.base_tree).toBe(entry.base_sha); + expect(treeBody.tree.map((item) => item.path)).toEqual([ + "clean-merge.txt", + "conflict.txt", + "pr-deleted.txt", + ]); + expect(treeBody.tree.find((item) => item.path === "pr-deleted.txt")).toEqual({ + mode: "100644", + path: "pr-deleted.txt", + sha: null, + type: "blob", + }); + expect(treeBody.tree.some((item) => item.path.startsWith("stale-main/"))).toBe(false); const blobRequests = requests.filter((item) => item.path.endsWith("/git/blobs")); expect( blobRequests diff --git a/tools/pr-merge-conflict-fixer/publish.mts b/tools/pr-merge-conflict-fixer/publish.mts index c5c91fe02a..d11bd8562c 100755 --- a/tools/pr-merge-conflict-fixer/publish.mts +++ b/tools/pr-merge-conflict-fixer/publish.mts @@ -161,11 +161,7 @@ function treeEntry(repository: string, tree: string, filePath: string): GitTreeE return entry; } -function parentContainsBlob( - repository: string, - parent: string, - entry: GitTreeEntry, -): boolean { +function parentContainsBlob(repository: string, parent: string, entry: GitTreeEntry): boolean { const parentEntry = optionalTreeEntry(repository, parent, entry.path); return parentEntry?.type === "blob" && parentEntry.sha === entry.sha; } @@ -179,8 +175,8 @@ async function createGitHubTree(input: { request: GitHubRequest; }): Promise { const entries: GitTreeEntry[] = []; - for (const change of changedPathStatuses(input.repository, input.headSha, input.finalTree)) { - const sourceTree = change.status === "D" ? input.headSha : input.finalTree; + for (const change of changedPathStatuses(input.repository, input.baseSha, input.finalTree)) { + const sourceTree = change.status === "D" ? input.baseSha : input.finalTree; const entry = treeEntry(input.repository, sourceTree, change.path); if (change.status === "D") { entries.push({ ...entry, sha: null }); @@ -204,7 +200,7 @@ async function createGitHubTree(input: { } const created = (await input.request("POST", `/repos/${input.repositoryName}/git/trees`, { - base_tree: input.headSha, + base_tree: input.baseSha, tree: entries, })) as { sha?: string }; if (created.sha !== input.finalTree) {