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

GET a repository answers `upstream: { repo, behind, checkedAt, conflict }` for a fork, and MCP `repo` says the same #5871

Merged opened this

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

Merged

`GET /api/v1/repos/:owner/:name` and the MCP `repo` tool now both return `upstream: { repo, behind, checkedAt, conflict }`, or `null` when the repository follows nothing. The new tests, the OpenAPI/coverage tests, `test/mcp.test.ts` and the type check pass. I didn't run the full check.

shipped production is serving it deploy f5d5671

opened this

GET /api/v1/repos/:owner/:name and the MCP repo tool now both return upstream: { repo, behind, checkedAt, conflict }, or null when the repository follows nothing. The new tests, the OpenAPI/coverage tests, test/mcp.test.ts and the type check pass. I didn't run the full check.

  • Source: the object is read from the stored follow check by one helper, upstreamFacts, in src/actions/upstream_follow.ts. The API and MCP both call it, so they answer the same.
  • repo: this is the followed upstream as host/path, for example example.com/up/lib.
  • conflict: it is true when the last check ended in a conflict.
  • Disabled follow: a follow that exists but is switched off also answers null, because "follows nothing" read most naturally that way.
  • OpenAPI: the repo read's description in src/contract/openapi.ts now describes upstream.
  • Tests: test/repo-upstream-facts.test.ts covers a fork that is behind, one that is level, one with a conflict, and a non-fork, over both doors.

Not done: repo is the followed URL, not a forge repository name. The fork's home page is untouched, so the person's view and the agent's view are not yet checked against each other.

To try it, call GET /api/v1/repos/:owner/:name on a repository that follows an upstream and read upstream.

🤖 Generated with Claude Code

Closes #5868

commented

GET /api/v1/repos/:owner/:name and the MCP repo tool now both return upstream: { repo, behind, checkedAt, conflict }, or null when the repository follows nothing. The new tests, the OpenAPI/coverage tests, test/mcp.test.ts and the type check pass. I didn't run the full check.

  • Source: the object is read from the stored follow check by one helper, upstreamFacts, in src/actions/upstream_follow.ts. The API and MCP both call it, so they answer the same.
  • repo: this is the followed upstream as host/path, for example example.com/up/lib.
  • conflict: it is true when the last check ended in a conflict.
  • Disabled follow: a follow that exists but is switched off also answers null, because "follows nothing" read most naturally that way.
  • OpenAPI: the repo read's description in src/contract/openapi.ts now describes upstream.
  • Tests: test/repo-upstream-facts.test.ts covers a fork that is behind, one that is level, one with a conflict, and a non-fork, over both doors.

Not done: repo is the followed URL, not a forge repository name. The fork's home page is untouched, so the person's view and the agent's view are not yet checked against each other.

To try it, call GET /api/v1/repos/:owner/:name on a repository that follows an upstream and read upstream.

🤖 Generated with Claude Code

Changes requested — before the latest push

High: the change shows follow facts that only admins can see today to everyone who can read the repository. Everywhere else, a repository's upstream follow is admin-only: GET /api/v1/repos/:owner/:name/upstream checks canAdmin (src/http/api.ts:2448), the MCP follow_upstream read does the same (src/http/mcp.ts:2789), the settings card checks it (src/web/pages/upstream-follow.ts:51), and so does the terminal's about screen (src/tui/screens/about.ts:348). This pull adds upstream to the plain repository read, and that read has auth: false. So an anonymous visitor to a public fork, or any reader with a ro token, now learns:

  • the upstream's host and path, which can name a private or internal host;
  • how far behind it is;
  • whether the last merge conflicted.

All three were admin-only before. The tests only ever read as the repository's admin, so they cannot catch this.

Fix first: in both doors, call upstreamFacts only when canAdmin(db, repo, user) holds, and answer null otherwise. The cleanest way is to give upstreamFacts the user and the repository and do the check there, so the API and MCP cannot drift apart. Then add a test where an anonymous reader and a non-admin ro reader of the public behind fork both get upstream: null. That test fails today.

Smaller points:

  • The OpenAPI summary reads conflict} with no space before the brace.
  • The preview has no repositories (/api/v1/repos is empty), so I could not see the field working there. The tests are the only proof.
commented

The repository read (API and MCP repo) now returns upstream: { repo, behind, checkedAt, conflict } only to an admin of the repository. Everyone else gets null, and so does a repository that follows nothing. The new test, the OpenAPI tests and the type check pass. I didn't run the full check.

  • Fix: upstreamFacts(db, repo, user) now makes the admin check itself, so both doors share it and neither can skip it. It returns null unless canAdmin holds, which is the rule the /upstream route, follow_upstream, the settings card and the terminal's about screen already follow.
  • Test: test/repo-upstream-facts.test.ts now also reads the public behind fork as an anonymous visitor and as a non-admin ro reader. Both get upstream: null over the API, and the ro reader gets null over the MCP. This test fails without the gate.
  • OpenAPI: the summary now reads conflict } and says the field is shown to an admin of the repository.
  • Your "preview has no repositories" point: I didn't act on it. The tests are the proof here, and the preview has nothing to show.

To try it, read a following fork with an admin token and see upstream. Read the same fork anonymously and see null.

🤖 Generated with Claude Code

Approved

Approved. The pull does what #5868 asks, and the second round put back the gate it was missing.

  • Security: upstreamFacts checks canAdmin itself before it reads the follow row. That is the same rule as followShown, which the /upstream page and the terminal's about screen already use, so the API, the MCP and the page agree on who sees the follow. repo is upstreamLabel(url), which is host/path with no userinfo, so no credentials in an upstream URL leak. The handler has no new input to validate.
  • Correctness: conflict reads error === "conflict", which is the exact string checkUpstream writes (upstream_follow.ts:305). A disabled follow answers null, which is a sensible reading of "follows nothing".
  • Tests: they cover a fork that is behind, one that is level, one with a conflict, and a repository that follows nothing, over both the API and the MCP. They also check that an anonymous reader and a non-admin ro reader get null. The tests would fail without the field and without the gate.
  • Preview: it answers 200 at /, and /api/v1/repos is empty there, so no following fork exists to read. That was expected for this change, and the tests carry the proof.
  • Scope: the change is small and single-purpose. There is no terminal work to add, because the about screen already shows the follow through followShown.

A nit, nothing to block on: in src/http/api.ts the upstreamFacts read runs inside the render timing phase. It is cheap, one indexed row, but it would read more honestly in its own timePhase.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs