diff --git a/web/src/lib/api/records.ts b/web/src/lib/api/records.ts index bc52e40da..0d2fa68c4 100644 --- a/web/src/lib/api/records.ts +++ b/web/src/lib/api/records.ts @@ -38,18 +38,20 @@ export type ProfileActivityRepoRef = ShTangledActorGetProfileActivity.RepoRef; export const BULK_LIMIT = 50; +export type RepoView = RecordView & { knot?: string }; + export const getRepo = (ctx: BobbinContext, repo: string, init?: XrpcRequestInit) => - jsonGet>(ctx, "sh.tangled.repo.getRepo", { repo }, init); + jsonGet(ctx, "sh.tangled.repo.getRepo", { repo }, init); export const getRepoByRepoDid = (ctx: BobbinContext, repoDid: string, init?: XrpcRequestInit) => - jsonGet>(ctx, "sh.tangled.repo.getRepoByRepoDid", { repoDid }, init); + jsonGet(ctx, "sh.tangled.repo.getRepoByRepoDid", { repoDid }, init); export const getRepoByName = ( ctx: BobbinContext, owner: string, name: string, init?: XrpcRequestInit -) => jsonGet>(ctx, "sh.tangled.repo.getRepoByName", { owner, name }, init); +) => jsonGet(ctx, "sh.tangled.repo.getRepoByName", { owner, name }, init); export interface RepoListPage { cursor?: string | null; diff --git a/web/src/lib/api/repo.test.ts b/web/src/lib/api/repo.test.ts index 72511208d..445a0cab1 100644 --- a/web/src/lib/api/repo.test.ts +++ b/web/src/lib/api/repo.test.ts @@ -5,6 +5,7 @@ import { resolveForkRepoLabels, resolveRepoByName, resolveRepoByRepoDid, + resolveRepoKnot, sortTreeEntries, toBranchSummary, toTagSummary, @@ -358,3 +359,25 @@ describe("resolveRepoByRepoDid", () => { expect(await resolveRepoByRepoDid(makeCtx(fetchMock), "did:plc:missing")).toBeNull(); }); }); + +describe("resolveRepoKnot", () => { + it("takes the knot bobbin puts on the repo view", async () => { + const fetchMock = vi.fn(async () => + jsonResponse({ + uri: "at://did:plc:kelp/org.tangled.repo.manifest/self", + value: {}, + knot: "knot.test" + }) + ); + await expect(resolveRepoKnot(makeCtx(fetchMock), "did:plc:kelp")).resolves.toBe( + "knot.test" + ); + }); + + it("leaves a repo bobbin can't find without a knot", async () => { + const fetchMock = vi.fn(async () => new Response(null, { status: 404 })); + await expect( + resolveRepoKnot(makeCtx(fetchMock), "did:plc:urchin") + ).resolves.toBeUndefined(); + }); +}); diff --git a/web/src/lib/api/repo.ts b/web/src/lib/api/repo.ts index 0db887eba..1058719f1 100644 --- a/web/src/lib/api/repo.ts +++ b/web/src/lib/api/repo.ts @@ -3,8 +3,15 @@ import { ClientResponseError, type BobbinContext, type XrpcRequestInit } from "$ import type { LineStat } from "$lib/api/diff"; import { REPO_OWNER_DOCS } from "$lib/api/descriptors"; import { enrich, handleOf, target, type Sidecar } from "$lib/api/enrich"; -import { resolveMiniDoc } from "$lib/api/identity"; -import { getRepoByName, type RecordList, type RecordView, type RepoRecord } from "$lib/api/records"; +import { serviceUrlFor } from "$lib/api/_request"; +import { + getRepoByName, + getRepoByRepoDid, + type RecordList, + type RecordView, + type RepoRecord, + type RepoView +} from "$lib/api/records"; import { ok } from "@atcute/client"; import { mainSchema as describeRepoSchema, @@ -44,14 +51,18 @@ export const repoOwnerOf = ( return { did, handle: handleOf(data, did, REPO_OWNER_DOCS) }; }; -// TODO(pre-1.0): resolve knot from #tangled_knot service. Don't do PDS fallback. +export const knotUrlFor = (knot: string): URL => { + if (!knot) throw new Error("This repository doesn't have a knot yet."); + return new URL(serviceUrlFor(knot)); +}; + export const resolveRepoKnot = async ( ctx: BobbinContext, repoDid: string, init?: XrpcRequestInit ): Promise => { - const doc = await resolveMiniDoc(ctx, repoDid, init).catch(() => null); - return (doc?.pds && URL.parse(doc.pds)?.host) || undefined; + const repo = await getRepoByRepoDid(ctx, repoDid, init).catch(() => null); + return repo?.knot; }; // a repo bobbin has never indexed is a miss, not an error @@ -60,7 +71,7 @@ export const resolveRepoByName = async ( ownerDid: string, name: string, init?: XrpcRequestInit -): Promise | null> => { +): Promise => { try { return await getRepoByName(ctx, ownerDid, name, init); } catch (cause) { diff --git a/web/src/lib/components/repo/SubjectLabelPanel.svelte b/web/src/lib/components/repo/SubjectLabelPanel.svelte index c6d0330e8..16abeb99d 100644 --- a/web/src/lib/components/repo/SubjectLabelPanel.svelte +++ b/web/src/lib/components/repo/SubjectLabelPanel.svelte @@ -16,7 +16,7 @@ interface Props { subject: string; repoUri: string; - knot: URL; + knot?: URL; labels: SubjectLabels; listHref: string; settingsHref: string; @@ -58,7 +58,7 @@ const change = async (keys: string[]) => { const agent = auth.agent; - if (!agent) return; + if (!agent || !knot) return; const before = applied; const current = picked.map((label) => label.key); const add = keys.filter((key) => !current.includes(key)); @@ -83,7 +83,7 @@ const createLabel = async (name: string, color: string) => { const agent = auth.agent; - if (!agent) return; + if (!agent || !knot) return; const repoDid = didOf(didFromUri(subject)); const label = await createRepoLabel(agent, knot, repoDid, auth.bobbinUrl, { name, diff --git a/web/src/lib/components/repo/issues/IssueForm.svelte b/web/src/lib/components/repo/issues/IssueForm.svelte index de172081a..8facc03ca 100644 --- a/web/src/lib/components/repo/issues/IssueForm.svelte +++ b/web/src/lib/components/repo/issues/IssueForm.svelte @@ -6,7 +6,7 @@ import X from "$icon/x"; import { page } from "$app/state"; import { createTicket, editTicket } from "$lib/api/ticket"; - import { serviceUrlFor } from "$lib/api/_request"; + import { knotUrlFor } from "$lib/api/repo"; import { didOf, recordUriOf } from "$lib/api/syntax"; import { rkeyFromUri } from "$lib/api/uri"; import { awaitIndexedOrSaved } from "$lib/api/awaitRecord"; @@ -86,7 +86,7 @@ return; } const agent = await auth.agentFor(actingDid as Did); - const knotUrl = new URL(serviceUrlFor(knot)); + const knotUrl = knotUrlFor(knot); const saved = uri ? await editTicket(agent, knotUrl, recordUriOf(uri), { title, body }) : await createTicket(agent, knotUrl, didOf(repoDid), { title, body }); diff --git a/web/src/lib/components/repo/issues/IssueThreadView.svelte b/web/src/lib/components/repo/issues/IssueThreadView.svelte index b4ff10359..b3d2017e2 100644 --- a/web/src/lib/components/repo/issues/IssueThreadView.svelte +++ b/web/src/lib/components/repo/issues/IssueThreadView.svelte @@ -32,7 +32,7 @@ } from "$lib/api/graph"; import { cobRefOf } from "$lib/api/cob"; import { editTicket, eraseTicket, setTicketState } from "$lib/api/ticket"; - import { serviceUrlFor } from "$lib/api/_request"; + import { knotUrlFor } from "$lib/api/repo"; import { recordUriOf } from "$lib/api/syntax"; import { createAction } from "$lib/action.svelte"; import { awaitDeletedRecord, awaitIndexedOrSaved } from "$lib/api/awaitRecord"; @@ -154,7 +154,7 @@ const issuesBase = $derived(`/${props.repo.ownerHandle}/${props.repo.name}/issues`); - const knot = $derived(new URL(serviceUrlFor(props.repo.knot))); + const knot = $derived(knotUrlFor(props.repo.knot)); const updateIssue = async ({ title, body }: { title: string; body: string }) => { const agent = auth.agent; @@ -218,7 +218,7 @@ targetBranch: undefined }} markup={props.markup} - {knot} + knot={props.repo.knot ? knot : undefined} reactions={issue.reactions} reactionsAlwaysShow onedit={updateIssue} @@ -274,7 +274,7 @@ subjectUri={issue.uri} subjectCid={issue.cid} markup={props.markup} - {knot} + knot={props.repo.knot ? knot : undefined} class="mt-md" /> @@ -283,7 +283,7 @@ direction="vertical" subject={issue.uri} repoUri={props.repo.uri} - {knot} + knot={props.repo.knot ? knot : undefined} labels={props.labels ?? NO_SUBJECT_LABELS} listHref={issuesBase} settingsHref={`/${props.repo.ownerHandle}/${props.repo.name}/settings#labels`} diff --git a/web/src/lib/components/repo/pulls/PullCompose.svelte b/web/src/lib/components/repo/pulls/PullCompose.svelte index f6c5c6a63..816277c94 100644 --- a/web/src/lib/components/repo/pulls/PullCompose.svelte +++ b/web/src/lib/components/repo/pulls/PullCompose.svelte @@ -14,7 +14,6 @@ import { getPatchStream } from "$lib/api/patchStream"; import { openPull } from "$lib/api/pull"; import { didOf } from "$lib/api/syntax"; - import { serviceUrlFor } from "$lib/api/_request"; import { rkeyFromUri } from "$lib/api/uri"; import { pingIndexNow } from "$lib/api/indexnow"; import { createEntitySubscription } from "$lib/api/graph"; @@ -40,7 +39,7 @@ import DiffView from "$lib/components/repo/DiffView.svelte"; import PullCommitList from "$lib/components/repo/pulls/PullCommitList.svelte"; import PullCommitListHeader from "$lib/components/repo/pulls/PullCommitListHeader.svelte"; - import type { BranchSummary } from "$lib/api/repo"; + import { knotUrlFor, type BranchSummary } from "$lib/api/repo"; import type { Range } from "$lib/components/ui/RangeSelector.svelte"; import type { SelectOption } from "$lib/components/ui/selectField"; import SourceSelector from "$lib/components/repo/SourceSelector.svelte"; @@ -254,7 +253,7 @@ const saved = await openPull( agent, - new URL(serviceUrlFor(repo.knot)), + knotUrlFor(repo.knot), didOf(repoDid), { title, body }, view.targetBranch, diff --git a/web/src/lib/components/settings/RepoGeneralForm.svelte b/web/src/lib/components/settings/RepoGeneralForm.svelte index 556e9e2b0..def1d6aae 100644 --- a/web/src/lib/components/settings/RepoGeneralForm.svelte +++ b/web/src/lib/components/settings/RepoGeneralForm.svelte @@ -3,7 +3,7 @@ import { resolve } from "$app/paths"; import { createAction } from "$lib/action.svelte"; import { deleteRepoLabel, type LabelView } from "$lib/api/labels"; - import { serviceUrlFor } from "$lib/api/_request"; + import { knotUrlFor } from "$lib/api/repo"; import type { RecordView, RepoRecord } from "$lib/api/records"; import { deleteRepo, saveRepoRecord, setDefaultBranch } from "$lib/api/repoSettings"; import { getAuth } from "$lib/auth.svelte"; @@ -106,7 +106,7 @@ const agent = auth.agent; if (!agent) return; try { - await deleteRepoLabel(agent, new URL(serviceUrlFor(repo.knot)), bobbinUrl, label.uri); + await deleteRepoLabel(agent, knotUrlFor(repo.knot), bobbinUrl, label.uri); } finally { await invalidateAll(); } diff --git a/web/src/routes/[handle]/[repo]/+layout.ts b/web/src/routes/[handle]/[repo]/+layout.ts index 234dfe8e3..682a4c7d4 100644 --- a/web/src/routes/[handle]/[repo]/+layout.ts +++ b/web/src/routes/[handle]/[repo]/+layout.ts @@ -9,7 +9,7 @@ import { getStarRkey } from "$lib/api/graph"; import { resolveMiniDoc } from "$lib/api/identity"; import { orNull, parallel, settle, stream, toHttpError } from "$lib/api/load"; import type { RepoRecord } from "$lib/api/records"; -import { resolveRepoByName, resolveRepoByRepoDid, resolveRepoKnot } from "$lib/api/repo"; +import { resolveRepoByName, resolveRepoByRepoDid } from "$lib/api/repo"; import { didFromUri } from "$lib/api/uri"; import type { BobbinContext } from "$lib/api/client"; import type { RepoCounts, RepoInfo, RepoSource } from "$lib/components/repo/types"; @@ -67,7 +67,7 @@ export const load: LayoutLoad = async (event) => { const record = view.value; const repoDid = didFromUri(view.uri); - const knot = (await resolveRepoKnot(ctx, repoDid)) ?? ""; + const knot = view.knot ?? ""; const git = gitTarget(parent.publicConfig, { uri: view.uri, repoDid }, event.fetch); const repo: RepoInfo = { diff --git a/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte b/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte index 35535c1da..51d37fb50 100644 --- a/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte +++ b/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte @@ -26,7 +26,7 @@ import { gitTarget } from "$lib/api/gitclient"; import { repoBranches } from "$lib/api/repoIndex"; import { getPatchStream } from "$lib/api/patchStream"; - import { resolveRepoKnot } from "$lib/api/repo"; + import { knotUrlFor, resolveRepoKnot } from "$lib/api/repo"; import { rkeyFromUri } from "$lib/api/uri"; import { loadDiff, type RepoDiffDeps, type RepoDiffSpec } from "$lib/api/repoDiff"; import { fileAnchor } from "$lib/components/repo/fileAnchor.svelte"; @@ -44,7 +44,6 @@ import type { Did } from "@atcute/lexicons/syntax"; import { cobRefOf, readCob } from "$lib/api/cob"; import type { TicketRecord, TicketState } from "$lib/api/records"; - import { serviceUrlFor } from "$lib/api/_request"; import { recordUriOf } from "$lib/api/syntax"; import type { CommentView } from "$lib/components/comment/comments"; import Button from "$lib/components/ui/Button.svelte"; @@ -387,7 +386,7 @@ let showChangeIds = $state(true); let formHeight = $state(0); - const knot = $derived(new URL(serviceUrlFor(data.repo.knot))); + const knot = $derived(knotUrlFor(data.repo.knot)); const pullUri = $derived(recordUriOf(data.uri)); const insertComment = async (body: string) => { @@ -598,7 +597,7 @@ branch.name)} defaultBranch={targetBranches.data?.find((branch) => branch.isDefault)?.name} @@ -640,7 +639,7 @@ diff --git a/web/src/routes/[handle]/[repo]/settings/labels/new/+page.svelte b/web/src/routes/[handle]/[repo]/settings/labels/new/+page.svelte index 288634c7d..702b77c06 100644 --- a/web/src/routes/[handle]/[repo]/settings/labels/new/+page.svelte +++ b/web/src/routes/[handle]/[repo]/settings/labels/new/+page.svelte @@ -3,7 +3,7 @@ import { resolve } from "$app/paths"; import { createAction } from "$lib/action.svelte"; import { createRepoLabel } from "$lib/api/labels"; - import { serviceUrlFor } from "$lib/api/_request"; + import { knotUrlFor } from "$lib/api/repo"; import { didOf } from "$lib/api/syntax"; import { getAuth } from "$lib/auth.svelte"; import Button from "$lib/components/ui/Button.svelte"; @@ -51,7 +51,7 @@ if (!repo.repoDid) throw new Error("This repository has no DID on its knot yet."); await createRepoLabel( agent, - new URL(serviceUrlFor(repo.knot)), + knotUrlFor(repo.knot), didOf(repo.repoDid), data.publicConfig.bobbinUrl, { name: name.trim(), color }