g1t/apps/web/app/lib/branches.server.ts

60 lines3,242 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Branches and Tags pages, each file's last commit, and the branch menu on files1/**
2 * A repository's branches as its pages show them: each one's head commit,
3 * how far it has moved from the default branch, the pull request open on
4 * it with its checks, and its preview. The overview shows the newest few;
5 * the Branches page shows them all.
6 */
7import type { Branch, Pull, RepoPath, Viewer } from "@g1t/contracts";
8
9import type { ActiveBranch } from "../components/branches";
10import { drift } from "./branches";
11import { repos } from "./services.server";
12
13/** How far back each branch's history, and the default branch's, is read to count ahead and behind. */
14export const BRANCH_DEPTH = 40;
15export const MAIN_DEPTH = 120;
16
17type Preview = { branch?: string | null; number?: number | null; url: string };
18
19/**
20 * The branches other than the default, at most `read` of them read (those
21 * with an open pull request first), newest commit first. `main` is the
22 * default branch's head commit with its first line.
23 */
24export async function readBranches(
25 path: RepoPath,
26 viewer: Viewer,
27 input: { defaultBranch: string; branches: Branch[]; pulls: Pull[]; previews: Preview[] },
28 read: number,
29): Promise<{ main: string; total: number; shown: ActiveBranch[]; head: ActiveBranch["commit"] }> {
30 const soft = <T,>(promise: Promise<T>): Promise<T | null> => promise.catch(() => null);
31 const main = input.defaultBranch;
32 const pullOn = new Map(input.pulls.filter((pull) => pull.branch).map((pull) => [pull.branch as string, pull]));
33 const others = input.branches.filter((branch) => branch.name !== main);
34 const reading = [...others.filter((b) => pullOn.has(b.name)), ...others.filter((b) => !pullOn.has(b.name))].slice(0, read);
35 // By commit hash, not name: history from a commit never changes, so
36 // repos keeps it (services/repos/src/store.rs) and only new heads cost a walk.
37 const mainHead = input.branches.find((branch) => branch.name === main)?.hash ?? main;
38 const [mainLog, ...logs] = await Promise.all([
39 soft(repos.log(path, viewer, mainHead, MAIN_DEPTH)),
40 ...reading.map((branch) => soft(repos.log(path, viewer, branch.hash || branch.name, BRANCH_DEPTH))),
41 ]);
42 const mainHistory = mainLog?.ok ? mainLog.value : [];
43 const mainHashes = mainHistory.map((c) => c.hash);
44 const summary = (commit: (typeof mainHistory)[number] | undefined): ActiveBranch["commit"] =>
45 commit ? { hash: commit.hash, message: commit.message.split("\n")[0] ?? "", author: commit.author.name, at: commit.authoredAt } : null;
46 const shown = reading
47 .map((branch, index): ActiveBranch => {
48 const history = logs[index]?.ok ? logs[index].value : [];
49 const pull = pullOn.get(branch.name);
50 return {
51 name: branch.name,
52 commit: summary(history[0]),
53 ...drift(history.map((c) => c.hash), mainHashes, BRANCH_DEPTH),
54 pull: pull ? { number: pull.number, title: pull.title, checkStatus: pull.checkStatus, draft: pull.status === "draft" } : null,
55 preview: input.previews.find((app) => app.branch === branch.name || (pull != null && app.number === pull.number))?.url ?? null,
56 };
57 })
58 .sort((a, b) => Date.parse(b.commit?.at ?? "0") - Date.parse(a.commit?.at ?? "0"));
59 return { main, total: others.length, shown, head: summary(mainHistory[0]) };
60}