import { describe, expect, mock, test } from "bun:test"; import type { CommitSummary } from "~/lib/types"; mock.module("cloudflare:workers", () => ({ env: {}, waitUntil: () => undefined })); type Entry = { name: string; mode: string; hash: string }; const commits = new Map(); const trees = new Map(); const blobs = new Map(); function ancestors(sha: string): Set { const seen = new Set(); const stack = [sha]; while (stack.length > 0) { const next = stack.pop() as string; if (seen.has(next)) continue; seen.add(next); stack.push(...(commits.get(next)?.parents ?? [])); } return seen; } mock.module("./git/history", () => ({ async compareCommits(_base: unknown, baseSha: string, _head: unknown, headSha: string) { const fromBase = ancestors(baseSha); const ahead: CommitSummary[] = []; let mergeBase: string | null = null; let cursor: string | undefined = headSha; while (cursor) { if (fromBase.has(cursor)) { mergeBase = cursor; break; } const commit = commits.get(cursor); if (!commit) break; ahead.push(commit); cursor = commit.parents[0]; } return { mergeBase, commits: ahead, behindBy: 0, truncated: false }; }, isAncestor: async (ancestor: string, descendant: string) => ancestors(descendant).has(ancestor), })); const { makeBlob, makeCommit, makeTree } = await import("./git/objects"); const { planRebase, RebaseConflictError } = await import("./pull-rebase"); const store = { commit: async (sha: string) => commits.get(sha) ?? null, tree: async (sha: string) => trees.get(sha) ?? null, blob: async (sha: string) => blobs.get(sha) ?? null, } as never; const who = { name: "Tester", email: "t@example.com", timestamp: 1_700_000_000, timezone: "+0000" }; async function commit(files: Record, parent: string | null, message: string): Promise { const items = []; for (const [name, content] of Object.entries(files)) { const blob = await makeBlob(content); blobs.set(blob.sha, blob.content); items.push({ mode: "100644", name, sha: blob.sha }); } const tree = await makeTree(items); trees.set( tree.sha, items.map((item) => ({ name: item.name, mode: item.mode, hash: item.sha })), ); const raw = await makeCommit({ tree: tree.sha, parents: parent ? [parent] : [], author: who, committer: who, message, }); commits.set(raw.sha, { sha: raw.sha, shortSha: raw.sha.slice(0, 7), subject: message, message, authorName: who.name, authorEmail: who.email, committerName: who.name, authoredAt: who.timestamp * 1000, committedAt: who.timestamp * 1000, parents: parent ? [parent] : [], treeSha: tree.sha, }); return raw.sha; } const labels = { ours: "base", theirs: "head" }; const committer = { name: "Restacker", email: "r@example.com" }; describe("planRebase with an upstream (rebase --onto)", () => { test("replays only the child's commits onto an amended parent", async () => { const main = await commit({ "a.txt": "base\n" }, null, "init"); const parent = await commit({ "a.txt": "parent\n" }, main, "parent"); const child = await commit({ "a.txt": "parent\n", "b.txt": "child\n" }, parent, "child"); const amended = await commit({ "a.txt": "parent v2\n" }, main, "parent (amended)"); await expect(planRebase(store, amended, child, committer, labels)).rejects.toBeInstanceOf(RebaseConflictError); const plan = await planRebase(store, amended, child, committer, labels, parent); expect(plan?.kind).toBe("rebased"); if (plan?.kind !== "rebased") return; const rewritten = plan.objects.find((object) => object.sha === plan.sha); const text = new TextDecoder().decode(rewritten?.content); expect(text).toContain(`parent ${amended}`); expect(text).toContain("child"); }); test("restacks a child onto its parent's squash commit", async () => { const main = await commit({ "a.txt": "base\n" }, null, "init 2"); const parent1 = await commit({ "a.txt": "one\n" }, main, "parent 1"); const parent2 = await commit({ "a.txt": "two\n" }, parent1, "parent 2"); const child = await commit({ "a.txt": "two\n", "c.txt": "child\n" }, parent2, "child 2"); const squash = await commit({ "a.txt": "two\n" }, main, "parent squashed"); const plan = await planRebase(store, squash, child, committer, labels, parent2); expect(plan?.kind).toBe("rebased"); if (plan?.kind !== "rebased") return; const commitObjects = plan.objects.filter((object) => object.type === "commit"); expect(commitObjects).toHaveLength(1); }); test("a child already on the target needs no new commits", async () => { const main = await commit({ "a.txt": "base\n" }, null, "init 3"); const parent = await commit({ "a.txt": "p\n" }, main, "p"); const child = await commit({ "a.txt": "p\n", "d.txt": "d\n" }, parent, "d"); expect(await planRebase(store, parent, child, committer, labels, parent)).toEqual({ kind: "fast_forward" }); const sibling = await commit({ "a.txt": "base\n", "e.txt": "e\n" }, main, "e"); const onSibling = await commit({ "a.txt": "base\n", "e.txt": "e\n", "f.txt": "f\n" }, sibling, "f"); expect(await planRebase(store, sibling, onSibling, committer, labels, sibling)).toEqual({ kind: "fast_forward" }); }); });