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.
| Usage lists Other once: kinds of work without words of their own are added into it | 1 | /** |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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 | |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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 | ||
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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. | |
| Usage lists Other once: kinds of work without words of their own are added into it | 49 | */ |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 50 | export function branchesToRead( |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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 } { | |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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 }; | |
| Usage lists Other once: kinds of work without words of their own are added into it | 59 | } |
| 60 | ||
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 61 | /** |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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, | |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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[] { |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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, | |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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, |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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")); | |
| Usage lists Other once: kinds of work without words of their own are added into it | 102 | } |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 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.