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

A test pins `revertCommit`: undoing a shipped commit keeps later changes, reports a real conflict, and refuses a merge commit with two parents #6703

Merged opened this

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

Merged

scout/forge:scout/6702-a-test-pins-revertcommit-undoing-a-shipp → main 1 commit fork

  • Merged. scout/6702-a-test-pins-revertcommit-undoing-a-shipp was merged into main as bf3f20b.

Review

Sign in to review this change.
1 file changed +145 −0
test/git-revert.test.ts added +145
@@ -0,0 +1,145 @@
1 /**
2 * revertCommit undoes one shipped commit with a three-way merge: later work
3 * stays, a real conflict is named, and the new commit sits on `ontoOid`.
4 */
5 import { afterAll, expect, test } from "bun:test";
6 import { mkdtemp, rm, writeFile } from "node:fs/promises";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { commitParents, revertCommit } from "../src/git/revert.ts";
10 import { gitClientEnv } from "./helpers/git-client.ts";
11
12 const env = gitClientEnv({
13 GIT_AUTHOR_NAME: "A",
14 GIT_AUTHOR_EMAIL: "[email protected]",
15 GIT_COMMITTER_NAME: "A",
16 GIT_COMMITTER_EMAIL: "[email protected]",
17 });
18 const dirs: string[] = [];
19 afterAll(() => Promise.all(dirs.map((d) => rm(d, { recursive: true, force: true }))));
20
21 async function git(args: string[], cwd: string): Promise<string> {
22 const p = Bun.spawn(["git", ...args], { cwd, env, stdout: "pipe", stderr: "pipe" });
23 const [out, e, code] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text(), p.exited]);
24 if (code !== 0) throw new Error(`git ${args.join(" ")}: ${e}`);
25 return out.trim();
26 }
27
28 async function init(): Promise<string> {
29 const dir = await mkdtemp(join(tmpdir(), "forge-revert-"));
30 dirs.push(dir);
31 await git(["init", "-q", "-b", "main", dir], dir);
32 return dir;
33 }
34
35 /** Write `text` to `file` and commit it; returns the new commit's oid. */
36 async function commit(dir: string, file: string, text: string, msg: string): Promise<string> {
37 await writeFile(join(dir, file), text);
38 await git(["add", file], dir);
39 await git(["commit", "-q", "-m", msg], dir);
40 return git(["rev-parse", "HEAD"], dir);
41 }
42
43 const author = { name: "Rev", email: "[email protected]" };
44 const lines = (...n: string[]) => n.join("\n") + "\n";
45 const base = lines("a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9");
46
47 test("reverting B with C on top undoes only B and keeps C", async () => {
48 const dir = await init();
49 await commit(dir, "f.txt", base, "A");
50 const b = await commit(dir, "f.txt", base.replace("a2", "B2"), "B");
51 const c = await commit(dir, "f.txt", base.replace("a2", "B2").replace("a8", "C8"), "C");
52 const r = await revertCommit(dir, { commitOid: b, ontoOid: c, message: "Revert B", author });
53 expect(r.ok).toBe(true);
54 if (!r.ok) return;
55 const out = await git(["show", `${r.value.commitOid}:f.txt`], dir);
56 expect(out + "\n").toBe(base.replace("a8", "C8"));
57 });
58
59 test("the new commit has ontoOid as its only parent, the message and the author", async () => {
60 const dir = await init();
61 await commit(dir, "f.txt", base, "A");
62 const b = await commit(dir, "f.txt", base.replace("a2", "B2"), "B");
63 const r = await revertCommit(dir, { commitOid: b, ontoOid: b, message: "Revert B\n\nwhy", author });
64 expect(r.ok).toBe(true);
65 if (!r.ok) return;
66 const o = r.value.commitOid;
67 expect(await commitParents(dir, o)).toEqual([b]);
68 expect(await git(["log", "-1", "--format=%B", o], dir)).toBe("Revert B\n\nwhy");
69 expect(await git(["log", "-1", "--format=%an <%ae>|%cn <%ce>", o], dir)).toBe(
70 "Rev <[email protected]>|Rev <[email protected]>",
71 );
72 });
73
74 test("a later rewrite of B's lines is a conflict naming the file, and no ref moves", async () => {
75 const dir = await init();
76 await commit(dir, "f.txt", base, "A");
77 const b = await commit(dir, "f.txt", base.replace("a2", "B2"), "B");
78 const c = await commit(dir, "f.txt", base.replace("a2", "C2"), "C");
79 const refs = () => git(["for-each-ref", "--format=%(refname) %(objectname)"], dir);
80 const before = await refs();
81 const r = await revertCommit(dir, { commitOid: b, ontoOid: c, message: "Revert B", author });
82 expect(r.ok).toBe(false);
83 if (r.ok) return;
84 expect(r.error).toEqual({ kind: "conflict", files: ["f.txt"] });
85 expect(await refs()).toBe(before);
86 });
87
88 test("commitParents: one for a squash, two for a merge, none for a root", async () => {
89 const dir = await init();
90 const root = await commit(dir, "f.txt", base, "root");
91 const second = await commit(dir, "f.txt", base.replace("a2", "B2"), "second");
92 await git(["checkout", "-q", "-b", "side", root], dir);
93 const side = await commit(dir, "g.txt", "g\n", "side");
94 await git(["checkout", "-q", "main"], dir);
95 await git(["merge", "-q", "--no-ff", "-m", "merge", "side"], dir);
96 const merge = await git(["rev-parse", "HEAD"], dir);
97
98 expect(await commitParents(dir, root)).toEqual([]);
99 expect(await commitParents(dir, second)).toEqual([root]);
100 expect(await commitParents(dir, merge)).toEqual([second, side]);
101 expect(await commitParents(dir, "not-an-oid")).toBeUndefined();
102 });
103
104 test("revertCommit refuses a root commit, undoes a merge against its first parent", async () => {
105 const dir = await init();
106 const root = await commit(dir, "f.txt", base, "root");
107 const second = await commit(dir, "f.txt", base.replace("a2", "B2"), "second");
108 const rootRevert = await revertCommit(dir, { commitOid: root, ontoOid: second, message: "r", author });
109 expect(rootRevert.ok).toBe(false);
110 if (!rootRevert.ok) expect(rootRevert.error.kind).toBe("git");
111
112 await git(["checkout", "-q", "-b", "side", root], dir);
113 await commit(dir, "g.txt", "g\n", "side");
114 await git(["checkout", "-q", "main"], dir);
115 await git(["merge", "-q", "--no-ff", "-m", "merge", "side"], dir);
116 const merge = await git(["rev-parse", "HEAD"], dir);
117 const r = await revertCommit(dir, { commitOid: merge, ontoOid: merge, message: "Revert merge", author });
118 expect(r.ok).toBe(true);
119 if (!r.ok) return;
120 // mainline is the first parent: the side branch's file goes, main's edit stays.
121 expect(await git(["ls-tree", "--name-only", r.value.commitOid], dir)).toBe("f.txt");
122 expect(await git(["show", `${r.value.commitOid}:f.txt`], dir) + "\n").toBe(base.replace("a2", "B2"));
123 });
124
125 test("revertCommit refuses an octopus merge and an unsafe oid", async () => {
126 const dir = await init();
127 const root = await commit(dir, "f.txt", base, "root");
128 const branches: string[] = [];
129 for (const n of ["x", "y", "z"]) {
130 await git(["checkout", "-q", "-b", n, root], dir);
131 await commit(dir, `${n}.txt`, n, n);
132 branches.push(n);
133 }
134 await git(["checkout", "-q", "main"], dir);
135 await git(["merge", "-q", "-m", "octopus", ...branches], dir);
136 const oct = await git(["rev-parse", "HEAD"], dir);
137 expect((await commitParents(dir, oct))?.length).toBe(3);
138 const r = await revertCommit(dir, { commitOid: oct, ontoOid: oct, message: "r", author });
139 expect(r.ok).toBe(false);
140 if (!r.ok) expect(r.error).toEqual({ kind: "octopus", parents: 3 });
141
142 const bad = await revertCommit(dir, { commitOid: "--bad", ontoOid: oct, message: "r", author });
143 expect(bad.ok).toBe(false);
144 if (!bad.ok) expect(bad.error.kind).toBe("unsafe");
145 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs