Public: anyone can read this; the crew can change it.
GET a repository's branches answers `{ items, next_cursor }`, and every test that reads it is updated #5085
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5060-get-a-repository-s-branches-answers-item → main 4 commits fork 3 threads · 3 unresolved
14 files
changed +116
−86
README.md +1 −1
| @@ -208,7 +208,7 @@ runs no check and no preview until a writer approves its head or lets it run | ||
| 208 | 208 | (**Let it run here**, which is not a review; a new push waits again), and a bare push to `child/…` starts none. A pull |
| 209 | 209 | opened with it says "proposed by child … from …" on every tab of the pull's |
| 210 | 210 | page, in the terminal's pull view, and as `proposal` in the pull's JSON |
| 211 | (`GET …/pulls/:number`, and MCP's `pull` tool). `GET …/pulls/:number?diff=0` reads the pull without its diff (and skips computing it); `diff` is `0` or `1`, default `1`, and anything else answers 422. `GET …/issues?fields=brief` lists issues with only number, kind, title, state, labels, assignee, author, originAuthor, authorIsAgent, createdAt and updatedAt (and `untrusted` where it applies); no bodies or comments. `fields` is `brief` or left out, and anything else answers 422 `invalid_fields`. `GET …/repos/:owner/:name/events` answers `{ items, next_cursor }`, newest first, `limit` 1 to 200 (default 50) and `cursor` from `next_cursor` (anything else is a 422); it used to answer a bare array of every event, so a client that read the array reads `items` now. A request reads at most 1,000 events, so when most of them are hidden from the reader a page can come back short with a `next_cursor`: keep following it until it is null. MCP's `events` tool answers the same. | |
| 211 | (`GET …/pulls/:number`, and MCP's `pull` tool). `GET …/pulls/:number?diff=0` reads the pull without its diff (and skips computing it); `diff` is `0` or `1`, default `1`, and anything else answers 422. `GET …/issues?fields=brief` lists issues with only number, kind, title, state, labels, assignee, author, originAuthor, authorIsAgent, createdAt and updatedAt (and `untrusted` where it applies); no bodies or comments. `fields` is `brief` or left out, and anything else answers 422 `invalid_fields`. `GET …/repos/:owner/:name/events` answers `{ items, next_cursor }`, newest first, `limit` 1 to 200 (default 50) and `cursor` from `next_cursor` (anything else is a 422); it used to answer a bare array of every event, so a client that read the array reads `items` now. A request reads at most 1,000 events, so when most of them are hidden from the reader a page can come back short with a `next_cursor`: keep following it until it is null. MCP's `events` tool answers the same. `GET …/repos/:owner/:name/branches` answers `{ items, next_cursor }` in the order it always had, `limit` 1 to 200 (default 50) and `cursor` the `next_cursor` of the page before (the name of the last branch on it); anything else is a 422, and a cursor naming a branch deleted between two requests is `invalid_cursor`, so start over. It used to answer a bare array, so a client that read the array reads `items` now. | |
| 212 | 212 | |
| 213 | 213 | ### Proposing to a parent forge |
| 214 | 214 |
src/http/api.ts +19 −6
| @@ -2834,21 +2834,34 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 2834 | 2834 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); |
| 2835 | 2835 | const repo = loadRepo(ctx); |
| 2836 | 2836 | if (!repo) return notFound(); |
| 2837 | const q = ctx.url.searchParams; | |
| 2838 | const limit = eventsLimit(q.get("limit") ?? undefined); | |
| 2839 | if (limit === undefined) | |
| 2840 | return apiError("invalid_limit", 422, "limit is a whole number from 1 to 200"); | |
| 2841 | const cursor = q.get("cursor"); | |
| 2842 | if (cursor !== null && (cursor === "" || cursor.length > 255)) | |
| 2843 | return apiError("invalid_cursor", 422, "cursor is the next_cursor of a page before"); | |
| 2837 | 2844 | const listed = await timePhaseAsync(ctx.request, "git", () => |
| 2838 | 2845 | listBranches(dirOf(ctx, repo), repo.defaultBranch), |
| 2839 | 2846 | ); |
| 2840 | if (!listed.ok) return timePhase(ctx.request, "render", () => json([])); | |
| 2841 | return timePhase(ctx.request, "render", () => | |
| 2842 | json( | |
| 2843 | listed.value.branches.map((b) => ({ | |
| 2847 | const rows = listed.ok | |
| 2848 | ? listed.value.branches.map((b) => ({ | |
| 2844 | 2849 | name: b.name, |
| 2845 | 2850 | oid: b.oid, |
| 2846 | 2851 | isDefault: b.isDefault, |
| 2847 | 2852 | ahead: b.ahead, |
| 2848 | 2853 | behind: b.behind, |
| 2849 | 2854 | protected: b.isDefault && repo.protectDefault, |
| 2850 | })), | |
| 2851 | ), | |
| 2855 | })) | |
| 2856 | : []; | |
| 2857 | // The cursor is the name of the last branch of the page before. | |
| 2858 | const start = cursor === null ? 0 : rows.findIndex((r) => r.name === cursor) + 1; | |
| 2859 | if (start === 0 && cursor !== null) | |
| 2860 | return apiError("invalid_cursor", 422, "cursor is the next_cursor of a page before"); | |
| 2861 | const items = rows.slice(start, start + limit); | |
| 2862 | const more = start + limit < rows.length; | |
| 2863 | return timePhase(ctx.request, "render", () => | |
| 2864 | json({ items, next_cursor: more ? items[items.length - 1]!.name : null }), | |
| 2852 | 2865 | ); |
| 2853 | 2866 | }); |
| 2854 | 2867 |
test/api-branches-timing.test.ts +1 −1
| @@ -57,7 +57,7 @@ test("the branches list names session, git, render in order", async () => { | ||
| 57 | 57 | const got = await phases("/api/v1/repos/alice/r/branches"); |
| 58 | 58 | expect(got.status).toBe(200); |
| 59 | 59 | expect(got.names).toEqual(["session", "git", "render"]); |
| 60 | expect(Array.isArray(JSON.parse(got.body))).toBe(true); | |
| 60 | expect(Array.isArray(JSON.parse(got.body).items)).toBe(true); | |
| 61 | 61 | }); |
| 62 | 62 | |
| 63 | 63 | test("a missing repository is a 404 naming session", async () => { |
test/api-list-shape.test.ts −1
| @@ -23,7 +23,6 @@ const BARE: Record<string, string> = { | ||
| 23 | 23 | "/api/v1/repos/:owner/:name/forks": "#4843", |
| 24 | 24 | "/api/v1/repos/:owner/:name/labels": "#4843", |
| 25 | 25 | "/api/v1/repos/:owner/:name/hooks": "#4843", |
| 26 | "/api/v1/repos/:owner/:name/branches": "#5060", | |
| 27 | 26 | "/api/v1/repos/:owner/:name/tags": "#5061", |
| 28 | 27 | "/api/v1/repos/:owner/:name/commits": "#5083", |
| 29 | 28 | "/api/v1/user/origins": "#4841", |
test/branches.test.ts +40 −5
| @@ -120,7 +120,9 @@ describe("branches on a native repo", () => { | ||
| 120 | 120 | }); |
| 121 | 121 | |
| 122 | 122 | const branches = async (): Promise<BranchRow[]> => |
| 123 | read<BranchRow[]>(await api("/api/v1/repos/alice/app/branches")); | |
| 123 | ( | |
| 124 | await read<{ items: BranchRow[] }>(await api("/api/v1/repos/alice/app/branches")) | |
| 125 | ).items; | |
| 124 | 126 | |
| 125 | 127 | const create = (payload: unknown) => |
| 126 | 128 | api("/api/v1/repos/alice/app/branches", { |
| @@ -207,6 +209,39 @@ describe("branches on a native repo", () => { | ||
| 207 | 209 | expect(rows[0]!.isDefault).toBe(true); |
| 208 | 210 | }); |
| 209 | 211 | |
| 212 | test("the list is { items, next_cursor }, pages with limit to the end, and refuses bad input", async () => { | |
| 213 | const all = await branches(); | |
| 214 | expect(all.length).toBeGreaterThanOrEqual(2); | |
| 215 | const whole = await read<{ items: unknown[]; next_cursor: string | null }>( | |
| 216 | await api("/api/v1/repos/alice/app/branches"), | |
| 217 | ); | |
| 218 | expect(Object.keys(whole).sort()).toEqual(["items", "next_cursor"]); | |
| 219 | expect(whole.next_cursor).toBeNull(); | |
| 220 | ||
| 221 | const seen: string[] = []; | |
| 222 | let cursor: string | null = null; | |
| 223 | for (let i = 0; i < all.length + 1; i++) { | |
| 224 | const q: string = `limit=1${cursor === null ? "" : `&cursor=${encodeURIComponent(cursor)}`}`; | |
| 225 | const page = await read<{ items: BranchRow[]; next_cursor: string | null }>( | |
| 226 | await api(`/api/v1/repos/alice/app/branches?${q}`), | |
| 227 | ); | |
| 228 | expect(page.items.length).toBe(1); | |
| 229 | seen.push(page.items[0]!.name); | |
| 230 | cursor = page.next_cursor; | |
| 231 | if (cursor === null) break; | |
| 232 | } | |
| 233 | expect(seen).toEqual(all.map((b) => b.name)); | |
| 234 | ||
| 235 | for (const bad of ["0", "201", "x", "-1", "1.5"]) { | |
| 236 | const r = await api(`/api/v1/repos/alice/app/branches?limit=${bad}`); | |
| 237 | expect(r.status).toBe(422); | |
| 238 | expect((await read<{ error: string }>(r)).error).toBe("invalid_limit"); | |
| 239 | } | |
| 240 | const r = await api("/api/v1/repos/alice/app/branches?cursor=no-such-branch"); | |
| 241 | expect(r.status).toBe(422); | |
| 242 | expect((await read<{ error: string }>(r)).error).toBe("invalid_cursor"); | |
| 243 | }); | |
| 244 | ||
| 210 | 245 | test("protection is reported per branch, on the default only", async () => { |
| 211 | 246 | await api( |
| 212 | 247 | "/api/v1/repos/alice/app", |
| @@ -384,12 +419,12 @@ describe("branches on a mirror go through the origin", () => { | ||
| 384 | 419 | }); |
| 385 | 420 | |
| 386 | 421 | const localBranches = async (): Promise<BranchRow[]> => |
| 387 | read<BranchRow[]>(await api("/api/v1/repos/alice/gears/branches")); | |
| 422 | ( | |
| 423 | await read<{ items: BranchRow[] }>(await api("/api/v1/repos/alice/gears/branches")) | |
| 424 | ).items; | |
| 388 | 425 | |
| 389 | 426 | const upstreamBranches = async (): Promise<{ name: string }[]> => |
| 390 | read<{ name: string }[]>( | |
| 391 | await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`), | |
| 392 | ); | |
| 427 | (await read<{ items: { name: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`))).items; | |
| 393 | 428 | |
| 394 | 429 | test("creating a branch lands it upstream, then here", async () => { |
| 395 | 430 | const created = await api("/api/v1/repos/alice/gears/branches", { |
test/edits-security.test.ts +12 −6
| @@ -363,9 +363,11 @@ describe("edits: refusals that must leave nothing behind", () => { | ||
| 363 | 363 | expect(b.ahead).toBeNull(); |
| 364 | 364 | expect(b.behind).toBeNull(); |
| 365 | 365 | } |
| 366 | const rows = await read<{ ahead: number | null }[]>( | |
| 367 | await api("/api/v1/repos/alice/app/branches"), | |
| 368 | ); | |
| 366 | const rows = ( | |
| 367 | await read<{ items: { ahead: number | null }[] }>( | |
| 368 | await api("/api/v1/repos/alice/app/branches"), | |
| 369 | ) | |
| 370 | ).items; | |
| 369 | 371 | expect(rows.every((b) => b.ahead === null)).toBe(true); |
| 370 | 372 | }); |
| 371 | 373 | }); |
| @@ -411,9 +413,13 @@ describe("edits on a mirror: the origin's refusal is final", () => { | ||
| 411 | 413 | .split("\n") |
| 412 | 414 | .filter((r) => r !== ""); |
| 413 | 415 | const upstreamBranches = async (): Promise<string[]> => |
| 414 | read<{ name: string }[]>( | |
| 415 | await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`), | |
| 416 | ).then((rows) => rows.map((b) => b.name).sort()); | |
| 416 | ( | |
| 417 | await read<{ items: { name: string }[] }>( | |
| 418 | await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`), | |
| 419 | ) | |
| 420 | ).items | |
| 421 | .map((b) => b.name) | |
| 422 | .sort(); | |
| 417 | 423 | |
| 418 | 424 | beforeAll(async () => { |
| 419 | 425 | dataDir = await mkdtemp(join(tmpdir(), "forge-mr-")); |
test/edits.test.ts +4 −8
| @@ -209,10 +209,10 @@ describe("edits: files, branches and a pull in one call", () => { | ||
| 209 | 209 | expect(edited.pull).toMatchObject({ title: "Add new.txt" }); |
| 210 | 210 | |
| 211 | 211 | // The branch exists, carries the file, and main does not. |
| 212 | const branches = await read<{ name: string }[]>( | |
| 212 | const branches = await read<{ items: { name: string }[] }>( | |
| 213 | 213 | await api("/api/v1/repos/alice/app/branches"), |
| 214 | 214 | ); |
| 215 | expect(branches.map((b) => b.name)).toContain("feat/edit"); | |
| 215 | expect(branches.items.map((b) => b.name)).toContain("feat/edit"); | |
| 216 | 216 | expect((await fileAt("alice/app", "new.txt", "feat/edit")).status).toBe( |
| 217 | 217 | 200, |
| 218 | 218 | ); |
| @@ -454,9 +454,7 @@ describe("edits on a mirror land upstream first", () => { | ||
| 454 | 454 | expect(response.status).toBe(201); |
| 455 | 455 | const edited = await read<EditResponse>(response); |
| 456 | 456 | |
| 457 | const upstreamMain = await read<{ name: string; oid: string }[]>( | |
| 458 | await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`), | |
| 459 | ); | |
| 457 | const upstreamMain = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`))).items; | |
| 460 | 458 | expect(upstreamMain.find((b) => b.name === "main")?.oid).toBe( |
| 461 | 459 | edited.commit.commitOid, |
| 462 | 460 | ); |
| @@ -482,9 +480,7 @@ describe("edits on a mirror land upstream first", () => { | ||
| 482 | 480 | expect(response.status).toBe(201); |
| 483 | 481 | const edited = await read<EditResponse>(response); |
| 484 | 482 | |
| 485 | const branches = await read<{ name: string; oid: string }[]>( | |
| 486 | await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`), | |
| 487 | ); | |
| 483 | const branches = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`))).items; | |
| 488 | 484 | expect(branches.find((b) => b.name === "feat/mirror")?.oid).toBe( |
| 489 | 485 | edited.commit.commitOid, |
| 490 | 486 | ); |
test/fork-flow.test.ts +8 −24
| @@ -344,11 +344,7 @@ describe("the fork flow", () => { | ||
| 344 | 344 | expect(result.pull?.local).toBeNull(); |
| 345 | 345 | |
| 346 | 346 | // The branch really landed on the fork's git, at the commit we made. |
| 347 | const branches = await read<{ name: string; oid: string }[]>( | |
| 348 | await fetch( | |
| 349 | `${originServers.url}/api/v1/repos/${ORIGIN_LOGIN}/gears/branches`, | |
| 350 | ), | |
| 351 | ); | |
| 347 | const branches = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/${ORIGIN_LOGIN}/gears/branches`))).items; | |
| 352 | 348 | const landed = branches.find((b) => b.name === "fix-typo"); |
| 353 | 349 | expect(landed?.oid).toBe(result.commit.commitOid); |
| 354 | 350 | |
| @@ -437,11 +433,7 @@ describe("the fork flow", () => { | ||
| 437 | 433 | expect(after).toBe(before); |
| 438 | 434 | |
| 439 | 435 | // The branch is on the upstream's own git. |
| 440 | const branches = await read<{ name: string; oid: string }[]>( | |
| 441 | await fetch( | |
| 442 | `${originServers.url}/api/v1/repos/${ORIGIN_LOGIN}/own/branches`, | |
| 443 | ), | |
| 444 | ); | |
| 436 | const branches = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/${ORIGIN_LOGIN}/own/branches`))).items; | |
| 445 | 437 | expect(branches.find((b) => b.name === "direct")?.oid).toBe( |
| 446 | 438 | result.commit.commitOid, |
| 447 | 439 | ); |
| @@ -482,11 +474,7 @@ describe("the fork flow", () => { | ||
| 482 | 474 | }>(response); |
| 483 | 475 | expect(result.via).toBe("fork"); |
| 484 | 476 | |
| 485 | const branches = await read<{ name: string; oid: string }[]>( | |
| 486 | await fetch( | |
| 487 | `${originServers.url}/api/v1/repos/${ORIGIN_LOGIN}/widgets/branches`, | |
| 488 | ), | |
| 489 | ); | |
| 477 | const branches = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/${ORIGIN_LOGIN}/widgets/branches`))).items; | |
| 490 | 478 | expect(branches.find((b) => b.name === "gh-fix")?.oid).toBe( |
| 491 | 479 | result.commit.commitOid, |
| 492 | 480 | ); |
| @@ -745,11 +733,7 @@ describe("the fork flow", () => { | ||
| 745 | 733 | const pull = await read<{ number: number }>(proposed); |
| 746 | 734 | |
| 747 | 735 | // The branch is on the upstream's own git, at bob's commit… |
| 748 | const branches = await read<{ name: string; oid: string }[]>( | |
| 749 | await fetch( | |
| 750 | `${originServers.url}/api/v1/repos/${ORIGIN_LOGIN}/own/branches`, | |
| 751 | ), | |
| 752 | ); | |
| 736 | const branches = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/${ORIGIN_LOGIN}/own/branches`))).items; | |
| 753 | 737 | expect(branches.find((b) => b.name === "refined")?.oid).toBe(commitOid); |
| 754 | 738 | // …the pull was opened there from it, as a bare branch… |
| 755 | 739 | const opened = forgejo.state.requests.filter( |
| @@ -760,10 +744,10 @@ describe("the fork flow", () => { | ||
| 760 | 744 | expect(sent.head).toBe("refined"); |
| 761 | 745 | expect(sent.base).toBe("main"); |
| 762 | 746 | // …and the mirror shows both the branch and the pull. |
| 763 | const local = await read<{ name: string; oid: string }[]>( | |
| 747 | const local = await read<{ items: { name: string; oid: string }[] }>( | |
| 764 | 748 | await api(`/api/v1/repos/${mirror.ownerName}/${mirror.name}/branches`), |
| 765 | 749 | ); |
| 766 | expect(local.find((b) => b.name === "refined")?.oid).toBe(commitOid); | |
| 750 | expect(local.items.find((b) => b.name === "refined")?.oid).toBe(commitOid); | |
| 767 | 751 | const listed = await read<{ items: { number: number }[] }>( |
| 768 | 752 | await api(`/api/v1/repos/${mirror.ownerName}/${mirror.name}/pulls`), |
| 769 | 753 | ); |
| @@ -814,10 +798,10 @@ describe("the fork flow", () => { | ||
| 814 | 798 | expect(seeded).toBe(1); |
| 815 | 799 | expect(job?.stage).toBe("done"); |
| 816 | 800 | expect(job?.repo?.name).toBe("late"); |
| 817 | const branches = await read<{ name: string }[]>( | |
| 801 | const branches = await read<{ items: { name: string }[] }>( | |
| 818 | 802 | await api(`/api/v1/repos/${job!.repo!.ownerName}/late/branches`), |
| 819 | 803 | ); |
| 820 | expect(branches.map((b) => b.name)).toContain("main"); | |
| 804 | expect(branches.items.map((b) => b.name)).toContain("main"); | |
| 821 | 805 | }, 60_000); |
| 822 | 806 | |
| 823 | 807 | test("a native repo has no upstream to contribute to", async () => { |
test/origins-security-push.test.ts +2 −2
| @@ -361,10 +361,10 @@ describe("push-through, adversarially", () => { | ||
| 361 | 361 | }; |
| 362 | 362 | |
| 363 | 363 | const localMain = async (): Promise<string> => { |
| 364 | const branches = await read<{ name: string; oid: string }[]>( | |
| 364 | const branches = await read<{ items: { name: string; oid: string }[] }>( | |
| 365 | 365 | await api("/api/v1/repos/alice/gears/branches"), |
| 366 | 366 | ); |
| 367 | return branches.find((b) => b.name === "main")?.oid ?? ""; | |
| 367 | return branches.items.find((b) => b.name === "main")?.oid ?? ""; | |
| 368 | 368 | }; |
| 369 | 369 | const originRef = async (ref: string): Promise<string> => |
| 370 | 370 | (await git(["rev-parse", "--verify", "--quiet", ref], origin.dir)).out; |
test/origins.test.ts +10 −6
| @@ -403,9 +403,11 @@ describe("origins: connecting, mirroring, writing through", () => { | ||
| 403 | 403 | expect(repo.pinned).toBe(false); |
| 404 | 404 | |
| 405 | 405 | // The git objects really arrived. |
| 406 | const branches = (await ( | |
| 407 | await api("/api/v1/repos/alice/widgets/branches") | |
| 408 | ).json()) as { name: string }[]; | |
| 406 | const branches = ( | |
| 407 | (await (await api("/api/v1/repos/alice/widgets/branches")).json()) as { | |
| 408 | items: { name: string }[]; | |
| 409 | } | |
| 410 | ).items; | |
| 409 | 411 | expect(branches.map((b) => b.name)).toEqual(["main"]); |
| 410 | 412 | const readme = (await ( |
| 411 | 413 | await api("/api/v1/repos/alice/widgets/contents/README.md") |
| @@ -581,9 +583,11 @@ describe("origins: connecting, mirroring, writing through", () => { | ||
| 581 | 583 | }); |
| 582 | 584 | if (!committed.ok) throw new Error(JSON.stringify(committed.error)); |
| 583 | 585 | const branchesOf = async (): Promise<Map<string, string>> => { |
| 584 | const listed = (await ( | |
| 585 | await fetch(`${originServers.url}/api/v1/repos/octo/widgets/branches`) | |
| 586 | ).json()) as { name: string; oid: string }[]; | |
| 586 | const listed = ( | |
| 587 | (await ( | |
| 588 | await fetch(`${originServers.url}/api/v1/repos/octo/widgets/branches`) | |
| 589 | ).json()) as { items: { name: string; oid: string }[] } | |
| 590 | ).items; | |
| 587 | 591 | return new Map(listed.map((b) => [b.name, b.oid])); |
| 588 | 592 | }; |
| 589 | 593 | const before = await branchesOf(); |
test/parity.test.ts +1 −1
| @@ -342,7 +342,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 342 | 342 | const dir = join(app.reposRoot, "alice", "pub.git"); |
| 343 | 343 | expect(await git(["symbolic-ref", "HEAD"], dir)).toBe("refs/heads/feature"); |
| 344 | 344 | const branches = await call("GET", "/api/v1/repos/alice/pub/branches"); |
| 345 | expect(branches.list.find((b) => b.isDefault)?.name).toBe("feature"); | |
| 345 | expect((branches.json.items as Json[]).find((b) => b.isDefault)?.name).toBe("feature"); | |
| 346 | 346 | const back = await call("PATCH", "/api/v1/repos/alice/pub", { |
| 347 | 347 | token: aliceAdmin, |
| 348 | 348 | body: { defaultBranch: "main" }, |
test/push-through.test.ts +8 −16
| @@ -218,16 +218,14 @@ describe("push-through: the origin gets the last word", () => { | ||
| 218 | 218 | expect(pushed.code).toBe(0); |
| 219 | 219 | |
| 220 | 220 | // The origin's ref actually moved — this is the whole feature. |
| 221 | const upstreamBranches = await read<{ name: string; oid: string }[]>( | |
| 222 | await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`), | |
| 223 | ); | |
| 221 | const upstreamBranches = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`))).items; | |
| 224 | 222 | expect(upstreamBranches.find((b) => b.name === "main")?.oid).toBe(local); |
| 225 | 223 | |
| 226 | 224 | // And the local ref moved too, so the mirror is not behind its origin. |
| 227 | const localBranches = await read<{ name: string; oid: string }[]>( | |
| 225 | const localBranches = await read<{ items: { name: string; oid: string }[] }>( | |
| 228 | 226 | await api("/api/v1/repos/alice/gears/branches"), |
| 229 | 227 | ); |
| 230 | expect(localBranches.find((b) => b.name === "main")?.oid).toBe(local); | |
| 228 | expect(localBranches.items.find((b) => b.name === "main")?.oid).toBe(local); | |
| 231 | 229 | }); |
| 232 | 230 | |
| 233 | 231 | // ---- the refusal path ---- |
| @@ -263,13 +261,11 @@ describe("push-through: the origin gets the last word", () => { | ||
| 263 | 261 | |
| 264 | 262 | // Nothing moved on either side: the local ref is still the old commit, |
| 265 | 263 | // which is exactly what "the origin is truth" has to mean. |
| 266 | const localBranches = await read<{ name: string; oid: string }[]>( | |
| 264 | const localBranches = await read<{ items: { name: string; oid: string }[] }>( | |
| 267 | 265 | await api("/api/v1/repos/alice/gears/branches"), |
| 268 | 266 | ); |
| 269 | expect(localBranches.find((b) => b.name === "main")?.oid).toBe(before); | |
| 270 | const upstreamBranches = await read<{ name: string; oid: string }[]>( | |
| 271 | await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`), | |
| 272 | ); | |
| 267 | expect(localBranches.items.find((b) => b.name === "main")?.oid).toBe(before); | |
| 268 | const upstreamBranches = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`))).items; | |
| 273 | 269 | expect(upstreamBranches.find((b) => b.name === "main")?.oid).toBe(before); |
| 274 | 270 | |
| 275 | 271 | await upstream("/api/v1/repos/octo/gears", { |
| @@ -287,15 +283,11 @@ describe("push-through: the origin gets the last word", () => { | ||
| 287 | 283 | const tip = (await git(["rev-parse", "HEAD"], clone)).out; |
| 288 | 284 | |
| 289 | 285 | expect((await git(["push", "-q", "origin", "topic"], clone)).code).toBe(0); |
| 290 | let upstreamBranches = await read<{ name: string; oid: string }[]>( | |
| 291 | await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`), | |
| 292 | ); | |
| 286 | let upstreamBranches = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`))).items; | |
| 293 | 287 | expect(upstreamBranches.find((b) => b.name === "topic")?.oid).toBe(tip); |
| 294 | 288 | |
| 295 | 289 | expect((await git(["push", "-q", "origin", ":topic"], clone)).code).toBe(0); |
| 296 | upstreamBranches = await read<{ name: string; oid: string }[]>( | |
| 297 | await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`), | |
| 298 | ); | |
| 290 | upstreamBranches = await (await read<{ items: { name: string; oid: string }[] }>(await fetch(`${originServers.url}/api/v1/repos/octo/gears/branches`))).items; | |
| 299 | 291 | expect(upstreamBranches.map((b) => b.name)).not.toContain("topic"); |
| 300 | 292 | }); |
| 301 | 293 |
test/read-api.test.ts +5 −6
| @@ -69,12 +69,11 @@ describe("read-back API (contents, branches, commits)", () => { | ||
| 69 | 69 | }); |
| 70 | 70 | |
| 71 | 71 | test("branches lists heads with the default flagged", async () => { |
| 72 | const branches = (await ( | |
| 73 | await api("/api/v1/repos/dev/lib/branches") | |
| 74 | ).json()) as { | |
| 75 | name: string; | |
| 76 | isDefault: boolean; | |
| 77 | }[]; | |
| 72 | const branches = ( | |
| 73 | (await (await api("/api/v1/repos/dev/lib/branches")).json()) as { | |
| 74 | items: { name: string; isDefault: boolean }[]; | |
| 75 | } | |
| 76 | ).items; | |
| 78 | 77 | const names = branches.map((b) => b.name).sort(); |
| 79 | 78 | expect(names).toEqual(["feature", "main"]); |
| 80 | 79 | expect(branches.find((b) => b.name === "main")?.isDefault).toBe(true); |
test/web-workflows.test.ts +5 −3
| @@ -392,9 +392,11 @@ describe("fork at the origin, and propose a change through it", () => { | ||
| 392 | 392 | expect(flash).toContain("octo/gears#1"); |
| 393 | 393 | |
| 394 | 394 | // The branch really landed on the fork's git. |
| 395 | const branches = (await ( | |
| 396 | await fetch(`${upServers.url}/api/v1/repos/me/gears/branches`) | |
| 397 | ).json()) as { name: string }[]; | |
| 395 | const branches = ( | |
| 396 | (await ( | |
| 397 | await fetch(`${upServers.url}/api/v1/repos/me/gears/branches`) | |
| 398 | ).json()) as { items: { name: string }[] } | |
| 399 | ).items; | |
| 398 | 400 | expect(branches.map((b) => b.name)).toContain("alice/fix-typo"); |
| 399 | 401 | |
| 400 | 402 | // And the pull went upstream with the cross-repository head form. |
new above
offline
Nit:
await (await read<…>(…)).itemshas a redundant outerawait(.itemsis not a promise), and the line is far past the width the rest of the file keeps. Not blocking.