Public: anyone can read this; the crew can change it.

A test pins `mergeForCheck`: the check runs on the merge with today's base, in the pull's own repository, and a conflict is said as the change's fault #6720

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6716-a-test-pins-mergeforcheck-the-check-runs → main 1 commit fork

  • Merged. scout/6716-a-test-pins-mergeforcheck-the-check-runs was merged into main as 1bd60db.

Review

Sign in to review this change.
1 file changed +119 −0
test/merged-check.test.ts added +119
@@ -0,0 +1,119 @@
1 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2 import { mkdtemp, rm, writeFile } from "node:fs/promises";
3 import { tmpdir } from "node:os";
4 import { join } from "node:path";
5 import { mergeForCheck, NO_MERGE } from "../src/checks/merged.ts";
6 import { gitClientEnv } from "./helpers/git-client.ts";
7
8 const env = gitClientEnv();
9
10 async function git(cwd: string, ...args: string[]): Promise<string> {
11 const p = Bun.spawn(["git", ...args], {
12 cwd,
13 env,
14 stdout: "pipe",
15 stderr: "pipe",
16 });
17 const [out, err, code] = await Promise.all([
18 new Response(p.stdout).text(),
19 new Response(p.stderr).text(),
20 p.exited,
21 ]);
22 if (code !== 0) throw new Error(`git ${args.join(" ")}: ${err}`);
23 return out.trim();
24 }
25
26 const refs = (dir: string) => git(dir, "for-each-ref");
27
28 describe("mergeForCheck: the pull as it would land on today's base", () => {
29 let root: string;
30 let baseDir: string; // the base repository, kept as a work tree
31 let pullDir: string; // the pull's own repository (a fork)
32 let forkPoint: string;
33 let head: string; // the pull's head: edits pull.txt
34 let conflicting: string; // edits shared.txt differently from the base
35 let baseTip: string; // the base moved ahead: edits base.txt and shared.txt
36
37 async function commit(dir: string, file: string, text: string, msg: string) {
38 await writeFile(join(dir, file), text);
39 await git(dir, "add", file);
40 await git(dir, "commit", "-m", msg);
41 return git(dir, "rev-parse", "HEAD");
42 }
43
44 beforeAll(async () => {
45 root = await mkdtemp(join(tmpdir(), "merged-check-"));
46 baseDir = join(root, "base");
47 pullDir = join(root, "fork");
48 await git(root, "init", "-b", "main", baseDir);
49 await commit(baseDir, "shared.txt", "one\n", "init");
50 await commit(baseDir, "pull.txt", "a\n", "pull file");
51 await commit(baseDir, "base.txt", "a\n", "base file");
52 forkPoint = await git(baseDir, "rev-parse", "HEAD");
53
54 await git(root, "clone", "--bare", baseDir, pullDir);
55 const work = join(root, "work");
56 await git(root, "clone", pullDir, work);
57 await git(work, "checkout", "-b", "pull");
58 head = await commit(work, "pull.txt", "b\n", "pull change");
59 await git(work, "checkout", "-b", "clash", forkPoint);
60 conflicting = await commit(work, "shared.txt", "theirs\n", "clash");
61 await git(work, "push", "origin", "pull", "clash");
62
63 await commit(baseDir, "base.txt", "b\n", "base moves");
64 baseTip = await commit(baseDir, "shared.txt", "ours\n", "base moves again");
65 // The base tip reaches the pull's repository as objects only, no ref.
66 await git(pullDir, "fetch", baseDir, baseTip);
67 });
68
69 afterAll(() => rm(root, { recursive: true, force: true }));
70
71 test("the merge commit has the base tip and the head as parents and both changes", async () => {
72 const before = [await refs(pullDir), await refs(baseDir)];
73 const m = await mergeForCheck(pullDir, "main", baseTip, head);
74 if (!m.ok) throw new Error(m.error);
75 expect((await git(pullDir, "rev-list", "--parents", "-n1", m.commit)).split(" ")).toEqual([
76 m.commit,
77 baseTip,
78 head,
79 ]);
80 const show = (f: string) => git(pullDir, "show", `${m.commit}:${f}`);
81 expect(await show("pull.txt")).toBe("b");
82 expect(await show("base.txt")).toBe("b");
83 expect(await show("shared.txt")).toBe("ours");
84 expect(m.line).toBe(`merge of ${head.slice(0, 7)} into main@${baseTip.slice(0, 7)}`);
85 // The commit is a loose object: no branch or ref moved anywhere.
86 expect([await refs(pullDir), await refs(baseDir)]).toEqual(before);
87 });
88
89 test("the merge commit lives in the pull's repository, not the base's", async () => {
90 const m = await mergeForCheck(pullDir, "main", baseTip, head);
91 if (!m.ok) throw new Error(m.error);
92 expect(await git(pullDir, "cat-file", "-t", m.commit)).toBe("commit");
93 await expect(git(baseDir, "cat-file", "-t", m.commit)).rejects.toThrow();
94 });
95
96 test("a head already on the base still gets a merge commit with the base's tree", async () => {
97 const m = await mergeForCheck(pullDir, "main", baseTip, forkPoint);
98 if (!m.ok) throw new Error(m.error);
99 expect(m.commit).not.toBe(baseTip);
100 expect((await git(pullDir, "rev-list", "--parents", "-n1", m.commit)).split(" ")).toEqual([
101 m.commit,
102 baseTip,
103 forkPoint,
104 ]);
105 expect(await git(pullDir, "rev-parse", `${m.commit}^{tree}`)).toBe(
106 await git(pullDir, "rev-parse", `${baseTip}^{tree}`),
107 );
108 });
109
110 test("a conflicting head fails as the change's fault and moves no ref", async () => {
111 const before = [await refs(pullDir), await refs(baseDir)];
112 const m = await mergeForCheck(pullDir, "main", baseTip, conflicting);
113 expect(m.ok).toBe(false);
114 if (m.ok) return;
115 expect(m.error.startsWith(NO_MERGE)).toBe(true);
116 expect(m.error).toBe(`${NO_MERGE} main: shared.txt`);
117 expect([await refs(pullDir), await refs(baseDir)]).toEqual(before);
118 });
119 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs