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

A mirrored repository's credit (upstream, licence, original authors) is in the REST and MCP answers, not only on its page #10970

Merged opened this

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

Merged

mason/forge:mason/10904-a-mirrored-repository-s-credit-upstream → main 4 commits fork

  • Merged. mason/10904-a-mirrored-repository-s-credit-upstream was merged into main as 4ecc7e7.

Review

Sign in to review this change.
6 files changed +55 −6
src/actions/credit_facts.ts added +16
@@ -0,0 +1,16 @@
1 /**
2 * A repository's credit as the REST and MCP answers carry it (#10904): the
3 * same reading the page and the terminal print, as plain data. Null for a
4 * repository that is not a mirror.
5 */
6
7 import type { App } from "../app.ts";
8 import { creditOf, type Credit } from "../core/credit.ts";
9 import type { Repo } from "../core/repos.ts";
10 import type { User } from "../core/users.ts";
11 import { localSource } from "../view/local.ts";
12
13 export async function creditFacts(app: App, repo: Repo, viewer: User | undefined): Promise<Credit | null> {
14 const credit = await creditOf(app.db, app.reposRoot, repo, viewer, localSource(app, repo, viewer), repo.defaultBranch);
15 return credit ?? null;
16 }
src/contract/openapi/routes/getApiV1ReposOwnerName.ts +1 −1
@@ -4,7 +4,7 @@ export const route: Route = {
44 method: "get",
55 path: REPO,
66 summary:
7 "Read one repository; `provenance` is where a migrated one came from ({ source, sourceUrl, parents: [{ fullName, url }], migratedAt }), null when it was not migrated or its owner hid it; `upstream` is the stored follow check ({ repo, behind, checkedAt, conflict }), shown to an admin of the repository, null when it follows nothing or to anyone else",
7 "Read one repository; `provenance` is where a migrated one came from ({ source, sourceUrl, parents: [{ fullName, url }], migratedAt }), null when it was not migrated or its owner hid it; `upstream` is the stored follow check ({ repo, behind, checkedAt, conflict }), shown to an admin of the repository, null when it follows nothing or to anyone else; `credit` is a mirror's { upstream: { fullName, htmlUrl, viaFork }, licence, authors }, null for a repository that is not a mirror",
88 auth: false,
99 role: "anonymous",
1010 refusals: { not_found: "404: no such repository, or it is not readable by the caller" },
src/http/api.ts +6 −2
@@ -132,6 +132,7 @@ import {
132132 import { OFFSET_LIMIT_MAX, offsetPage, offsetQuery } from "./offset_page.ts";
133133 import { EVENTS_LIMIT_MAX, eventsLimit, eventsPage, validEventsCursor, validEventsKind } from "../core/events_page.ts";
134134 import { audienceFor } from "../core/audience.ts";
135 import { creditFacts } from "../actions/credit_facts.ts";
135136 import {
136137 forkParentFor,
137138 getReposByIds,
@@ -1605,15 +1606,18 @@ export function apiRoutes(router: Router<Handler>): void {
16051606 return json(created.value, 201);
16061607 });
16071608
1608 router.add("GET", "/api/v1/repos/:owner/:name", (ctx) => {
1609 router.add("GET", "/api/v1/repos/:owner/:name", async (ctx) => {
16091610 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
16101611 const repo = timePhase(ctx.request, "repo", () => loadRepo(ctx));
16111612 if (!repo) return notFound();
16121613 const facts = timePhase(ctx.request, "counts", () => repoJson(ctx, repo));
16131614 const audience = timePhase(ctx.request, "audience", () => audienceFor(ctx.app.db, repo, ctx.auth.user));
1615 const credited = performance.now();
1616 const credit = await creditFacts(ctx.app, repo, ctx.auth.user);
1617 notePhase(ctx.request, "credit", performance.now() - credited);
16141618 const rendering = performance.now();
16151619 const upstream = upstreamFacts(ctx.app.db, repo, ctx.auth.user);
1616 const res = json({ ...facts, audience, upstream });
1620 const res = json({ ...facts, audience, upstream, credit });
16171621 notePhase(ctx.request, "render", performance.now() - rendering);
16181622 return res;
16191623 });
src/http/mcp/tools/repo.ts +4 −2
@@ -2,11 +2,12 @@ import { provenanceFacts } from "../../../core/provenance.ts";
22 import { forkParentFor } from "../../../core/repos.ts";
33 import { audienceFor } from "../../../core/audience.ts";
44 import { upstreamFacts } from "../../../actions/upstream_follow.ts";
5 import { creditFacts } from "../../../actions/credit_facts.ts";
56 import { good, type Tool, REPO_ARG, repoArg, isAnswer } from "../shared.ts";
67
78 export const tool: Tool = {
89 name: "repo",
9 doc: "`{ repo }`: one repository's description, visibility, autonomy, `audience { level, who, sentence }` and `provenance` (where a migrated one came from, `null` when hidden), the same as `GET /api/v1/repos/:owner/:name`",
10 doc: "`{ repo }`: one repository's description, visibility, autonomy, `audience { level, who, sentence }` and `provenance` (where a migrated one came from, `null` when hidden), `credit` (a mirror's upstream, licence, authors; `null` otherwise), the same as `GET /api/v1/repos/:owner/:name`",
1011 description:
1112 "One repository you can read, as GET /api/v1/repos/:owner/:name.",
1213 inputSchema: {
@@ -15,7 +16,7 @@ export const tool: Tool = {
1516 required: ["repo"],
1617 },
1718 writes: false,
18 run(ctx, user, args) {
19 async run(ctx, user, args) {
1920 const repo = repoArg(ctx, user, args);
2021 if (isAnswer(repo)) return repo;
2122 return good({
@@ -30,6 +31,7 @@ export const tool: Tool = {
3031 forkParentFor(ctx.app.db, repo, user),
3132 ),
3233 upstream: upstreamFacts(ctx.app.db, repo, user),
34 credit: await creditFacts(ctx.app, repo, user),
3335 });
3436 },
3537 };
test/mirror-credit.test.ts +27
@@ -12,6 +12,7 @@ import { AUTHOR_WINDOW, clearCreditCache, creditOf, creditText, licenceName, top
1212 import { runGit } from "../src/git/env.ts";
1313 import { createOrigin, linkRepoToOrigin } from "../src/core/origins.ts";
1414 import { createRepo, forkRepo, getRepoById, type Repo } from "../src/core/repos.ts";
15 import { createToken } from "../src/core/tokens.ts";
1516 import { createUser } from "../src/core/users.ts";
1617 import { repoDir } from "../src/git/repos.ts";
1718 import { commitFiles } from "../src/git/write.ts";
@@ -32,6 +33,7 @@ describe("the credit line of a mirror", () => {
3233 let plain: Repo;
3334 let frozen: Repo;
3435 let orphan: Repo;
36 let token = "";
3537
3638 const put = async (repo: Repo, who: string, files: { path: string; content: string }[]) => {
3739 const r = await commitFiles(repoDir(app.reposRoot, repo.ownerName, repo.name), {
@@ -52,6 +54,7 @@ describe("the credit line of a mirror", () => {
5254 const made0 = await createUser(app.db, { origin: "seed", trusted: true, username: "ada", email: "[email protected]", adminIfFirst: true });
5355 if (!made0.ok) throw new Error(String(made0.error));
5456 const u = made0.value;
57 token = createToken(app.db, u.id, "credit", "rw").token;
5558 const mk = async (name: string) => {
5659 const r = await createRepo(app.db, app.reposRoot, { ownerId: u.id, name, visibility: "public", description: "", defaultBranch: "main" });
5760 if (!r.ok) throw new Error(r.error);
@@ -163,4 +166,28 @@ describe("the credit line of a mirror", () => {
163166 expect(await topAuthors(join(dataDir, "nowhere"), "main")).toEqual([]);
164167 expect((await topAuthors(dir, "main"))[0]).toBe("Zed & Zeta");
165168 });
169
170 const rest = async (repo: Repo) =>
171 (await (await fetch(`${servers.url}/api/v1/repos/${repo.ownerName}/${repo.name}`)).json()) as { credit: unknown };
172 const mcp = async (repo: Repo) => {
173 const res = await fetch(`${servers.url}/mcp`, {
174 method: "POST",
175 headers: { "content-type": "application/json", accept: "application/json, text/event-stream", authorization: `Bearer ${token}` },
176 body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "repo", arguments: { repo: `${repo.ownerName}/${repo.name}` } } }),
177 });
178 const body = (await res.json()) as any;
179 return JSON.parse(body.result.content[0].text) as { credit: unknown };
180 };
181
182 test("REST and MCP carry the credit a page prints, and null for a native repository", async () => {
183 const want = { upstream: { fullName: "up/lib", htmlUrl: "https://up.example/up/lib", viaFork: false }, licence: "MIT", authors: ["Zed & Zeta", "Quinn"] };
184 expect((await rest(mirror)).credit).toEqual(want);
185 expect((await mcp(mirror)).credit).toEqual(want);
186 const viaFork = { ...want, upstream: { ...want.upstream, viaFork: true } };
187 expect((await rest(fork)).credit).toEqual(viaFork);
188 expect((await mcp(fork)).credit).toEqual(viaFork);
189 expect((await rest(plain)).credit).toBeNull();
190 expect((await mcp(plain)).credit).toBeNull();
191 expect(((await rest(bare)).credit as { licence: string }).licence).toBe("no licence file");
192 });
166193 });
test/security-floor-doors.test.ts +1 −1
@@ -92,7 +92,7 @@ test("each return of the public door and of `answered` is floored", () => {
9292 // `response` is only ever what `answered` gave back.
9393 for (const m of region.matchAll(/\bresponse\s*=\s*([^\n;]+)/g)) expect(m[1]!.trim()).toMatch(/^await answered\(/);
9494 // `answered` floors what the work returns, and its failure path floors too.
95 expect(region).toContain("const done = await compress(request, response).then(securityFloor)");
95 expect(region).toMatch(/const done = await compress\(request, response\)\.then\(\(r\) => securityFloor\(/);
9696 expect(server).toMatch(/function failure\([^]*?\n}\n/);
9797 const failure = server.match(/function failure\([^]*?\n}\n/)![0];
9898 for (const r of failure.matchAll(/\breturn\s+([^\n]+)/g)) expect(r[1]!.trim()).toMatch(/^securityFloor\(/);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs