import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { removeWithRetries } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; import * as git from "../src/utils/git"; const gitInitHelp = await $`git init -h`.quiet().nothrow().text(); const supportsReftable = gitInitHelp.includes("--ref-format"); describe.skipIf(!supportsReftable)("git reftable support", () => { // All git plumbing (init, commits, branch, worktree) is real I/O that exercises // the reftable backend, but none of it is the contract under test in the bodies // below — the bodies test our *resolution* code against an already-built reftable // repo. So the heavy plumbing is built once in beforeAll and shared: // - `sharedRepoDir`: a reftable repo with two committed branches (main + // feature-branch, HEAD on feature-branch), reused by the repository and // worktree tests. Neither test mutates it, so one fixture is safe. // - `worktreeDir`: a linked worktree (wt-branch) off the shared repo. // - `configRepoDir`: a separate reftable repo for the config-comment test, // which rewrites `.git/config` on disk and therefore must not share state. let sharedRepoDir: string; let worktreeDir: string; let configRepoDir: string; let headSha: string; beforeAll(async () => { // Shared reftable repo: two distinct commits on two branches so ref // resolution has independent main/feature-branch targets to resolve. sharedRepoDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-reftable-")); const initResult = await $`git init --ref-format=reftable --initial-branch=main`.cwd(sharedRepoDir).quiet(); if (initResult.exitCode !== 0) throw new Error(`reftable git init failed (exit ${initResult.exitCode})`); await $`git config user.name "Test User"`.cwd(sharedRepoDir).quiet(); await $`git config user.email "test@example.com"`.cwd(sharedRepoDir).quiet(); await fs.writeFile(path.join(sharedRepoDir, "file.txt"), "hello world"); await $`git add file.txt`.cwd(sharedRepoDir).quiet(); await $`git commit -m "initial commit"`.cwd(sharedRepoDir).quiet(); await $`git checkout -b feature-branch`.cwd(sharedRepoDir).quiet(); await fs.writeFile(path.join(sharedRepoDir, "file2.txt"), "hello feature"); await $`git add file2.txt`.cwd(sharedRepoDir).quiet(); await $`git commit -m "feature commit"`.cwd(sharedRepoDir).quiet(); // Ground-truth HEAD sha, resolved independently of our utilities. headSha = (await $`git rev-parse HEAD`.cwd(sharedRepoDir).quiet().text()).trim(); // Linked worktree on its own branch, off the shared repo's HEAD. worktreeDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-reftable-wt-")); await $`git worktree add ${worktreeDir} -b wt-branch`.cwd(sharedRepoDir).quiet(); // Independent reftable repo for the config-comment test (no commits needed; // it only inspects/rewrites the freshly-initialized config). configRepoDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-reftable-cfg-")); const cfgInit = await $`git init --ref-format=reftable --initial-branch=main`.cwd(configRepoDir).quiet(); if (cfgInit.exitCode !== 0) throw new Error(`reftable git init (config) failed (exit ${cfgInit.exitCode})`); }); afterAll(async () => { await $`git worktree remove ${worktreeDir} -f`.cwd(sharedRepoDir).quiet().nothrow(); await removeWithRetries(worktreeDir).catch(() => {}); await removeWithRetries(sharedRepoDir).catch(() => {}); await removeWithRetries(configRepoDir).catch(() => {}); }); test("resolves references in a reftable repository", async () => { const repository = await git.repo.resolve(sharedRepoDir); expect(repository).not.toBeNull(); const currentBranch = await git.branch.current(sharedRepoDir); expect(currentBranch).toBe("feature-branch"); const resolvedHeadSha = await git.head.sha(sharedRepoDir); expect(resolvedHeadSha).not.toBeNull(); expect(resolvedHeadSha).toHaveLength(40); expect(resolvedHeadSha).toBe(headSha); // Resolve refs/heads/main and refs/heads/feature-branch const mainSha = await git.ref.resolve(sharedRepoDir, "refs/heads/main"); const featureSha = await git.ref.resolve(sharedRepoDir, "refs/heads/feature-branch"); expect(mainSha).not.toBeNull(); expect(featureSha).not.toBeNull(); expect(mainSha).toHaveLength(40); expect(featureSha).toHaveLength(40); expect(featureSha).toBe(headSha); // Test HEAD resolution (object shape) const headState = await git.head.resolve(sharedRepoDir); expect(headState).not.toBeNull(); if (headState?.kind !== "ref") throw new Error("expected ref head"); expect(headState.branchName).toBe("feature-branch"); expect(headState.commit).toBe(headSha); // Test HEAD resolution sync const headStateSync = git.head.resolveSync(sharedRepoDir); expect(headStateSync).not.toBeNull(); if (headStateSync?.kind !== "ref") throw new Error("expected ref head sync"); expect(headStateSync.branchName).toBe("feature-branch"); expect(headStateSync.commit).toBe(headSha); // Test exists check const mainExists = await git.ref.exists(sharedRepoDir, "refs/heads/main"); const nonexistentExists = await git.ref.exists(sharedRepoDir, "refs/heads/nonexistent"); expect(mainExists).toBe(true); expect(nonexistentExists).toBe(false); }); test("handles git config trailing comments correctly", async () => { const repository = await git.repo.resolve(configRepoDir); expect(repository).not.toBeNull(); if (!repository) return; expect(await git.repo.isReftable(repository)).toBe(true); // Now let's manually write to .git/config with comments and test const configPath = path.join(repository.commonDir, "config"); const baseConfig = await fs.readFile(configPath, "utf8"); // Test trailing semicolon comment const newConfigWithSemicolon = baseConfig.replace( "refstorage = reftable", "refstorage = reftable ; trailing comment", ); await fs.writeFile(configPath, newConfigWithSemicolon); const repository2 = await git.repo.resolve(configRepoDir); expect(repository2).not.toBeNull(); if (repository2) { expect(await git.repo.isReftable(repository2)).toBe(true); expect(git.repo.isReftableSync(repository2)).toBe(true); } // Test trailing hash comment const newConfigWithHash = baseConfig.replace("refstorage = reftable", "refstorage = reftable # trailing hash"); await fs.writeFile(configPath, newConfigWithHash); const repository3 = await git.repo.resolve(configRepoDir); expect(repository3).not.toBeNull(); if (repository3) { expect(await git.repo.isReftable(repository3)).toBe(true); expect(git.repo.isReftableSync(repository3)).toBe(true); } // Test double-quoted value containing semicolon (not a comment) const newConfigWithQuotes = baseConfig.replace("refstorage = reftable", 'refstorage = "reftable ; not comment"'); await fs.writeFile(configPath, newConfigWithQuotes); const repository4 = await git.repo.resolve(configRepoDir); expect(repository4).not.toBeNull(); if (repository4) { // This value would be "reftable ; not comment", which shouldn't match "reftable" expect(await git.repo.isReftable(repository4)).toBe(false); expect(git.repo.isReftableSync(repository4)).toBe(false); } // Test adjacent hash comment (no preceding space) const newConfigWithAdjacentHash = baseConfig.replace( "refstorage = reftable", "refstorage = reftable#adjacenthash", ); await fs.writeFile(configPath, newConfigWithAdjacentHash); const repository5 = await git.repo.resolve(configRepoDir); expect(repository5).not.toBeNull(); if (repository5) { expect(await git.repo.isReftable(repository5)).toBe(true); expect(git.repo.isReftableSync(repository5)).toBe(true); } // Test adjacent semicolon comment (no preceding space) const newConfigWithAdjacentSemicolon = baseConfig.replace( "refstorage = reftable", "refstorage = reftable;adjacentsemi", ); await fs.writeFile(configPath, newConfigWithAdjacentSemicolon); const repository6 = await git.repo.resolve(configRepoDir); expect(repository6).not.toBeNull(); if (repository6) { expect(await git.repo.isReftable(repository6)).toBe(true); expect(git.repo.isReftableSync(repository6)).toBe(true); } // Test section header with trailing comment const newConfigWithSectionComment = baseConfig.replace( "[extensions]", "[extensions] # extensions section comment", ); await fs.writeFile(configPath, newConfigWithSectionComment); const repository7 = await git.repo.resolve(configRepoDir); expect(repository7).not.toBeNull(); if (repository7) { expect(await git.repo.isReftable(repository7)).toBe(true); expect(git.repo.isReftableSync(repository7)).toBe(true); } }); test("resolves references in a reftable worktree", async () => { // Resolve the repository for the worktree (built in beforeAll) const repository = await git.repo.resolve(worktreeDir); expect(repository).not.toBeNull(); if (!repository) return; expect(repository.gitDir).not.toBe(repository.commonDir); expect(await git.repo.isReftable(repository)).toBe(true); // Check current branch on worktree const currentBranch = await git.branch.current(worktreeDir); expect(currentBranch).toBe("wt-branch"); // Check that HEAD resolves correctly in the worktree const headState = await git.head.resolve(worktreeDir); expect(headState).not.toBeNull(); if (headState?.kind !== "ref") throw new Error("expected ref head in worktree"); expect(headState.branchName).toBe("wt-branch"); }); });