Public: anyone can read this; the crew can change it.

A repository's upstream and forks routes leave api.ts for src/http/api/upstream.ts #6932

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/6305-a-repository-s-upstream-and-forks-routes → main 2 commits fork

  • Merged. mason/6305-a-repository-s-upstream-and-forks-routes was merged into main as 450838f.

Review

Sign in to review this change.
3 files changed +109 −89
src/http/api.ts +4 −89
@@ -9,14 +9,7 @@
99 */
1010
1111 import { homeTruth } from "../system/home_truth.ts";
12 import {
13 checkUpstreamNow,
14 FOLLOW_REFUSALS,
15 followJson,
16 upstreamFacts,
17 setFollowUpstream,
18 } from "../actions/upstream_follow.ts";
19 import { getFollow } from "../core/upstream_follow.ts";
12 import { upstreamFacts } from "../actions/upstream_follow.ts";
2013 import { FAMILY_WORDS } from "../actions/family_words.ts";
2114 import { showProvenance } from "../actions/provenance.ts";
2215 import {
@@ -160,8 +153,6 @@ import {
160153 createWebhook,
161154 deleteRepo,
162155 deleteWebhook,
163 FORKS_PER_DAY,
164 forkRepo,
165156 repositoryLimitMessage,
166157 resolveOwner,
167158 privateRepoForSealed,
@@ -223,7 +214,6 @@ import {
223214 getRepo,
224215 listReposByOwner,
225216 listReposVisibleTo,
226 visibleForks,
227217 type Repo,
228218 type RepoCounts,
229219 canWrite,
@@ -437,6 +427,7 @@ import { boardRoutes } from "./api/boards.ts";
437427 import { labelsRoutes } from "./api/labels.ts";
438428 import { orgRoutes } from "./api/orgs.ts";
439429 import { secretRoutes } from "./api/secrets.ts";
430 import { upstreamRoutes } from "./api/upstream.ts";
440431 import type { Router } from "./router.ts";
441432 import { clientAddress, clientBucket } from "./peer.ts";
442433 import { manifestFor } from "../actions/here.ts";
@@ -455,7 +446,7 @@ import { REPORT_RECEIVED } from "../actions/report_words.ts";
455446 import { ADMIN_WORDS } from "../actions/admin_words.ts";
456447 import { SETTING_WORDS } from "../actions/setting_words.ts";
457448 import { SPEND_WORDS } from "../actions/spend_words.ts";
458 import { ANSWER_WORDS, ENABLED_WORDS, ORDER_WORDS, SIGNUP_WORDS, VISIBILITY_WORDS } from "../core/arg_words.ts";
449 import { ANSWER_WORDS, ORDER_WORDS, SIGNUP_WORDS, VISIBILITY_WORDS } from "../core/arg_words.ts";
459450
460451 /** Take the parent's updates for `repo` and answer as the API does, for every route that asks. */
461452 async function respondTake(ctx: Ctx, repo: Repo): Promise<Response> {
@@ -2128,52 +2119,7 @@ export function apiRoutes(router: Router<Handler>): void {
21282119 return res;
21292120 });
21302121
2131 // Following an outside upstream (#3442): an admin reads, sets and checks.
2132 router.add("GET", "/api/v1/repos/:owner/:name/upstream", (ctx) => {
2133 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
2134 const denied = requireScope(ctx, "ro");
2135 if (denied) return denied;
2136 const read = timePhase(ctx.request, "upstream", () => {
2137 const repo = loadRepo(ctx);
2138 if (!repo || !canAdmin(ctx.app.db, repo, ctx.auth.user)) return undefined;
2139 return getFollow(ctx.app.db, repo.id) ?? null;
2140 });
2141 if (read === undefined) return notFound();
2142 return timePhase(ctx.request, "render", () => json({ following: read === null ? null : followJson(read) }));
2143 });
2144
2145 router.add("PUT", "/api/v1/repos/:owner/:name/upstream", async (ctx) => {
2146 const denied = requireScope(ctx, "admin");
2147 if (denied) return denied;
2148 const repo = loadRepo(ctx);
2149 if (!repo) return notFound();
2150 const input = await body(ctx.request);
2151 if (typeof input.enabled !== "boolean")
2152 return apiError("bad_enabled", 422, ENABLED_WORDS);
2153 const saved = setFollowUpstream(ctx.app, ctx.auth.user, repo, {
2154 url: str(input.url),
2155 branch: str(input.branch),
2156 enabled: input.enabled,
2157 });
2158 if (!saved.ok)
2159 return saved.error === "not_found" || saved.error === "unauthorized" || saved.error === "forbidden"
2160 ? respondAccess(saved.error, 422)
2161 : apiError(saved.error, 422, FOLLOW_REFUSALS[saved.error]);
2162 return json({ following: followJson(saved.value) });
2163 });
2164
2165 router.add("POST", "/api/v1/repos/:owner/:name/upstream/check", async (ctx) => {
2166 const denied = requireScope(ctx, "admin");
2167 if (denied) return denied;
2168 const repo = loadRepo(ctx);
2169 if (!repo) return notFound();
2170 const checked = await checkUpstreamNow(ctx.app, ctx.auth.user, repo);
2171 if (!checked.ok)
2172 return checked.error === "not_found" || checked.error === "unauthorized" || checked.error === "forbidden"
2173 ? respondAccess(checked.error, 422)
2174 : apiError(checked.error, checked.error === "busy" ? 409 : 422, FOLLOW_REFUSALS[checked.error]);
2175 return json(checked.value);
2176 });
2122 upstreamRoutes(router);
21772123
21782124 router.add("PATCH", "/api/v1/repos/:owner/:name", async (ctx) => {
21792125 const denied = requireScope(ctx, "admin");
@@ -2280,37 +2226,6 @@ export function apiRoutes(router: Router<Handler>): void {
22802226 return timePhase(ctx.request, "render", () => json(page));
22812227 });
22822228
2283 router.add("GET", "/api/v1/repos/:owner/:name/forks", (ctx) => {
2284 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
2285 const repo = loadRepo(ctx);
2286 if (!repo) return notFound();
2287 const forks = timePhase(ctx.request, "forks", () => visibleForks(ctx.app.db, repo.id, ctx.auth.user));
2288 return timePhase(ctx.request, "render", () => json(forks));
2289 });
2290
2291 router.add("POST", "/api/v1/repos/:owner/:name/forks", async (ctx) => {
2292 const denied = requireScope(ctx, "rw");
2293 if (denied) return denied;
2294 const parent = loadRepo(ctx);
2295 if (!parent) return notFound();
2296 const input = await body(ctx.request);
2297 const forked = await forkRepo(
2298 ctx.app,
2299 ctx.auth.user,
2300 parent,
2301 str(input.name) || undefined,
2302 );
2303 if (!forked.ok)
2304 return forked.error === "repository_limit"
2305 ? apiError("repository_limit", 403, repositoryLimitMessage(ctx.app, ctx.auth.user!))
2306 : forked.error === "storage_limit"
2307 ? apiError("storage_limit", 403, storageLimitMessage(ctx.app, ctx.auth.user!.id))
2308 : forked.error === "limit"
2309 ? apiError("limit", 429, `at most ${FORKS_PER_DAY} forks a day: try again tomorrow`)
2310 : respondAccess(forked.error, 422);
2311 return json(forked.value, 201);
2312 });
2313
23142229 // ── following. A fork's standing against its parent, read from git each
23152230 // time; taking updates is git's part, converging is the crew's.
23162231 router.add("GET", "/api/v1/repos/:owner/:name/following", async (ctx) => {
src/http/api/upstream.ts added +104
@@ -0,0 +1,104 @@
1 /**
2 * A repository's upstream and forks over the API: `GET` and `PUT`
3 * `/api/v1/repos/:owner/:name/upstream`, `POST …/upstream/check`, and `GET`
4 * and `POST` `/api/v1/repos/:owner/:name/forks`. Moved out of `api.ts`
5 * (#6305); the rules live in `actions/upstream_follow.ts` and
6 * `actions/repos.ts`.
7 */
8
9 import { forkRepo, FORKS_PER_DAY, repositoryLimitMessage } from "../../actions/repos.ts";
10 import {
11 checkUpstreamNow,
12 FOLLOW_REFUSALS,
13 followJson,
14 setFollowUpstream,
15 } from "../../actions/upstream_follow.ts";
16 import { ENABLED_WORDS } from "../../core/arg_words.ts";
17 import { canAdmin, visibleForks } from "../../core/repos.ts";
18 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
19 import { storageLimitMessage } from "../../core/storage.ts";
20 import { getFollow } from "../../core/upstream_follow.ts";
21 import { loadRepo } from "../api.ts";
22 import type { Handler } from "../context.ts";
23 import { apiError, body, json, notFound, requireScope, respondAccess, str } from "../respond.ts";
24 import type { Router } from "../router.ts";
25
26 export function upstreamRoutes(router: Router<Handler>): void {
27 // Following an outside upstream (#3442): an admin reads, sets and checks.
28 router.add("GET", "/api/v1/repos/:owner/:name/upstream", (ctx) => {
29 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
30 const denied = requireScope(ctx, "ro");
31 if (denied) return denied;
32 const read = timePhase(ctx.request, "upstream", () => {
33 const repo = loadRepo(ctx);
34 if (!repo || !canAdmin(ctx.app.db, repo, ctx.auth.user)) return undefined;
35 return getFollow(ctx.app.db, repo.id) ?? null;
36 });
37 if (read === undefined) return notFound();
38 return timePhase(ctx.request, "render", () => json({ following: read === null ? null : followJson(read) }));
39 });
40
41 router.add("PUT", "/api/v1/repos/:owner/:name/upstream", async (ctx) => {
42 const denied = requireScope(ctx, "admin");
43 if (denied) return denied;
44 const repo = loadRepo(ctx);
45 if (!repo) return notFound();
46 const input = await body(ctx.request);
47 if (typeof input.enabled !== "boolean")
48 return apiError("bad_enabled", 422, ENABLED_WORDS);
49 const saved = setFollowUpstream(ctx.app, ctx.auth.user, repo, {
50 url: str(input.url),
51 branch: str(input.branch),
52 enabled: input.enabled,
53 });
54 if (!saved.ok)
55 return saved.error === "not_found" || saved.error === "unauthorized" || saved.error === "forbidden"
56 ? respondAccess(saved.error, 422)
57 : apiError(saved.error, 422, FOLLOW_REFUSALS[saved.error]);
58 return json({ following: followJson(saved.value) });
59 });
60
61 router.add("POST", "/api/v1/repos/:owner/:name/upstream/check", async (ctx) => {
62 const denied = requireScope(ctx, "admin");
63 if (denied) return denied;
64 const repo = loadRepo(ctx);
65 if (!repo) return notFound();
66 const checked = await checkUpstreamNow(ctx.app, ctx.auth.user, repo);
67 if (!checked.ok)
68 return checked.error === "not_found" || checked.error === "unauthorized" || checked.error === "forbidden"
69 ? respondAccess(checked.error, 422)
70 : apiError(checked.error, checked.error === "busy" ? 409 : 422, FOLLOW_REFUSALS[checked.error]);
71 return json(checked.value);
72 });
73
74 router.add("GET", "/api/v1/repos/:owner/:name/forks", (ctx) => {
75 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
76 const repo = loadRepo(ctx);
77 if (!repo) return notFound();
78 const forks = timePhase(ctx.request, "forks", () => visibleForks(ctx.app.db, repo.id, ctx.auth.user));
79 return timePhase(ctx.request, "render", () => json(forks));
80 });
81
82 router.add("POST", "/api/v1/repos/:owner/:name/forks", async (ctx) => {
83 const denied = requireScope(ctx, "rw");
84 if (denied) return denied;
85 const parent = loadRepo(ctx);
86 if (!parent) return notFound();
87 const input = await body(ctx.request);
88 const forked = await forkRepo(
89 ctx.app,
90 ctx.auth.user,
91 parent,
92 str(input.name) || undefined,
93 );
94 if (!forked.ok)
95 return forked.error === "repository_limit"
96 ? apiError("repository_limit", 403, repositoryLimitMessage(ctx.app, ctx.auth.user!))
97 : forked.error === "storage_limit"
98 ? apiError("storage_limit", 403, storageLimitMessage(ctx.app, ctx.auth.user!.id))
99 : forked.error === "limit"
100 ? apiError("limit", 429, `at most ${FORKS_PER_DAY} forks a day: try again tomorrow`)
101 : respondAccess(forked.error, 422);
102 return json(forked.value, 201);
103 });
104 }
test/arg-words.test.ts +1
@@ -15,6 +15,7 @@ const DEEP = "../../../core/arg_words.ts";
1515 /** Each file that refuses with these words, and how it imports them. */
1616 const DOORS: Record<string, string> = {
1717 "src/http/api.ts": "../core/arg_words.ts",
18 "src/http/api/upstream.ts": "../../core/arg_words.ts",
1819 "src/http/feed.ts": "../core/arg_words.ts",
1920 "src/http/mcp/tools/asked.ts": DEEP,
2021 "src/http/mcp/tools/board_pins.ts": DEEP,

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs