Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

139 lines · 6.2 KBRaw History
1import { execFileSync } from "node:child_process";
2import { mkdtempSync, writeFileSync, readFileSync, mkdirSync } from "node:fs";
3import { tmpdir } from "node:os";
4import { join } from "node:path";
5import { describe, expect, it } from "vitest";
6import {
7 MemoryStore,
8 applyChanges,
9 deflate,
10 diffTrees,
11 fileDiff,
12 mergeBase,
13 mergeText,
14 mergeTrees,
15 objectsForFetch,
16 parseCommit,
17 parsePack,
18 readCommit,
19 serializeCommit,
20 utf8,
21 writePack,
22 fromUtf8,
23 listFiles,
24} from "../src/index.js";
25
26const git = (cwd: string, ...args: string[]) =>
27 execFileSync("git", args, { cwd, env: { ...process.env, GIT_AUTHOR_NAME: "T", GIT_AUTHOR_EMAIL: "t@x", GIT_COMMITTER_NAME: "T", GIT_COMMITTER_EMAIL: "t@x" } });
28
29function makeRepo(): string {
30 const dir = mkdtempSync(join(tmpdir(), "gc-"));
31 git(dir, "init", "-q", "-b", "main");
32 const big = Array.from({ length: 400 }, (_, i) => `line ${i} of a fairly long file to encourage deltas`).join("\n");
33 writeFileSync(join(dir, "a.txt"), big);
34 mkdirSync(join(dir, "src/deep"), { recursive: true });
35 writeFileSync(join(dir, "src/deep/x.js"), "console.log(1)\n");
36 writeFileSync(join(dir, "bin.dat"), Buffer.from([0, 1, 2, 3, 0, 255]));
37 git(dir, "add", "-A");
38 git(dir, "commit", "-q", "-m", "first");
39 writeFileSync(join(dir, "a.txt"), big.replace("line 200", "LINE 200"));
40 git(dir, "commit", "-qam", "second");
41 git(dir, "tag", "-a", "v1", "-m", "release");
42 return dir;
43}
44
45async function loadAll(dir: string): Promise<{ store: MemoryStore; expected: string[] }> {
46 const expected = git(dir, "rev-list", "--objects", "--all").toString().trim().split("\n").map((l) => l.split(" ")[0]!);
47 expected.push(git(dir, "rev-parse", "v1").toString().trim());
48 const pack = execFileSync("git", ["pack-objects", "--stdout", "--delta-base-offset"], { cwd: dir, input: expected.join("\n") + "\n" });
49 const objs = await parsePack(new Uint8Array(pack));
50 const store = new MemoryStore();
51 for (const o of objs) await store.write(o.type, o.content);
52 return { store, expected };
53}
54
55describe("packfiles", () => {
56 it("parses a real git pack with ofs deltas and verifies all oids", async () => {
57 const dir = makeRepo();
58 const { store, expected } = await loadAll(dir);
59 for (const oid of expected) expect(store.objects.has(oid), oid).toBe(true);
60 });
61
62 it("writes packs that git accepts", async () => {
63 const dir = makeRepo();
64 const { store } = await loadAll(dir);
65 const head = git(dir, "rev-parse", "HEAD").toString().trim();
66 const oids = await objectsForFetch(store, [head], []);
67 const pack = await writePack(
68 await Promise.all(oids.map(async (oid) => {
69 const o = (await store.read(oid))!;
70 return { type: o.type, size: o.content.length, deflated: deflate(o.content) };
71 })),
72 );
73 const out = mkdtempSync(join(tmpdir(), "gc-out-"));
74 git(out, "init", "-q", "--bare");
75 const p = join(out, "objects/pack/pack-x.pack");
76 writeFileSync(p, pack);
77 git(out, "index-pack", p);
78 git(out, "update-ref", "refs/heads/main", head);
79 git(out, "fsck", "--strict", "main");
80 });
81
82 it("sends only new objects on incremental fetch", async () => {
83 const dir = makeRepo();
84 const { store } = await loadAll(dir);
85 const head = git(dir, "rev-parse", "HEAD").toString().trim();
86 const first = git(dir, "rev-parse", "HEAD~1").toString().trim();
87 const oids = await objectsForFetch(store, [head], [first]);
88 // new commit + new root tree + new a.txt blob
89 expect(oids.length).toBe(3);
90 });
91});
92
93describe("trees, diffs and merges", () => {
94 it("builds commits from file changes and diffs them", async () => {
95 const store = new MemoryStore();
96 const t1 = await applyChanges(store, null, [
97 { path: "README.md", content: utf8("hello\n") },
98 { path: "src/a.ts", content: utf8("a\n") },
99 ]);
100 const t2 = await applyChanges(store, t1, [
101 { path: "src/a.ts", content: utf8("a\nb\n") },
102 { path: "README.md", delete: true },
103 { path: "src/new/b.ts", content: utf8("x") },
104 ]);
105 const changes = await diffTrees(store, t1, t2);
106 expect(changes.map((c) => `${c.status}:${c.path}`)).toEqual(["deleted:README.md", "modified:src/a.ts", "added:src/new/b.ts"]);
107 const d = await fileDiff(store, changes[1]!);
108 expect(d.additions).toBe(1);
109 expect((await listFiles(store, t2)).map((f) => f.path)).toEqual(["src/a.ts", "src/new/b.ts"]);
110 });
111
112 it("merges non-overlapping line edits and detects conflicts", () => {
113 const base = "a\nb\nc\nd\ne\n";
114 expect(mergeText(base, "A\nb\nc\nd\ne\n", "a\nb\nc\nd\nE\n")).toBe("A\nb\nc\nd\nE\n");
115 expect(mergeText(base, "a\nX\nc\nd\ne\n", "a\nY\nc\nd\ne\n")).toBeNull();
116 expect(mergeText(base, "a\nb\nc\nd\ne\nf\n", "z\na\nb\nc\nd\ne\n")).toBe("z\na\nb\nc\nd\ne\nf\n");
117 });
118
119 it("three-way merges trees and finds the merge base", async () => {
120 const store = new MemoryStore();
121 const sig = { name: "T", email: "t@x", timestamp: 1, timezone: "+0000" };
122 const commit = async (tree: string, parents: string[], ts: number) =>
123 store.write("commit", serializeCommit({ tree, parents, author: { ...sig, timestamp: ts }, committer: { ...sig, timestamp: ts }, message: "m\n" }));
124 const tb = await applyChanges(store, null, [{ path: "f.txt", content: utf8("1\n2\n3\n4\n5\n") }, { path: "g.txt", content: utf8("g") }]);
125 const base = await commit(tb, [], 1);
126 const to = await applyChanges(store, tb, [{ path: "f.txt", content: utf8("one\n2\n3\n4\n5\n") }, { path: "new.txt", content: utf8("n") }]);
127 const ours = await commit(to, [base], 2);
128 const tt = await applyChanges(store, tb, [{ path: "f.txt", content: utf8("1\n2\n3\n4\nfive\n") }, { path: "g.txt", delete: true }]);
129 const theirs = await commit(tt, [base], 3);
130 expect(await mergeBase(store, ours, theirs)).toBe(base);
131 const res = await mergeTrees(store, tb, to, tt);
132 expect(res.conflicts).toEqual([]);
133 const files = await listFiles(store, res.tree!);
134 expect(files.map((f) => f.path)).toEqual(["f.txt", "new.txt"]);
135 expect(fromUtf8((await store.read(files[0]!.oid))!.content)).toBe("one\n2\n3\n4\nfive\n");
136 expect(parseCommit((await store.read(ours))!.content).parents).toEqual([base]);
137 expect((await readCommit(store, theirs)).tree).toBe(tt);
138 });
139});