diff --git a/apps/server/src/git.test.ts b/apps/server/src/git.test.ts index 5ceddd7db93fa5f85ad60b5e8d59fb45cf3c78ca..e0612f09f3e57ce1f95bfcb458373f1409965401 100644 --- a/apps/server/src/git.test.ts +++ b/apps/server/src/git.test.ts @@ -51,6 +51,25 @@ async function initRepoWithCommit(cwd: string): Promise { await git(cwd, "commit -m 'initial commit'"); } +async function commitWithDate( + cwd: string, + fileName: string, + fileContents: string, + dateIsoString: string, + message: string, +): Promise { + await writeFile(path.join(cwd, fileName), fileContents); + await git(cwd, `add ${fileName}`); + const commitResult = await runTerminalCommand({ + command: `GIT_AUTHOR_DATE="${dateIsoString}" GIT_COMMITTER_DATE="${dateIsoString}" git commit -m "${message}"`, + cwd, + timeoutMs: 10_000, + }); + if (commitResult.code !== 0) { + throw new Error(`git dated commit failed: ${commitResult.stderr}`); + } +} + // ── Tests ── describe("git integration", () => { @@ -106,14 +125,39 @@ describe("git integration", () => { expect(current!.current).toBe(true); }); - it("sorts current branch first", async () => { + it("sorts branches by most recent commit first", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); - await createGitBranch({ cwd: tmp.path, branch: "aaa-first-alpha" }); - await createGitBranch({ cwd: tmp.path, branch: "zzz-last" }); + const initialBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (branch) => branch.current, + )!.name; + + await createGitBranch({ cwd: tmp.path, branch: "older-branch" }); + await checkoutGitBranch({ cwd: tmp.path, branch: "older-branch" }); + await commitWithDate( + tmp.path, + "older.txt", + "older branch change\n", + "Thu, 1 Jan 2037 00:00:00 +0000", + "older branch change", + ); + + await checkoutGitBranch({ cwd: tmp.path, branch: initialBranch }); + await createGitBranch({ cwd: tmp.path, branch: "newer-branch" }); + await checkoutGitBranch({ cwd: tmp.path, branch: "newer-branch" }); + await commitWithDate( + tmp.path, + "newer.txt", + "newer branch change\n", + "Fri, 1 Jan 2038 00:00:00 +0000", + "newer branch change", + ); + + // Switch away to show sort order is recency-based, not "current"-based. + await checkoutGitBranch({ cwd: tmp.path, branch: "older-branch" }); const result = await listGitBranches({ cwd: tmp.path }); - expect(result.branches[0]!.current).toBe(true); + expect(result.branches[0]!.name).toBe("newer-branch"); }); it("lists multiple branches after creating them", async () => { diff --git a/apps/server/src/git.ts b/apps/server/src/git.ts index 6c55ef9cdd1d35e5391f78dec1d59995d8c9e55d..734ee3038b062684720b14458e36c6daf282d16e 100644 --- a/apps/server/src/git.ts +++ b/apps/server/src/git.ts @@ -326,7 +326,7 @@ export class GitCoreService { throw new Error(stderr || "git branch failed"); } - const [defaultRef, worktreeList] = await Promise.all([ + const [defaultRef, worktreeList, branchRecency] = await Promise.all([ executeGit(input.cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"], { timeoutMs: 5_000, allowNonZeroExit: true, @@ -335,6 +335,14 @@ export class GitCoreService { timeoutMs: 5_000, allowNonZeroExit: true, }), + executeGit( + input.cwd, + ["for-each-ref", "--format=%(refname:short)%09%(committerdate:unix)", "refs/heads"], + { + timeoutMs: 5_000, + allowNonZeroExit: true, + }, + ), ]); const defaultBranch = defaultRef.code === 0 @@ -356,6 +364,21 @@ export class GitCoreService { } } + const branchLastCommit = new Map(); + if (branchRecency.code === 0) { + for (const line of branchRecency.stdout.split("\n")) { + if (line.length === 0) { + continue; + } + const [name, lastCommitRaw] = line.split("\t"); + if (!name) { + continue; + } + const lastCommit = Number.parseInt(lastCommitRaw ?? "0", 10); + branchLastCommit.set(name, Number.isFinite(lastCommit) ? lastCommit : 0); + } + } + const branches = result.stdout .split("\n") .map((line) => line.trim()) @@ -370,6 +393,9 @@ export class GitCoreService { }; }) .toSorted((a, b) => { + const aLastCommit = branchLastCommit.get(a.name) ?? 0; + const bLastCommit = branchLastCommit.get(b.name) ?? 0; + if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit; if (a.current !== b.current) return a.current ? -1 : 1; if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1; return a.name.localeCompare(b.name);