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.
| What g1t can't do yet, and an open letter to Cloudflare | 1 | /** |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 2 | * Branches as the overview and the Branches page show them, from the |
| 3 | * repos service's `branch_drift` answer. The counting itself is done | |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 4 | * there (services/repos/src/drift.rs), kept by the pair of head commits. |
| 5 | */ | |
| 6 | import type { Branch, BranchDrifts, Commit, Pull } from "@g1t/contracts"; | |
| 7 | ||
| 8 | import type { ActiveBranch } from "../components/branches"; | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 9 | import type { ShownCommit } from "./commit-people"; |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 10 | |
| 11 | /** | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 12 | * How far a branch has moved from the default branch: commits it has that |
| 13 | * the default branch does not (ahead), and commits the default branch has | |
| 14 | * that it does not (behind), the way `git rev-list --left-right --count` | |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 15 | * says it. |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 16 | */ |
| 17 | export type Drift = { ahead: number; behind: number }; | |
| 18 | ||
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 19 | type Preview = { branch?: string | null; number?: number | null; url: string }; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 20 | |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 21 | type PullOn = Pick<Pull, "branch" | "number" | "title" | "checkStatus" | "status">; |
| 22 | ||
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 23 | /** A commit's people, as lib/commit-people.ts `showCommit` finds them. */ |
| 24 | export type People = (commit: Commit) => Pick<ShownCommit, "author" | "coAuthors">; | |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 25 | |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 26 | /** Nobody matched to an account: the name on the commit. */ |
| 27 | const byName: People = (commit) => ({ | |
| 28 | author: { kind: "author", name: commit.author.name, username: null, avatar: null }, | |
| 29 | coAuthors: [], | |
| 30 | }); | |
| 31 | ||
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 32 | /** Whether a pull request is still open: ready for review or a draft. */ |
| 33 | const isOpen = (pull: Pick<Pull, "status">) => pull.status === "open" || pull.status === "draft"; | |
| 34 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 35 | /** |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 36 | * A commit as a branch row shows it: its first line, and its people as |
| 37 | * `people` finds them (lib/branches.server.ts passes the accounts). | |
| 38 | */ | |
| 39 | export function summary(commit: Commit | null | undefined, people: People = byName): ActiveBranch["commit"] { | |
| 40 | if (!commit) return null; | |
| 41 | const shown = people(commit); | |
| 42 | return { hash: commit.hash, message: commit.message.split("\n")[0] ?? "", author: shown.author, coAuthors: shown.coAuthors, at: commit.authoredAt }; | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 46 | * The branches other than the default that are read (at most `read`, those |
| 47 | * with an open pull request first), how many there are, and the default | |
| 48 | * branch's head commit. | |
| What g1t can't do yet, and an open letter to Cloudflare | 49 | */ |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 50 | export function branchesToRead( |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 51 | input: { defaultBranch: string; branches: Branch[]; pulls: Pick<Pull, "branch" | "status">[] }, |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 52 | read: number, |
| 53 | ): { reading: Branch[]; total: number; mainHead: string | null } { | |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 54 | const pullOn = new Set(input.pulls.flatMap((pull) => (pull.branch && isOpen(pull) ? [pull.branch] : []))); |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 55 | const others = input.branches.filter((branch) => branch.name !== input.defaultBranch); |
| 56 | const reading = [...others.filter((b) => pullOn.has(b.name)), ...others.filter((b) => !pullOn.has(b.name))].slice(0, read); | |
| 57 | const mainHead = input.branches.find((branch) => branch.name === input.defaultBranch)?.hash || null; | |
| 58 | return { reading, total: others.length, mainHead }; | |
| What g1t can't do yet, and an open letter to Cloudflare | 59 | } |
| 60 | ||
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 61 | /** |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 62 | * Each branch's pull request: the open one, else the newest that was |
| 63 | * merged or closed. `pulls` come newest first, as `listPulls` answers. | |
| 64 | */ | |
| 65 | function pullsByBranch(pulls: PullOn[]): Map<string, PullOn> { | |
| 66 | const found = new Map<string, PullOn>(); | |
| 67 | for (const pull of pulls) { | |
| 68 | if (!pull.branch) continue; | |
| 69 | const kept = found.get(pull.branch); | |
| 70 | if (!kept || (!isOpen(kept) && isOpen(pull))) found.set(pull.branch, pull); | |
| 71 | } | |
| 72 | return found; | |
| 73 | } | |
| 74 | ||
| 75 | /** | |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 76 | * Each branch read, newest commit first, with what repos measured (null |
| 77 | * when that could not be had: no commits, no counts), its pull request and | |
| 78 | * its preview. | |
| 79 | */ | |
| 80 | export function activeBranches( | |
| 81 | reading: Branch[], | |
| 82 | measured: BranchDrifts | null, | |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 83 | input: { pulls: PullOn[]; previews: Preview[] }, |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 84 | people: People = byName, |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 85 | ): ActiveBranch[] { |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 86 | const pullOn = pullsByBranch(input.pulls); |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 87 | const byHead = new Map((measured?.branches ?? []).map((found) => [found.head, found])); |
| 88 | return reading | |
| 89 | .map((branch): ActiveBranch => { | |
| 90 | const pull = pullOn.get(branch.name); | |
| 91 | const found = byHead.get(branch.hash); | |
| 92 | return { | |
| 93 | name: branch.name, | |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 94 | head: branch.hash, |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 95 | commit: summary(found?.commit, people), |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 96 | drift: found?.drift ?? null, |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 97 | pull: pull ? { number: pull.number, title: pull.title, checkStatus: pull.checkStatus, draft: pull.status === "draft", status: pull.status } : null, |
| 98 | preview: input.previews.find((app) => app.branch === branch.name || (pull != null && isOpen(pull) && app.number === pull.number))?.url ?? null, | |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 99 | }; |
| 100 | }) | |
| 101 | .sort((a, b) => Date.parse(b.commit?.at ?? "0") - Date.parse(a.commit?.at ?? "0")); | |
| What g1t can't do yet, and an open letter to Cloudflare | 102 | } |
| Merge remote-tracking branch 'origin/main' into claude/cool-newton-7nm8r9 | 103 | |
| 104 | // --- The Branches page: its tabs and sections -------------------------------- | |
| 105 | ||
| 106 | /** The Branches page's tabs, kept in the address as `?tab=`. */ | |
| 107 | export const BRANCH_TABS = ["overview", "yours", "active", "stale", "all"] as const; | |
| 108 | export type BranchTab = (typeof BRANCH_TABS)[number]; | |
| 109 | ||
| 110 | /** A tab from the address: the overview for anything else. */ | |
| 111 | export function branchTab(value: string | null | undefined): BranchTab { | |
| 112 | return (BRANCH_TABS as readonly string[]).includes(value ?? "") ? (value as BranchTab) : "overview"; | |
| 113 | } | |
| 114 | ||
| 115 | /** A branch whose last commit is older than this is stale. */ | |
| 116 | export const STALE_DAYS = 90; | |
| 117 | ||
| 118 | /** Whether a branch's last commit is more than `STALE_DAYS` old. A branch whose commit was not read is not. */ | |
| 119 | export function isStale(branch: Pick<ActiveBranch, "commit">, now: number): boolean { | |
| 120 | const at = branch.commit ? Date.parse(branch.commit.at) : Number.NaN; | |
| 121 | return Number.isFinite(at) && at < now - STALE_DAYS * 86_400_000; | |
| 122 | } | |
| 123 | ||
| 124 | /** Whether `username` made a branch's latest commit. */ | |
| 125 | export function isYours(branch: Pick<ActiveBranch, "commit">, username: string | null | undefined): boolean { | |
| 126 | return Boolean(username) && branch.commit?.author.username?.toLowerCase() === username?.toLowerCase(); | |
| 127 | } | |
| 128 | ||
| 129 | /** | |
| 130 | * Whether deleting a branch loses nothing the default branch lacks: every | |
| 131 | * commit on it is on the default branch, or its pull request was merged. | |
| 132 | * Such a branch is deleted without asking. | |
| 133 | */ | |
| 134 | export function isMerged(branch: Pick<ActiveBranch, "drift" | "pull">): boolean { | |
| 135 | return branch.drift?.ahead === 0 || branch.pull?.status === "merged"; | |
| 136 | } | |
| 137 | ||
| 138 | export type BranchSection = { | |
| 139 | tab: Exclude<BranchTab, "overview">; | |
| 140 | title: string; | |
| 141 | /** Under the title, when it needs saying. */ | |
| 142 | note?: string; | |
| 143 | branches: ActiveBranch[]; | |
| 144 | /** How many more the section's own tab lists. */ | |
| 145 | more: number; | |
| 146 | }; | |
| 147 | ||
| 148 | /** How many branches each section of the overview shows. */ | |
| 149 | export const OVERVIEW_SHOWN = 5; | |
| 150 | ||
| 151 | /** | |
| 152 | * The sections a tab lists, besides the default branch: the overview | |
| 153 | * shows a few of the viewer's branches, the active ones and the stale ones; each other | |
| 154 | * tab lists its own. | |
| 155 | */ | |
| 156 | export function branchSections( | |
| 157 | branches: ActiveBranch[], | |
| 158 | tab: BranchTab, | |
| 159 | viewer: { username: string | null; now: number }, | |
| 160 | shown = OVERVIEW_SHOWN, | |
| 161 | ): BranchSection[] { | |
| 162 | const yours = branches.filter((branch) => isYours(branch, viewer.username)); | |
| 163 | const active = branches.filter((branch) => !isStale(branch, viewer.now)); | |
| 164 | const stale = branches.filter((branch) => isStale(branch, viewer.now)); | |
| 165 | const whole = (section: Omit<BranchSection, "more">): BranchSection => ({ ...section, more: 0 }); | |
| 166 | switch (tab) { | |
| 167 | case "overview": { | |
| 168 | const few = (section: Omit<BranchSection, "more">): BranchSection => ({ | |
| 169 | ...section, | |
| 170 | branches: section.branches.slice(0, shown), | |
| 171 | more: Math.max(0, section.branches.length - shown), | |
| 172 | }); | |
| 173 | return [ | |
| 174 | ...(yours.length > 0 ? [few({ tab: "yours", title: "Your branches", branches: yours })] : []), | |
| 175 | few({ tab: "active", title: "Active branches", branches: active }), | |
| 176 | ...(stale.length > 0 ? [few({ tab: "stale", title: "Stale branches", branches: stale })] : []), | |
| 177 | ]; | |
| 178 | } | |
| 179 | case "yours": | |
| 180 | return [whole({ tab, title: "Your branches", note: "Branches whose latest commit is yours.", branches: yours })]; | |
| 181 | case "active": | |
| 182 | return [whole({ tab, title: "Active branches", note: `Commits in the last ${STALE_DAYS} days.`, branches: active })]; | |
| 183 | case "stale": | |
| 184 | return [whole({ tab, title: "Stale branches", note: `No commits in the last ${STALE_DAYS} days.`, branches: stale })]; | |
| 185 | case "all": | |
| 186 | return [whole({ tab, title: "All branches", branches })]; | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | /** The longest behind or ahead count among `branches`, which a full bar stands for. */ | |
| 191 | export function driftScale(branches: Pick<ActiveBranch, "drift">[]): number { | |
| 192 | return Math.max(1, ...branches.map((branch) => Math.max(branch.drift?.ahead ?? 0, branch.drift?.behind ?? 0))); | |
| 193 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.