import assert from "node:assert/strict"; import test from "node:test"; import { describeDefaultBranch, gatherCommitDetails, gatherCommits, gatherGraphCommits, gatherRepository, isDefaultBranch, isSameRepositoryPullRequest, mapWithConcurrency, normalizeRemoteUrl, parseBranchRecords, parseCommitRecords, parseDivergence, parseTracking, parseWorktreePorcelain, resolveDefaultBranch, } from "./git-data.mjs"; test("parses linked, detached, and locked worktrees", () => { const worktrees = parseWorktreePorcelain([ "worktree C:/repos/main", "HEAD 1111111111111111111111111111111111111111", "branch refs/heads/main", "", "worktree C:/repos/feature", "HEAD 2222222222222222222222222222222222222222", "detached", "locked in use", "", ].join("\n")); assert.deepEqual(worktrees, [ { path: "C:/repos/main", head: "1111111111111111111111111111111111111111", branch: "main", detached: false, bare: false, locked: false, prunable: false, }, { path: "C:/repos/feature", head: "2222222222222222222222222222222222222222", branch: null, detached: true, bare: false, locked: "in use", prunable: false, }, ]); }); test("parses branch tracking and excludes symbolic remote HEAD", () => { const separator = "\x1f"; const branches = parseBranchRecords([ ["refs/heads/main", "main", "a".repeat(40), "origin/main", "[ahead 2, behind 3]", "2026-01-02T03:04:05Z", "Main"].join(separator), ["refs/remotes/origin/HEAD", "origin/HEAD", "a".repeat(40), "", "", "", ""].join(separator), ["refs/remotes/origin/main", "origin/main", "a".repeat(40), "", "", "2026-01-02T03:04:05Z", "Main"].join(separator), ].join("\n")); assert.equal(branches.length, 2); assert.deepEqual(branches[0].tracking, { ahead: 2, behind: 3, gone: false }); assert.equal(branches[1].remote, true); assert.deepEqual(parseTracking("[gone]"), { ahead: 0, behind: 0, gone: true }); }); test("parses commit records with parents and timestamps", () => { const separator = "\x1f"; const recordSeparator = "\x1e"; const output = [ "a".repeat(40), "aaaaaaaa", `${"b".repeat(40)} ${"c".repeat(40)}`, "Ada", "ada@example.com", "2026-01-01T00:00:00Z", "2026-01-01T01:00:00Z", "Merge topic", ].join(separator) + recordSeparator; const [commit] = parseCommitRecords(output); assert.equal(commit.shortSha, "aaaaaaaa"); assert.equal(commit.parents.length, 2); assert.equal(commit.subject, "Merge topic"); }); test("parses branch divergence from git rev-list output", () => { assert.deepEqual(parseDivergence("3\t7"), { ahead: 7, behind: 3 }); assert.equal(parseDivergence("invalid"), null); }); test("resolves a remote default branch when origin HEAD is unavailable", () => { const branches = [ { ref: "refs/heads/main", remote: false }, { ref: "refs/remotes/origin/main", remote: true }, ]; assert.equal(resolveDefaultBranch(null, branches), "refs/remotes/origin/main"); assert.equal(resolveDefaultBranch("refs/remotes/upstream/trunk", branches), "refs/remotes/upstream/trunk"); assert.equal(resolveDefaultBranch(null, [{ ref: "refs/heads/main", remote: false }]), null); }); test("normalizes supported GitHub remote URL forms", () => { assert.deepEqual(normalizeRemoteUrl("git@github.com:octo/repo.git"), { raw: "git@github.com:octo/repo.git", host: "github.com", owner: "octo", repo: "repo", github: true, webUrl: "https://github.com/octo/repo", }); assert.equal(normalizeRemoteUrl("https://github.com/octo/repo.git").repo, "repo"); assert.equal(normalizeRemoteUrl("not a remote"), null); assert.equal(normalizeRemoteUrl("https://github.com/too/many/parts"), null); }); test("repository snapshot creates a virtual group for branches without worktrees", async () => { const root = process.cwd(); const sha = "a".repeat(40); const separator = "\x1f"; const runner = async (command, args) => { const key = `${command} ${args.join(" ")}`; if (key === "git rev-parse --show-toplevel") return { stdout: root, stderr: "" }; if (key === "git rev-parse --git-common-dir") return { stdout: ".git", stderr: "" }; if (key === "git rev-parse --verify HEAD") return { stdout: sha, stderr: "" }; if (key === "git remote get-url origin") return { stdout: "git@github.com:octo/repo.git", stderr: "" }; if (key === "git symbolic-ref --quiet refs/remotes/origin/HEAD") { return { stdout: "refs/remotes/origin/main", stderr: "" }; } if (key.startsWith("git status ")) return { stdout: "## main...origin/main\n M file.txt", stderr: "" }; if (key === "git worktree list --porcelain") { return { stdout: `worktree ${root}\nHEAD ${sha}\nbranch refs/heads/main\n`, stderr: "" }; } if (key.startsWith("git for-each-ref ") && key.includes("ahead-behind")) { const error = new Error("unknown field name: ahead-behind"); return { stdout: "", stderr: error.message, error }; } if (key.startsWith("git for-each-ref ")) { return { stdout: [ ["refs/heads/main", "main", sha, "origin/main", "", "2026-01-01T00:00:00Z", "Main"].join(separator), ["refs/heads/topic", "topic", sha, "", "", "2026-01-01T00:00:00Z", "Topic"].join(separator), ].join("\n"), stderr: "", }; } if (key.startsWith("git rev-list --left-right --count ")) { return { stdout: key.includes("refs/heads/topic") ? "4\t2" : "0\t0", stderr: "" }; } if (key.startsWith("gh pr list ")) { const error = new Error("not found"); error.code = "ENOENT"; return { stdout: "", stderr: "not found", error }; } throw new Error(`Unexpected command: ${key}`); }; const snapshot = await gatherRepository(root, { commandRunner: runner }); assert.equal(snapshot.repository.dirty, true); assert.equal(snapshot.repository.defaultBranch, "refs/remotes/origin/main"); assert.equal(snapshot.github.status, "unavailable"); assert.equal(snapshot.worktrees.length, 2); assert.deepEqual(snapshot.worktrees[1].branchIds, ["branch:topic"]); assert.equal(snapshot.branches[0].worktrees[0], root); assert.equal(snapshot.branches[0].isDefault, true); assert.equal(snapshot.branches[1].isDefault, false); assert.equal(snapshot.repository.defaultBranchName, "main"); assert.deepEqual(snapshot.branches[1].defaultTracking, { ahead: 2, behind: 4 }); }); test("branch divergence uses a single for-each-ref query when Git supports ahead-behind", async () => { const root = process.cwd(); const sha = "a".repeat(40); const separator = "\x1f"; const commands = []; const runner = async (command, args) => { const key = `${command} ${args.join(" ")}`; commands.push(key); if (key === "git rev-parse --show-toplevel") return { stdout: root, stderr: "" }; if (key === "git rev-parse --git-common-dir") return { stdout: ".git", stderr: "" }; if (key === "git rev-parse --verify HEAD") return { stdout: sha, stderr: "" }; if (key === "git remote get-url origin") return { stdout: "", stderr: "", error: new Error("none") }; if (key === "git symbolic-ref --quiet refs/remotes/origin/HEAD") { return { stdout: "refs/remotes/origin/feature/x", stderr: "" }; } if (key.startsWith("git status ")) return { stdout: "## x", stderr: "" }; if (key === "git worktree list --porcelain") { return { stdout: `worktree ${root}\nHEAD ${sha}\nbranch refs/heads/x\n`, stderr: "" }; } if (key.includes("ahead-behind")) { assert.ok(args.some((arg) => arg.includes("%(ahead-behind:refs/remotes/origin/feature/x)"))); return { stdout: [ `refs/heads/feature/x${separator}0 0`, `refs/heads/x${separator}3 1`, ].join("\n"), stderr: "", }; } if (key.startsWith("git for-each-ref ")) { return { stdout: [ ["refs/heads/feature/x", "feature/x", sha, "origin/feature/x", "", "2026-01-01T00:00:00Z", "Default"].join(separator), ["refs/heads/x", "x", sha, "", "", "2026-01-01T00:00:00Z", "Suffix"].join(separator), ].join("\n"), stderr: "", }; } throw new Error(`Unexpected command: ${key}`); }; const snapshot = await gatherRepository(root, { commandRunner: runner }); assert.ok(!commands.some((key) => key.startsWith("git rev-list ")), "should not spawn per-branch rev-list"); const byName = Object.fromEntries(snapshot.branches.map((branch) => [branch.name, branch])); assert.deepEqual(byName["feature/x"].defaultTracking, { ahead: 0, behind: 0 }); assert.deepEqual(byName.x.defaultTracking, { ahead: 3, behind: 1 }); assert.equal(byName["feature/x"].isDefault, true); assert.equal(byName.x.isDefault, false, "suffix of the default branch name must not be marked default"); }); test("per-branch divergence fallback is bounded to a small concurrency", async () => { const defaultBranch = "refs/remotes/origin/main"; const branches = Array.from({ length: 40 }, (_, index) => ({ ref: `refs/heads/b${index}`, name: `b${index}` })); let active = 0; let peak = 0; const runner = async (_command, args) => { if (args.includes("for-each-ref")) { return { stdout: "", stderr: "", error: new Error("old git") }; } active++; peak = Math.max(peak, active); await new Promise((resolve) => setTimeout(resolve, 2)); active--; return { stdout: "1\t2", stderr: "" }; }; const results = await mapWithConcurrency(branches, 8, async (branch) => { const result = await runner("git", ["rev-list", branch.ref]); return { ...branch, defaultTracking: result.error ? null : { ahead: 2, behind: 1 } }; }); assert.equal(results.length, 40); assert.ok(peak <= 8, `peak concurrency was ${peak}`); assert.deepEqual(results[39].defaultTracking, { ahead: 2, behind: 1 }); assert.equal(defaultBranch, "refs/remotes/origin/main"); }); test("default branch detection compares full branch names and upstreams", () => { const info = describeDefaultBranch("refs/remotes/origin/feature/x"); assert.deepEqual(info, { ref: "refs/remotes/origin/feature/x", short: "origin/feature/x", name: "feature/x" }); assert.equal(isDefaultBranch({ name: "feature/x", upstream: null }, info), true); assert.equal(isDefaultBranch({ name: "x", upstream: null }, info), false); assert.equal(isDefaultBranch({ name: "local-main", upstream: "origin/feature/x" }, info), true); assert.equal(isDefaultBranch({ name: "feature/x", upstream: "upstream/feature/x" }, info), false); assert.equal(isDefaultBranch({ name: "main" }, null), false); }); test("pull requests from forks are not attached to same-named local branches", () => { const remote = { owner: "octo", repo: "repo" }; assert.equal(isSameRepositoryPullRequest({ headRefName: "main", isCrossRepository: true }, remote), false); assert.equal(isSameRepositoryPullRequest({ headRefName: "main", isCrossRepository: false, headRepositoryOwner: { login: "Octo" }, }, remote), true); assert.equal(isSameRepositoryPullRequest({ headRefName: "main", headRepositoryOwner: { login: "contributor" }, }, remote), false); assert.equal(isSameRepositoryPullRequest({ headRefName: "main" }, remote), true); }); test("commit details run only metadata and file listing commands", async () => { const separator = "\x1f"; const commands = []; const sha = "a".repeat(40); const details = await gatherCommitDetails(process.cwd(), sha, null, { commandRunner: async (_command, args) => { commands.push(args[0]); if (args[0] === "show") { return { stdout: [sha, "aaaaaaaa", "", "Ada", "ada@example.com", "2026", "2026", "Subject", "Body"].join(separator), stderr: "", }; } return { stdout: "M\tsrc/app.js", stderr: "" }; }, }); assert.deepEqual(commands.sort(), ["diff-tree", "show"]); assert.equal(details.summary, undefined); assert.deepEqual(details.files, [{ status: "M", path: "src/app.js" }]); }); test("commit pagination returns a cursor only when more records exist", async () => { const separator = "\x1f"; const recordSeparator = "\x1e"; const output = Array.from({ length: 51 }, (_, index) => [ String(index).padStart(40, "a"), String(index).padStart(8, "a"), "", "Ada", "ada@example.com", "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z", `Commit ${index}`, ].join(separator) + recordSeparator).join(""); let receivedArgs; const runner = async (_command, args) => { receivedArgs = args; return { stdout: output, stderr: "" }; }; const page = await gatherCommits( process.cwd(), "refs/heads/topic", "refs/remotes/origin/main", 0, 50, { commandRunner: runner }, ); assert.equal(page.commits.length, 50); assert.equal(page.nextOffset, 50); assert.equal(page.comparisonBase, "refs/remotes/origin/main"); assert.equal(page.comparisonUnavailable, false); assert.deepEqual(receivedArgs.slice(-4), [ "refs/heads/topic", "--not", "refs/remotes/origin/main", "--", ]); }); test("combined graph uses all local branch refs in topological order", async () => { let receivedArgs; const runner = async (_command, args) => { receivedArgs = args; return { stdout: "", stderr: "" }; }; const page = await gatherGraphCommits( process.cwd(), ["refs/heads/main", "refs/heads/topic", "refs/remotes/origin/main"], 0, 100, { commandRunner: runner }, ); assert.equal(page.commits.length, 0); assert.ok(receivedArgs.includes("--topo-order")); assert.ok(receivedArgs.includes("--date-order")); assert.ok(receivedArgs.includes("refs/heads/main")); assert.ok(receivedArgs.includes("refs/heads/topic")); assert.ok(!receivedArgs.includes("refs/remotes/origin/main")); }); test("combined graph accepts pinned commit tips for stable pagination", async () => { const tip = "a".repeat(40); let receivedArgs; const runner = async (_command, args) => { receivedArgs = args; return { stdout: "", stderr: "" }; }; await gatherGraphCommits(process.cwd(), [tip, "--all"], 100, 100, { commandRunner: runner }); assert.ok(receivedArgs.includes(tip)); assert.ok(!receivedArgs.includes("--all")); assert.ok(receivedArgs.includes("--skip=100")); }); test("commit details reject non-SHA revisions before executing Git", async () => { await assert.rejects( gatherCommitDetails(process.cwd(), "--all", null, { commandRunner: async () => { throw new Error("should not run"); }, }), /Invalid commit SHA/, ); });