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.
| Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 4 | import type { BranchDrifts, Commit } from "@g1t/contracts"; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 5 | |
| 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. | 6 | import { activeBranches, branchSections, branchTab, branchesToRead, driftScale, isMerged, isStale, isYours } from "./branches.ts"; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 7 | |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 8 | const commit = (hash: string, at: string, message = `${hash}\n\nbody`): Commit => ({ |
| 9 | hash, | |
| 10 | treeHash: `t${hash}`, | |
| 11 | message, | |
| 12 | author: { name: "Ada", email: "ada@example.com" }, | |
| 13 | parents: [], | |
| 14 | authoredAt: at, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 15 | }); |
| 16 | ||
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 17 | const branches = [ |
| 18 | { name: "main", hash: "m3" }, | |
| 19 | { name: "old", hash: "o1" }, | |
| 20 | { name: "fix", hash: "f2" }, | |
| 21 | { name: "idea", hash: "i1" }, | |
| 22 | ]; | |
| 23 | const pulls = [{ branch: "fix", number: 7, title: "Fix it", checkStatus: "passed" as const, status: "open" as const }]; | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 24 | |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 25 | test("branches with an open pull request are read first, the default branch never", () => { |
| 26 | const read = branchesToRead({ defaultBranch: "main", branches, pulls }, 2); | |
| 27 | assert.deepEqual(read.reading.map((b) => b.name), ["fix", "old"]); | |
| 28 | assert.equal(read.total, 3); | |
| 29 | assert.equal(read.mainHead, "m3"); | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 30 | }); |
| 31 | ||
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 32 | test("no default branch head, nothing to measure against", () => { |
| 33 | assert.equal(branchesToRead({ defaultBranch: "trunk", branches, pulls: [] }, 10).mainHead, null); | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 34 | }); |
| 35 | ||
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 36 | test("each branch gets its measured commit and drift, newest first, by head hash", () => { |
| 37 | const measured: BranchDrifts = { | |
| 38 | base: commit("m3", "2026-10-08T00:00:00Z"), | |
| 39 | branches: [ | |
| 40 | { head: "f2", commit: commit("f2", "2026-10-07T00:00:00Z"), drift: { ahead: 2, behind: 1 } }, | |
| 41 | { head: "o1", commit: commit("o1", "2026-01-01T00:00:00Z"), drift: null }, | |
| 42 | { head: "i1", commit: commit("i1", "2026-10-08T00:00:00Z", "one line"), drift: { ahead: 1, behind: 0 } }, | |
| 43 | ], | |
| 44 | }; | |
| 45 | const { reading } = branchesToRead({ defaultBranch: "main", branches, pulls }, 10); | |
| 46 | const shown = activeBranches(reading, measured, { pulls, previews: [{ number: 7, url: "https://fix.g1t.page" }] }); | |
| 47 | assert.deepEqual(shown.map((b) => b.name), ["idea", "fix", "old"]); | |
| 48 | assert.deepEqual(shown[1], { | |
| 49 | name: "fix", | |
| 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. | 50 | head: "f2", |
| Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016) | 51 | // No accounts given: the name on the commit, and never its address. |
| 52 | commit: { hash: "f2", message: "f2", author: { kind: "author", name: "Ada", username: null, avatar: null }, coAuthors: [], at: "2026-10-07T00:00:00Z" }, | |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 53 | drift: { ahead: 2, behind: 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. | 54 | pull: { number: 7, title: "Fix it", checkStatus: "passed", draft: false, status: "open" }, |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 55 | preview: "https://fix.g1t.page", |
| 56 | }); | |
| 57 | assert.equal(shown[2]?.drift, null); | |
| Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index | 58 | }); |
| 59 | ||
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 60 | test("when repos could not answer, the branches still show, without commits or counts", () => { |
| 61 | const { reading } = branchesToRead({ defaultBranch: "main", branches, pulls: [] }, 10); | |
| 62 | const shown = activeBranches(reading, null, { pulls: [], previews: [] }); | |
| 63 | assert.equal(shown.length, 3); | |
| 64 | assert.ok(shown.every((b) => b.commit == null && b.drift == null && b.pull == null)); | |
| Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index | 65 | }); |
| Merge branch drift: count across merges the way git does; v2 cache key | 66 | |
| 67 | test("repos' answer, as its JSON reads, keeps every count (flagon-io/hello's farewell)", () => { | |
| 68 | // What services/repos serializes for `branch_drift` (drift.rs | |
| 69 | // `the_answer_has_the_names_the_site_reads`): a name that differs here | |
| 70 | // would drop every count without an error. | |
| 71 | const wire = JSON.stringify({ | |
| 72 | base: { | |
| 73 | hash: "8407eba", | |
| 74 | treeHash: "t1", | |
| 75 | message: "Use farewell() in the --both call", | |
| 76 | author: { name: "g1t agent", email: "agent@example.com" }, | |
| 77 | parents: ["ac45e10"], | |
| 78 | authoredAt: "2026-10-03T09:11:34.000Z", | |
| 79 | }, | |
| 80 | branches: [ | |
| 81 | { | |
| 82 | head: "bab14ff", | |
| 83 | commit: { | |
| 84 | hash: "bab14ff", | |
| 85 | treeHash: "t2", | |
| 86 | message: "Add a farewell\n\nbody", | |
| 87 | author: { name: "syntaqx", email: "s@example.com" }, | |
| 88 | parents: ["c2ef68d"], | |
| 89 | authoredAt: "2026-10-02T07:52:16.000Z", | |
| 90 | }, | |
| 91 | drift: { ahead: 1, behind: 41 }, | |
| 92 | }, | |
| 93 | ], | |
| 94 | }); | |
| 95 | const measured = JSON.parse(wire) as BranchDrifts; | |
| 96 | const shown = activeBranches([{ name: "farewell", hash: "bab14ff" }], measured, { pulls: [], previews: [] }); | |
| 97 | assert.deepEqual(shown[0]?.drift, { ahead: 1, behind: 41 }); | |
| Branches test: a commit's author is a person since commits show their accounts | 98 | assert.deepEqual(shown[0]?.commit, { |
| 99 | hash: "bab14ff", | |
| 100 | message: "Add a farewell", | |
| 101 | author: { kind: "author", name: "syntaqx", username: null, avatar: null }, | |
| 102 | coAuthors: [], | |
| 103 | at: "2026-10-02T07:52:16.000Z", | |
| 104 | }); | |
| Merge branch drift: count across merges the way git does; v2 cache key | 105 | }); |
| 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. | 106 | |
| 107 | test("a branch's pull request is the open one, else the newest merged or closed", () => { | |
| 108 | const reading = [ | |
| 109 | { name: "fix", hash: "f2" }, | |
| 110 | { name: "done", hash: "d1" }, | |
| 111 | ]; | |
| 112 | const newestFirst = [ | |
| 113 | { branch: "done", number: 9, title: "Second try", checkStatus: null, status: "merged" as const }, | |
| 114 | { branch: "fix", number: 8, title: "Closed", checkStatus: null, status: "closed" as const }, | |
| 115 | { branch: "done", number: 5, title: "First try", checkStatus: null, status: "closed" as const }, | |
| 116 | { branch: "fix", number: 7, title: "Fix it", checkStatus: "passed" as const, status: "open" as const }, | |
| 117 | ]; | |
| 118 | const shown = activeBranches(reading, null, { pulls: newestFirst, previews: [] }); | |
| 119 | assert.deepEqual( | |
| 120 | shown.map((b) => [b.name, b.pull?.number, b.pull?.status]), | |
| 121 | [ | |
| 122 | ["fix", 7, "open"], | |
| 123 | ["done", 9, "merged"], | |
| 124 | ], | |
| 125 | ); | |
| 126 | // Only an open pull request puts a branch first in line to be read. | |
| 127 | const all = [ | |
| 128 | { name: "main", hash: "m" }, | |
| 129 | { name: "a", hash: "a" }, | |
| 130 | { name: "done", hash: "d1" }, | |
| 131 | ]; | |
| 132 | assert.deepEqual(branchesToRead({ defaultBranch: "main", branches: all, pulls: newestFirst }, 1).reading.map((b) => b.name), ["a"]); | |
| 133 | }); | |
| 134 | ||
| 135 | const now = Date.parse("2026-10-11T00:00:00Z"); | |
| 136 | const row = (name: string, at: string | null, username: string | null = null, drift: { ahead: number; behind: number } | null = null) => ({ | |
| 137 | name, | |
| 138 | head: name, | |
| 139 | commit: at ? { hash: name, message: name, author: { kind: "user" as const, name, username, avatar: null }, coAuthors: [], at } : null, | |
| 140 | drift, | |
| 141 | pull: null, | |
| 142 | preview: null, | |
| 143 | }); | |
| 144 | ||
| 145 | test("a tab comes from the address, the overview otherwise", () => { | |
| 146 | assert.equal(branchTab("stale"), "stale"); | |
| 147 | assert.equal(branchTab("STALE"), "overview"); | |
| 148 | assert.equal(branchTab(null), "overview"); | |
| 149 | }); | |
| 150 | ||
| 151 | test("stale is no commits in 90 days; yours is the viewer's latest commit", () => { | |
| 152 | assert.equal(isStale(row("old", "2026-07-01T00:00:00Z"), now), true); | |
| 153 | assert.equal(isStale(row("new", "2026-09-01T00:00:00Z"), now), false); | |
| 154 | assert.equal(isStale(row("unread", null), now), false); | |
| 155 | assert.equal(isYours(row("mine", "2026-10-01T00:00:00Z", "Ada"), "ada"), true); | |
| 156 | assert.equal(isYours(row("theirs", "2026-10-01T00:00:00Z", "grace"), "ada"), false); | |
| 157 | assert.equal(isYours(row("nobody", "2026-10-01T00:00:00Z", null), null), false); | |
| 158 | }); | |
| 159 | ||
| 160 | test("a branch with nothing the default branch lacks, or a merged pull request, is merged", () => { | |
| 161 | assert.equal(isMerged({ drift: { ahead: 0, behind: 3 }, pull: null }), true); | |
| 162 | assert.equal(isMerged({ drift: { ahead: 2, behind: 0 }, pull: null }), false); | |
| 163 | assert.equal(isMerged({ drift: null, pull: null }), false); | |
| 164 | assert.equal(isMerged({ drift: { ahead: 2, behind: 0 }, pull: { number: 1, title: "", checkStatus: null, draft: false, status: "merged" } }), true); | |
| 165 | }); | |
| 166 | ||
| 167 | test("the overview shows a few of yours, the active and the stale ones; each tab lists its own", () => { | |
| 168 | const rows = [ | |
| 169 | row("a", "2026-10-10T00:00:00Z", "ada"), | |
| 170 | row("b", "2026-10-09T00:00:00Z", "grace"), | |
| 171 | row("c", "2026-10-08T00:00:00Z", "ada"), | |
| 172 | row("d", "2026-01-01T00:00:00Z", "ada"), | |
| 173 | ]; | |
| 174 | const viewer = { username: "ada", now }; | |
| 175 | assert.deepEqual( | |
| 176 | branchSections(rows, "overview", viewer, 2).map((s) => [s.tab, s.branches.map((b) => b.name), s.more]), | |
| 177 | [ | |
| 178 | ["yours", ["a", "c"], 1], | |
| 179 | ["active", ["a", "b"], 1], | |
| 180 | ["stale", ["d"], 0], | |
| 181 | ], | |
| 182 | ); | |
| 183 | // Someone with no branches of their own sees no section of theirs. | |
| 184 | assert.deepEqual(branchSections(rows, "overview", { username: "linus", now }).map((s) => s.tab), ["active", "stale"]); | |
| 185 | assert.deepEqual(branchSections(rows, "stale", viewer)[0]?.branches.map((b) => b.name), ["d"]); | |
| 186 | assert.deepEqual(branchSections(rows, "active", viewer)[0]?.branches.map((b) => b.name), ["a", "b", "c"]); | |
| 187 | assert.deepEqual(branchSections(rows, "yours", viewer)[0]?.branches.map((b) => b.name), ["a", "c", "d"]); | |
| 188 | assert.equal(branchSections(rows, "all", viewer)[0]?.branches.length, 4); | |
| 189 | }); | |
| 190 | ||
| 191 | test("the bars are drawn against the longest count shown", () => { | |
| 192 | assert.equal(driftScale([row("a", null, null, { ahead: 3, behind: 40 }), row("b", null, null, { ahead: 12, behind: 0 })]), 40); | |
| 193 | assert.equal(driftScale([row("a", null)]), 1); | |
| 194 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.