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 files | 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 | */ | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 7 | import type { Branch, Commit, Pull, RepoPath, Viewer } from "@g1t/contracts"; |
| Branches and Tags pages, each file's last commit, and the branch menu on files | 8 | |
| 9 | import type { ActiveBranch } from "../components/branches"; | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 10 | import { bounded, drift } from "./branches"; |
| 11 | import { immutable } from "./immutable.server"; | |
| Branches and Tags pages, each file's last commit, and the branch menu on files | 12 | import { repos } from "./services.server"; |
| 13 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 14 | /** |
| 15 | * How deep each history is read, in turn, to find where a branch and the | |
| 16 | * default branch meet: most branches meet it within the first; a branch | |
| 17 | * left long ago needs the default branch's history further back; one far | |
| 18 | * from both reads both deeply. Past the last, the counts are not shown. | |
| 19 | */ | |
| 20 | const DEPTHS: ReadonlyArray<{ branch: number; main: number }> = [ | |
| 21 | { branch: 40, main: 120 }, | |
| 22 | { branch: 40, main: 1000 }, | |
| 23 | { branch: 1000, main: 1000 }, | |
| 24 | ]; | |
| 25 | /** Branches counted at once. */ | |
| 26 | const COUNTING = 8; | |
| Branches and Tags pages, each file's last commit, and the branch menu on files | 27 | |
| 28 | type Preview = { branch?: string | null; number?: number | null; url: string }; | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 29 | /** What never changes for a branch head and a default branch head. */ |
| 30 | type Measured = { commit: ActiveBranch["commit"]; drift: ActiveBranch["drift"] }; | |
| 31 | ||
| 32 | const summary = (commit: Commit | undefined): ActiveBranch["commit"] => | |
| 33 | commit ? { hash: commit.hash, message: commit.message.split("\n")[0] ?? "", author: commit.author.name, at: commit.authoredAt } : null; | |
| Branches and Tags pages, each file's last commit, and the branch menu on files | 34 | |
| 35 | /** | |
| 36 | * The branches other than the default, at most `read` of them read (those | |
| 37 | * with an open pull request first), newest commit first. `main` is the | |
| 38 | * default branch's head commit with its first line. | |
| 39 | */ | |
| 40 | export async function readBranches( | |
| 41 | path: RepoPath, | |
| 42 | viewer: Viewer, | |
| 43 | input: { defaultBranch: string; branches: Branch[]; pulls: Pull[]; previews: Preview[] }, | |
| 44 | read: number, | |
| 45 | ): Promise<{ main: string; total: number; shown: ActiveBranch[]; head: ActiveBranch["commit"] }> { | |
| 46 | const soft = <T,>(promise: Promise<T>): Promise<T | null> => promise.catch(() => null); | |
| 47 | const main = input.defaultBranch; | |
| 48 | const pullOn = new Map(input.pulls.filter((pull) => pull.branch).map((pull) => [pull.branch as string, pull])); | |
| 49 | const others = input.branches.filter((branch) => branch.name !== main); | |
| 50 | const reading = [...others.filter((b) => pullOn.has(b.name)), ...others.filter((b) => !pullOn.has(b.name))].slice(0, read); | |
| 51 | // By commit hash, not name: history from a commit never changes, so | |
| 52 | // repos keeps it (services/repos/src/store.rs) and only new heads cost a walk. | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 53 | const mainHead = input.branches.find((branch) => branch.name === main)?.hash ?? null; |
| 54 | const log = async (hash: string, depth: number): Promise<Commit[] | null> => { | |
| 55 | const found = await soft(repos.log(path, viewer, hash, depth)); | |
| 56 | return found?.ok && found.value.length > 0 ? found.value : null; | |
| 57 | }; | |
| 58 | // The default branch's history, read once per depth for every branch, | |
| 59 | // and not read deeper when a shallower read already reached its start. | |
| 60 | const mainLogs = new Map<number, Promise<Commit[] | null>>(); | |
| 61 | const mainLog = (depth: number): Promise<Commit[] | null> => { | |
| 62 | if (!mainHead) return Promise.resolve(null); | |
| 63 | let kept = mainLogs.get(depth); | |
| 64 | if (!kept) { | |
| 65 | const shallower = Math.max(0, ...[...mainLogs.keys()].filter((read) => read < depth)); | |
| 66 | const before = mainLogs.get(shallower); | |
| 67 | kept = before | |
| 68 | ? before.then((read) => (read && read.length < shallower ? read : log(mainHead, depth))) | |
| 69 | : log(mainHead, depth); | |
| 70 | mainLogs.set(depth, kept); | |
| 71 | } | |
| 72 | return kept; | |
| 73 | }; | |
| 74 | // Reads deeper only while the two histories have not met. Null when a | |
| 75 | // read failed, so a failure is not kept as the answer. | |
| 76 | const measure = async (head: string, main: string): Promise<Measured | null> => { | |
| 77 | let branch: Commit[] | null = null; | |
| 78 | let readTo = 0; | |
| 79 | for (const depth of DEPTHS) { | |
| 80 | // Not read again when no deeper, or when it already reached the start. | |
| 81 | const again: boolean = branch == null || (depth.branch > readTo && branch.length >= readTo); | |
| 82 | const [read, mainRead]: [Commit[] | null, Commit[] | null] = await Promise.all([ | |
| 83 | again ? log(head, depth.branch) : branch, | |
| 84 | mainLog(depth.main), | |
| 85 | ]); | |
| 86 | if (!read || !mainRead) return null; | |
| 87 | if (again) readTo = depth.branch; | |
| 88 | branch = read; | |
| 89 | const counted = drift(head, main, [...mainRead, ...read]); | |
| 90 | if (counted) return { commit: summary(read[0]), drift: counted }; | |
| 91 | } | |
| 92 | return { commit: summary(branch?.[0]), drift: null }; | |
| 93 | }; | |
| 94 | // Kept by the pair of heads: neither history can change, so neither can | |
| 95 | // the answer. Without one, the head commit alone. | |
| 96 | const measureOrHead = async (branch: Branch): Promise<Measured> => { | |
| 97 | const kept = | |
| 98 | branch.hash && mainHead | |
| 99 | ? await immutable(`branch-drift:${path.namespace}/${path.name}:${mainHead}:${branch.hash}`, () => measure(branch.hash, mainHead)) | |
| 100 | : null; | |
| 101 | if (kept) return kept; | |
| 102 | const head = await log(branch.hash || branch.name, 1); | |
| 103 | return { commit: summary(head?.[0]), drift: null }; | |
| 104 | }; | |
| 105 | const [mainTop, measured] = await Promise.all([ | |
| 106 | mainHead ? log(mainHead, 1) : Promise.resolve(null), | |
| 107 | bounded(reading, COUNTING, measureOrHead), | |
| Branches and Tags pages, each file's last commit, and the branch menu on files | 108 | ]); |
| 109 | const shown = reading | |
| 110 | .map((branch, index): ActiveBranch => { | |
| 111 | const pull = pullOn.get(branch.name); | |
| 112 | return { | |
| 113 | name: branch.name, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 114 | commit: measured[index]?.commit ?? null, |
| 115 | drift: measured[index]?.drift ?? null, | |
| Branches and Tags pages, each file's last commit, and the branch menu on files | 116 | pull: pull ? { number: pull.number, title: pull.title, checkStatus: pull.checkStatus, draft: pull.status === "draft" } : null, |
| 117 | preview: input.previews.find((app) => app.branch === branch.name || (pull != null && app.number === pull.number))?.url ?? null, | |
| 118 | }; | |
| 119 | }) | |
| 120 | .sort((a, b) => Date.parse(b.commit?.at ?? "0") - Date.parse(a.commit?.at ?? "0")); | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 121 | return { main, total: others.length, shown, head: summary(mainTop?.[0]) }; |
| Branches and Tags pages, each file's last commit, and the branch menu on files | 122 | } |