| 1 | /** |
| 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 | */ |
| 7 | import type { Branch, Pull, RepoPath, Viewer } from "@g1t/contracts"; |
| 8 | |
| 9 | import type { ActiveBranch } from "../components/branches"; |
| 10 | import { drift } from "./branches"; |
| 11 | import { 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. */ |
| 14 | export const BRANCH_DEPTH = 40; |
| 15 | export const MAIN_DEPTH = 120; |
| 16 | |
| 17 | type 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 | */ |
| 24 | export 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 | } |