authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-12 23:18:24-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-12 23:29:44-08:00
logba9585966c948d761054d54ccaaa71a1420bd394
tree413cb808022a2b96a8c65296ac740ceabd770101
parent293f7b1507b694e10abd9030fa3dccf3635ab3c0

Sort git branches by latest commit timestamp

- Order `listGitBranches` by each branch's most recent commit date - Keep current/default/name tiebreakers for stable ordering - Add integration test coverage with dated commits to verify recency sort

2 files changed, 75 insertions(+), 5 deletions(-)

apps/server/src/git.test.ts+48-4
......@@ -51,6 +51,25 @@ async function initRepoWithCommit(cwd: string): Promise<void> {
5151 await git(cwd, "commit -m 'initial commit'");
5252}
5353
54async function commitWithDate(
55 cwd: string,
56 fileName: string,
57 fileContents: string,
58 dateIsoString: string,
59 message: string,
60): Promise<void> {
61 await writeFile(path.join(cwd, fileName), fileContents);
62 await git(cwd, `add ${fileName}`);
63 const commitResult = await runTerminalCommand({
64 command: `GIT_AUTHOR_DATE="${dateIsoString}" GIT_COMMITTER_DATE="${dateIsoString}" git commit -m "${message}"`,
65 cwd,
66 timeoutMs: 10_000,
67 });
68 if (commitResult.code !== 0) {
69 throw new Error(`git dated commit failed: ${commitResult.stderr}`);
70 }
71}
72
5473// ── Tests ──
5574
5675describe("git integration", () => {
......@@ -106,14 +125,39 @@ describe("git integration", () => {
106125 expect(current!.current).toBe(true);
107126 });
108127
109 it("sorts current branch first", async () => {
128 it("sorts branches by most recent commit first", async () => {
110129 await using tmp = await makeTmpDir();
111130 await initRepoWithCommit(tmp.path);
112 await createGitBranch({ cwd: tmp.path, branch: "aaa-first-alpha" });
113 await createGitBranch({ cwd: tmp.path, branch: "zzz-last" });
131 const initialBranch = (await listGitBranches({ cwd: tmp.path })).branches.find(
132 (branch) => branch.current,
133 )!.name;
134
135 await createGitBranch({ cwd: tmp.path, branch: "older-branch" });
136 await checkoutGitBranch({ cwd: tmp.path, branch: "older-branch" });
137 await commitWithDate(
138 tmp.path,
139 "older.txt",
140 "older branch change\n",
141 "Thu, 1 Jan 2037 00:00:00 +0000",
142 "older branch change",
143 );
144
145 await checkoutGitBranch({ cwd: tmp.path, branch: initialBranch });
146 await createGitBranch({ cwd: tmp.path, branch: "newer-branch" });
147 await checkoutGitBranch({ cwd: tmp.path, branch: "newer-branch" });
148 await commitWithDate(
149 tmp.path,
150 "newer.txt",
151 "newer branch change\n",
152 "Fri, 1 Jan 2038 00:00:00 +0000",
153 "newer branch change",
154 );
155
156 // Switch away to show sort order is recency-based, not "current"-based.
157 await checkoutGitBranch({ cwd: tmp.path, branch: "older-branch" });
114158
115159 const result = await listGitBranches({ cwd: tmp.path });
116 expect(result.branches[0]!.current).toBe(true);
160 expect(result.branches[0]!.name).toBe("newer-branch");
117161 });
118162
119163 it("lists multiple branches after creating them", async () => {
apps/server/src/git.ts+27-1
......@@ -326,7 +326,7 @@ export class GitCoreService {
326326 throw new Error(stderr || "git branch failed");
327327 }
328328
329 const [defaultRef, worktreeList] = await Promise.all([
329 const [defaultRef, worktreeList, branchRecency] = await Promise.all([
330330 executeGit(input.cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"], {
331331 timeoutMs: 5_000,
332332 allowNonZeroExit: true,
......@@ -335,6 +335,14 @@ export class GitCoreService {
335335 timeoutMs: 5_000,
336336 allowNonZeroExit: true,
337337 }),
338 executeGit(
339 input.cwd,
340 ["for-each-ref", "--format=%(refname:short)%09%(committerdate:unix)", "refs/heads"],
341 {
342 timeoutMs: 5_000,
343 allowNonZeroExit: true,
344 },
345 ),
338346 ]);
339347 const defaultBranch =
340348 defaultRef.code === 0
......@@ -356,6 +364,21 @@ export class GitCoreService {
356364 }
357365 }
358366
367 const branchLastCommit = new Map<string, number>();
368 if (branchRecency.code === 0) {
369 for (const line of branchRecency.stdout.split("\n")) {
370 if (line.length === 0) {
371 continue;
372 }
373 const [name, lastCommitRaw] = line.split("\t");
374 if (!name) {
375 continue;
376 }
377 const lastCommit = Number.parseInt(lastCommitRaw ?? "0", 10);
378 branchLastCommit.set(name, Number.isFinite(lastCommit) ? lastCommit : 0);
379 }
380 }
381
359382 const branches = result.stdout
360383 .split("\n")
361384 .map((line) => line.trim())
......@@ -370,6 +393,9 @@ export class GitCoreService {
370393 };
371394 })
372395 .toSorted((a, b) => {
396 const aLastCommit = branchLastCommit.get(a.name) ?? 0;
397 const bLastCommit = branchLastCommit.get(b.name) ?? 0;
398 if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
373399 if (a.current !== b.current) return a.current ? -1 : 1;
374400 if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1;
375401 return a.name.localeCompare(b.name);