From 43a04e967969ff8080b0a4b1c49f0885abda9e09 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Fri, 25 Sep 2026 01:07:27 +0900 Subject: [PATCH] web: remove at-uri repo path Signed-off-by: Seongmin Lee --- web/src/lib/api/blame.test.ts | 78 +--- web/src/lib/api/blame.ts | 38 +- web/src/lib/api/diff.ts | 69 ---- web/src/lib/api/gitclient.test.ts | 69 +--- web/src/lib/api/gitclient.ts | 102 ++--- web/src/lib/api/index.ts | 1 - web/src/lib/api/knot.test.ts | 26 -- web/src/lib/api/knot.ts | 107 ------ web/src/lib/api/knotmirror.ts | 6 + web/src/lib/api/rawdiff.test.ts | 359 ------------------ web/src/lib/api/rawdiff.ts | 132 ------- web/src/lib/api/repo.test.ts | 61 +-- web/src/lib/api/repo.ts | 79 +--- web/src/lib/api/repoDiff.test.ts | 13 +- .../components/repo/CommitView.stories.svelte | 22 +- web/src/lib/components/repo/CommitView.svelte | 2 +- .../repo/blame/request.stories.svelte | 4 +- .../components/repo/blame/request.svelte.ts | 10 +- .../components/repo/pulls/PullCompose.svelte | 2 +- .../[repo]/blob/[ref]/[...path]/+page.svelte | 4 +- .../[repo]/commit/[spec=rawCommit]/+server.ts | 34 +- .../[version]/[[range]]/PullViewPage.svelte | 2 +- 22 files changed, 108 insertions(+), 1112 deletions(-) delete mode 100644 web/src/lib/api/knot.test.ts delete mode 100644 web/src/lib/api/knot.ts delete mode 100644 web/src/lib/api/rawdiff.test.ts delete mode 100644 web/src/lib/api/rawdiff.ts diff --git a/web/src/lib/api/blame.test.ts b/web/src/lib/api/blame.test.ts index e9ad86a8..cbc9829b 100644 --- a/web/src/lib/api/blame.test.ts +++ b/web/src/lib/api/blame.test.ts @@ -1,12 +1,8 @@ -import type { ResourceUri } from "@atcute/lexicons"; import type { Did } from "@atcute/lexicons/syntax"; import { describe, expect, it, vi } from "vitest"; import { blameFor, hydrateBlameAuthors } from "$lib/api/blame"; import { createBobbinClient } from "$lib/api/client"; -// bobbin mints a repo did that is not the owner's: the mirror is keyed by the -// former, the knot by the record's at-uri -const REPO = "at://did:plc:owner/sh.tangled.repo/core" as ResourceUri; const REPO_DID = "did:plc:repo" as Did; const jsonResponse = (body: unknown): Response => @@ -30,7 +26,7 @@ const makeCtx = (fetchMock: typeof globalThis.fetch) => createBobbinClient({ serviceUrl: "https://bobbin.example", fetch: fetchMock }); const blameCall = (ctx: ReturnType) => - blameFor(ctx, { repo: REPO, repoDid: REPO_DID }, "main", "src/main.rs"); + blameFor(ctx, REPO_DID, "main", "src/main.rs"); describe("blameFor", () => { it("requests gitmirror getBlame and maps its one-based runs", async () => { @@ -96,78 +92,6 @@ describe("blameFor", () => { expect(runs[0].commit.author.did).toBeUndefined(); }); - // a repo the mirror doesn't have, and a commit it hasn't fetched yet, both mean "ask the knot" - it.each(["RepoNotFound", "RefNotFound"])( - "falls back to the knot when the mirror answers %s", - async (miss) => { - const fetchMock = vi - .fn() - .mockResolvedValueOnce(errorResponse(404, miss)) - .mockResolvedValueOnce( - jsonResponse({ - commit: "abc123", - commits: [ - { - sha: "abc123", - subject: "add main", - authoredAt: "2026-06-01T12:30:00Z", - author: { name: "dawn", email: "did:plc:dawn" } - } - ], - runs: [{ start: 1, count: 3, commit: "abc123" }] - }) - ); - const ctx = makeCtx(fetchMock); - - await expect(blameCall(ctx)).resolves.toEqual([ - { - start: 1, - count: 3, - commit: { - sha: "abc123", - subject: "add main", - when: "2026-06-01T12:30:00Z", - author: { name: "dawn", email: "did:plc:dawn", did: "did:plc:dawn" } - } - } - ]); - - const urls = fetchMock.mock.calls.map((call) => new URL(String(call[0])).pathname); - expect(urls).toEqual([ - "/xrpc/sh.tangled.git.temp2.getBlame", - "/xrpc/sh.tangled.repo.blame" - ]); - const knotUrl = new URL(String(fetchMock.mock.calls[1][0])); - expect(knotUrl.searchParams.get("repo")).toBe(REPO); - } - ); - - // bobbin never indexed the repo, so there is no did to ask the mirror with - it("goes straight to the knot without a repo did", async () => { - const fetchMock = vi.fn().mockResolvedValue( - jsonResponse({ - commit: "abc123", - commits: [ - { - sha: "abc123", - subject: "add main", - authoredAt: "2026-06-01T12:30:00Z", - author: { name: "dawn", email: "did:plc:dawn" } - } - ], - runs: [{ start: 1, count: 3, commit: "abc123" }] - }) - ); - const ctx = makeCtx(fetchMock); - - const runs = await blameFor(ctx, { repo: REPO }, "main", "src/main.rs"); - expect(runs[0].commit.sha).toBe("abc123"); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(new URL(String(fetchMock.mock.calls[0][0])).pathname).toBe( - "/xrpc/sh.tangled.repo.blame" - ); - }); - it("propagates mirror errors that are not a mirror miss", async () => { const fetchMock = vi .fn() diff --git a/web/src/lib/api/blame.ts b/web/src/lib/api/blame.ts index 9c07fb62..84fb5383 100644 --- a/web/src/lib/api/blame.ts +++ b/web/src/lib/api/blame.ts @@ -1,13 +1,10 @@ -import type { ResourceUri } from "@atcute/lexicons"; import { isDid, type Did } from "@atcute/lexicons/syntax"; -import { ClientResponseError, type BobbinContext, type XrpcRequestInit } from "$lib/api/client"; +import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import { resolveEmailToDid } from "$lib/api/emailDid"; import { getBlame } from "$lib/api/gitmirror"; import { resolveMiniDoc } from "$lib/api/identity"; -import { blame as knotBlame } from "$lib/api/knot"; import { didFromSignature, splitMessage } from "$lib/api/repo"; import type * as GetBlame from "$lib/api/lexicons/types/sh/tangled/git/temp2/getBlame"; -import type * as KnotBlame from "$lib/api/lexicons/types/sh/tangled/repo/blame"; import type { Blame, BlameCommit } from "$lib/components/repo/blame/types"; type RunTable = { start: number; count: number; commit: string }; @@ -41,17 +38,6 @@ const toAuthor = ({ name, email }: { name: string; email: string }) => ({ did: didFromSignature(email) }); -const fromKnot = (result: KnotBlame.$output): Blame => - toBlame( - result.commits.map((commit) => ({ - sha: commit.sha, - subject: commit.subject, - when: commit.authoredAt, - author: toAuthor(commit.author) - })), - result.runs - ); - const fromMirror = (result: GetBlame.$output): Blame => toBlame( result.commits.map((commit) => ({ @@ -63,31 +49,13 @@ const fromMirror = (result: GetBlame.$output): Blame => result.runs ); -// the mirror only speaks for what it has fetched: a repo it doesn't mirror is -// RepoNotFound, a commit it hasn't caught up to is RefNotFound, and the knot can -// still answer both -const mirrorMiss = (cause: unknown): boolean => - cause instanceof ClientResponseError && - (cause.error === "RepoNotFound" || cause.error === "RefNotFound"); - -// the mirror keys repos by bobbin's repo did, the knot by the record's at-uri, so -// blame needs both names for the same repo export const blameFor = async ( ctx: BobbinContext, - repo: { repo: ResourceUri; repoDid?: Did }, + repo: Did, ref: string, path: string, init?: XrpcRequestInit -): Promise => { - if (repo.repoDid) { - try { - return fromMirror(await getBlame(ctx, { repo: repo.repoDid, ref, path }, init)); - } catch (cause) { - if (!mirrorMiss(cause)) throw cause; - } - } - return fromKnot(await knotBlame(ctx, { repo: repo.repo, ref, path }, init)); -}; +): Promise => fromMirror(await getBlame(ctx, { repo, ref, path }, init)); const MAX_AUTHOR_HYDRATIONS = 250; const AUTHOR_HYDRATION_CONCURRENCY = 8; diff --git a/web/src/lib/api/diff.ts b/web/src/lib/api/diff.ts index 3c86cc9b..01cc133a 100644 --- a/web/src/lib/api/diff.ts +++ b/web/src/lib/api/diff.ts @@ -1,22 +1,3 @@ -import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; -import { diff as knotDiff } from "$lib/api/knot"; -import type { GitSignature } from "$lib/api/repo"; - -export interface NiceCommit { - hash?: number[]; - // hex form of the hash bytes above - this?: string; - // hex of the first parent, empty for a root commit - parent?: string; - parent_hashes?: number[][]; - author?: GitSignature; - committer?: GitSignature; - message?: string; - tree?: string; - change_id?: string; - pgp_signature?: string; -} - export interface DiffStat { insertions: number; deletions: number; @@ -28,53 +9,3 @@ export interface LineStat { deletions: number; files_changed?: number; } - -export const LineOp = { Context: 0, Delete: 1, Add: 2 } as const; -export type LineOp = (typeof LineOp)[keyof typeof LineOp]; - -export interface DiffLine { - Op: LineOp; - Line: string; -} - -export interface TextFragment { - Comment?: string; - OldPosition?: number; - OldLines?: number; - NewPosition?: number; - NewLines?: number; - LinesAdded?: number; - LinesDeleted?: number; - LeadingContext?: number; - TrailingContext?: number; - Lines?: DiffLine[]; -} - -export interface DiffFile { - name: { old: string; new: string }; - text_fragments?: TextFragment[]; - is_binary?: boolean; - is_new?: boolean; - is_delete?: boolean; - is_copy?: boolean; - is_rename?: boolean; -} - -export interface NiceDiff { - commit?: NiceCommit; - stat?: DiffStat; - diff?: DiffFile[]; -} - -// the new path, or the old one for deletions -export const diffFileName = (file: DiffFile): string => file.name.new || file.name.old; - -export interface RepoCommitResponse { - ref?: string; - diff?: NiceDiff; -} - -// the mirror's temp surface has no diff endpoint, diffs go through -// bobbin's knot proxy -export const diffFor = (ctx: BobbinContext, repo: string, ref: string, init?: XrpcRequestInit) => - knotDiff(ctx, { repo, ref }, init); diff --git a/web/src/lib/api/gitclient.test.ts b/web/src/lib/api/gitclient.test.ts index e4a9126d..8018f777 100644 --- a/web/src/lib/api/gitclient.test.ts +++ b/web/src/lib/api/gitclient.test.ts @@ -28,18 +28,16 @@ describe("gitTarget", () => { const target = gitTarget(config, repo, globalThis.fetch); expect(target.ctx.serviceUrl).toBe("https://bobbin.example"); expect(target.repo).toBe("did:plc:reporepo"); - expect(target.via).toBe("mirror"); }); - it("falls back to bobbin when the repo DID is unknown", () => { - const target = gitTarget(config, { uri: repo.uri }, globalThis.fetch); - expect(target.ctx.serviceUrl).toBe("https://bobbin.example"); - expect(target.repo).toBe(repo.uri); - expect(target.via).toBe("bobbin"); + it("404s when the repo DID is unknown", () => { + expect(() => gitTarget(config, { uri: repo.uri }, globalThis.fetch)).toThrow( + expect.objectContaining({ status: 404 }) + ); }); - it("can require the mirror surface", () => { - expect(knotMirrorTarget(config, repo, globalThis.fetch).via).toBe("mirror"); + it("keys the mirror target by repo DID", () => { + expect(knotMirrorTarget(config, repo, globalThis.fetch).repo).toBe("did:plc:reporepo"); }); }); @@ -62,18 +60,6 @@ describe("resolveDefaultBranch", () => { expect(url.searchParams.get("repo")).toBe(repo.repoDid); }); - it("asks the knot endpoint directly on the bobbin path", async () => { - const fetchMock = vi - .fn() - .mockResolvedValue(jsonResponse({ name: "main", hash: "ccc", when: "" })); - const target = gitTarget(config, { uri: repo.uri }, fetchMock); - - await expect(resolveDefaultBranch(target)).resolves.toBe("main"); - - const url = new URL(String(fetchMock.mock.calls[0][0])); - expect(url.pathname).toBe("/xrpc/sh.tangled.repo.getDefaultBranch"); - }); - it("returns null instead of throwing when the backend is down", async () => { const fetchMock = vi .fn() @@ -86,8 +72,6 @@ describe("resolveDefaultBranch", () => { describe("endpoint routing", () => { const mirrorTarget = (fetchMock: typeof globalThis.fetch) => gitTarget(config, repo, fetchMock); - const bobbinTarget = (fetchMock: typeof globalThis.fetch) => - gitTarget(config, { uri: repo.uri }, fetchMock); const calledUrl = (fetchMock: ReturnType>, call = 0) => new URL(String(fetchMock.mock.calls[call][0])); @@ -105,20 +89,6 @@ describe("endpoint routing", () => { expect(url.searchParams.get("cursor")).toBe("abc"); }); - it("sends log to bobbin's repo.log keyed by at-uri, cursor intact", async () => { - const fetchMock = vi - .fn() - .mockResolvedValue(jsonResponse({ commits: [] })); - - await log(bobbinTarget(fetchMock), { ref: "main", limit: 10, cursor: "abc" }); - - const url = calledUrl(fetchMock); - expect(url.origin).toBe("https://bobbin.example"); - expect(url.pathname).toBe("/xrpc/sh.tangled.repo.log"); - expect(url.searchParams.get("repo")).toBe(repo.uri); - expect(url.searchParams.get("cursor")).toBe("abc"); - }); - it("sends tree to bobbin's getTree with the path param", async () => { const fetchMock = vi .fn() @@ -133,18 +103,6 @@ describe("endpoint routing", () => { expect(url.searchParams.get("path")).toBe("src/lib"); }); - it("sends tree to bobbin's repo.tree", async () => { - const fetchMock = vi - .fn() - .mockResolvedValue(jsonResponse({ files: [] })); - - await tree(bobbinTarget(fetchMock), { ref: "main" }); - - const url = calledUrl(fetchMock); - expect(url.pathname).toBe("/xrpc/sh.tangled.repo.tree"); - expect(url.searchParams.get("repo")).toBe(repo.uri); - }); - it("sends branches and tags to the legacy list endpoints on the mirror", async () => { const fetchMock = vi .fn() @@ -158,16 +116,13 @@ describe("endpoint routing", () => { expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/sh.tangled.git.temp.listTags"); }); - it("routes single-tag reads to getTag on the mirror and repo.tag on bobbin", async () => { + it("routes single-tag reads to getTag on the mirror", async () => { const fetchMock = vi .fn() .mockImplementation(() => Promise.resolve(jsonResponse({ tag: undefined }))); await tag(mirrorTarget(fetchMock), "v1.0.0"); expect(calledUrl(fetchMock).pathname).toBe("/xrpc/sh.tangled.git.temp.getTag"); - - await tag(bobbinTarget(fetchMock), "v1.0.0"); - expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/sh.tangled.repo.tag"); }); it("builds the raw url against the mirror without a raw flag", () => { @@ -179,14 +134,4 @@ describe("endpoint routing", () => { expect(url.searchParams.get("repo")).toBe(repo.repoDid); expect(url.searchParams.has("raw")).toBe(false); }); - - it("builds the raw url against bobbin with raw=true", () => { - const url = new URL( - blobRawUrl(bobbinTarget(globalThis.fetch), { ref: "main", path: "README.md" }) - ); - expect(url.origin).toBe("https://bobbin.example"); - expect(url.pathname).toBe("/xrpc/sh.tangled.repo.blob"); - expect(url.searchParams.get("repo")).toBe(repo.uri); - expect(url.searchParams.get("raw")).toBe("true"); - }); }); diff --git a/web/src/lib/api/gitclient.ts b/web/src/lib/api/gitclient.ts index fae3f247..df312baf 100644 --- a/web/src/lib/api/gitclient.ts +++ b/web/src/lib/api/gitclient.ts @@ -1,3 +1,4 @@ +import { error } from "@sveltejs/kit"; import { createBobbinClient, type BobbinContext } from "$lib/api/client"; import { branches as knotMirrorBranches, @@ -9,26 +10,15 @@ import { tags as knotMirrorTags, tree as knotMirrorTree } from "$lib/api/knotmirror"; -import { - blob as knotBlob, - blobRawUrl as knotBlobRawUrl, - getDefaultBranch, - languages as knotLanguages, - tree as knotTree -} from "$lib/api/knot"; -import { branchesFor, logFor, tagFor, tagsFor } from "$lib/api/repo"; -import type { Did, ResourceUri } from "@atcute/lexicons"; +import type { Did } from "@atcute/lexicons"; export interface GitServiceConfig { bobbinUrl: string; } -// both paths talk to bobbin: "mirror" is the knot mirror's xrpc surface keyed by -// repo DID, which bobbin proxies, "bobbin" is its own knot passthrough keyed by at-uri export interface GitTarget { ctx: BobbinContext; - repo: Did | ResourceUri; - via: "mirror" | "bobbin"; + repo: Did; } export const knotMirrorTarget = ( @@ -37,88 +27,56 @@ export const knotMirrorTarget = ( fetch: typeof globalThis.fetch ): GitTarget => ({ ctx: createBobbinClient({ serviceUrl: config.bobbinUrl, fetch }), - repo: repo.repoDid as Did, - via: "mirror" + repo: repo.repoDid as Did }); export const gitTarget = ( config: GitServiceConfig, repo: { uri: string; repoDid?: string }, fetch: typeof globalThis.fetch -): GitTarget => - repo.repoDid - ? knotMirrorTarget(config, { repoDid: repo.repoDid }, fetch) - : { - ctx: createBobbinClient({ serviceUrl: config.bobbinUrl, fetch }), - repo: repo.uri as ResourceUri, - via: "bobbin" - }; +): GitTarget => { + if (!repo.repoDid) error(404, "This repository has not been indexed yet"); + return knotMirrorTarget(config, { repoDid: repo.repoDid }, fetch); +}; -// the mirror has no getDefaultBranch, the default falls out of the branch -// list's is_default flag instead export const resolveDefaultBranch = ( target: GitTarget, init?: { signal?: AbortSignal } ): Promise => - target.via === "mirror" - ? (async () => { - for (let offset = 0; ; offset += 100) { - const response = await knotMirrorBranches( - target.ctx, - { repo: target.repo, limit: 100, cursor: offset ? `${offset}` : undefined }, - init - ); - const list = response.branches ?? []; - const found = list.find((branch) => branch.is_default)?.reference.name; - if (found) return found; - if (list.length < 100) return null; - } - })().catch(() => null) - : getDefaultBranch(target.ctx, { repo: target.repo }) - .then((branch) => branch.name) - .catch(() => null); + (async () => { + for (let offset = 0; ; offset += 100) { + const response = await knotMirrorBranches( + target.ctx, + { repo: target.repo, limit: 100, cursor: offset ? `${offset}` : undefined }, + init + ); + const list = response.branches ?? []; + const found = list.find((branch) => branch.is_default)?.reference.name; + if (found) return found; + if (list.length < 100) return null; + } + })().catch(() => null); export const tree = (target: GitTarget, params: { ref: string; path?: string }) => - target.via === "mirror" - ? knotMirrorTree(target.ctx, { repo: target.repo, ...params }) - : knotTree(target.ctx, { repo: target.repo, ...params }); + knotMirrorTree(target.ctx, { repo: target.repo, ...params }); export const log = (target: GitTarget, params: { ref: string; limit: number; cursor?: string }) => - target.via === "mirror" - ? knotMirrorLog(target.ctx, { repo: target.repo, ...params }) - : logFor(target.ctx, target.repo, params.ref, params.limit, params.cursor); + knotMirrorLog(target.ctx, { repo: target.repo, ...params }); export const branches = (target: GitTarget, limit: number, cursor?: string) => - target.via === "mirror" - ? knotMirrorBranches(target.ctx, { repo: target.repo, limit, cursor }) - : branchesFor(target.ctx, target.repo, limit, cursor); + knotMirrorBranches(target.ctx, { repo: target.repo, limit, cursor }); export const tags = (target: GitTarget, limit: number, cursor?: string) => - target.via === "mirror" - ? knotMirrorTags(target.ctx, { repo: target.repo, limit, cursor }) - : tagsFor(target.ctx, target.repo, limit, cursor); + knotMirrorTags(target.ctx, { repo: target.repo, limit, cursor }); export const tag = (target: GitTarget, name: string) => - target.via === "mirror" - ? knotMirrorGetTag(target.ctx, { repo: target.repo, tag: name }) - : tagFor(target.ctx, target.repo, name); + knotMirrorGetTag(target.ctx, { repo: target.repo, tag: name }); -export const languages = ( - target: GitTarget, - ref: string -): Promise<{ languages?: { name: string; size: number }[] | null }> => - target.via === "mirror" - ? knotMirrorLanguages(target.ctx, { repo: target.repo, ref }) - : knotLanguages(target.ctx, { repo: target.repo, ref }); +export const languages = (target: GitTarget, ref: string) => + knotMirrorLanguages(target.ctx, { repo: target.repo, ref }); -// only the mirror needs the inline cap, the knot enforces its own and 413s -// past it export const blob = (target: GitTarget, params: { ref: string; path: string }, maxBytes: number) => - target.via === "mirror" - ? knotMirrorBlob(target.ctx, { repo: target.repo, ...params }, maxBytes) - : knotBlob(target.ctx, { repo: target.repo as ResourceUri, ...params }); + knotMirrorBlob(target.ctx, { repo: target.repo, ...params }, maxBytes); export const blobRawUrl = (target: GitTarget, params: { ref: string; path: string }): string => - target.via === "mirror" - ? knotMirrorBlobRawUrl(target.ctx.serviceUrl, { repo: target.repo, ...params }) - : knotBlobRawUrl(target.ctx.serviceUrl, { repo: target.repo, ...params }); + knotMirrorBlobRawUrl(target.ctx.serviceUrl, { repo: target.repo, ...params }); diff --git a/web/src/lib/api/index.ts b/web/src/lib/api/index.ts index 21a4de25..4a2412dd 100644 --- a/web/src/lib/api/index.ts +++ b/web/src/lib/api/index.ts @@ -3,7 +3,6 @@ export * from "$lib/api/client"; export * from "$lib/api/pagination"; export * from "$lib/api/count"; export * from "$lib/api/records"; -export * as knot from "$lib/api/knot"; export * from "$lib/api/search"; export * from "$lib/api/coverage"; export * from "$lib/api/identity"; diff --git a/web/src/lib/api/knot.test.ts b/web/src/lib/api/knot.test.ts deleted file mode 100644 index 8672da6c..00000000 --- a/web/src/lib/api/knot.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import * as knot from "$lib/api/knot"; -import { ClientResponseError, createBobbinClient, type BobbinContext } from "$lib/api/client"; - -const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => - new Response(JSON.stringify(body), { - status: 200, - headers: { "content-type": "application/json" }, - ...init - }); - -const makeCtx = (fetchMock: typeof globalThis.fetch): BobbinContext => - createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); - -describe("knot.archive (binary passthrough)", () => { - it("throws ClientResponseError on a non-2xx archive response", async () => { - const fetchMock = vi - .fn() - .mockResolvedValue(jsonResponse({ error: "UpstreamGone" }, { status: 502 })); - const err = await knot - .archive(makeCtx(fetchMock), { repo: "did:plc:x/r", ref: "main" }) - .catch((e: unknown) => e); - expect(err).toBeInstanceOf(ClientResponseError); - expect((err as ClientResponseError).status).toBe(502); - }); -}); diff --git a/web/src/lib/api/knot.ts b/web/src/lib/api/knot.ts deleted file mode 100644 index 01632e74..00000000 --- a/web/src/lib/api/knot.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { BobbinContext, QueryValue, XrpcRequestInit } from "$lib/api/client"; -import { buildUrl, jsonGet, rawGet } from "$lib/api/_request"; -import type * as Archive from "$lib/api/lexicons/types/sh/tangled/repo/archive"; -import type * as Blame from "$lib/api/lexicons/types/sh/tangled/repo/blame"; -import type * as Blob_ from "$lib/api/lexicons/types/sh/tangled/repo/blob"; -import type * as Branch from "$lib/api/lexicons/types/sh/tangled/repo/branch"; -import type * as Branches from "$lib/api/lexicons/types/sh/tangled/repo/branches"; -import type * as Compare from "$lib/api/lexicons/types/sh/tangled/repo/compare"; -import type * as DescribeRepo from "$lib/api/lexicons/types/sh/tangled/repo/describeRepo"; -import type * as Diff from "$lib/api/lexicons/types/sh/tangled/repo/diff"; -import type * as GetDefaultBranch from "$lib/api/lexicons/types/sh/tangled/repo/getDefaultBranch"; -import type * as Languages from "$lib/api/lexicons/types/sh/tangled/repo/languages"; -import type * as ListSecrets from "$lib/api/lexicons/types/sh/tangled/repo/listSecrets"; -import type * as Log from "$lib/api/lexicons/types/sh/tangled/repo/log"; -import type * as Tag from "$lib/api/lexicons/types/sh/tangled/repo/tag"; -import type * as Tags from "$lib/api/lexicons/types/sh/tangled/repo/tags"; -import type * as Tree from "$lib/api/lexicons/types/sh/tangled/repo/tree"; - -// wrappers for bobbin's knot-proxied repo endpoints. - -const asParams = (params: object): Record => - params as unknown as Record; - -export const tree = (ctx: BobbinContext, params: Tree.$params, init?: XrpcRequestInit) => - jsonGet(ctx, "sh.tangled.repo.tree", asParams(params), init); - -export const blob = (ctx: BobbinContext, params: Blob_.$params, init?: XrpcRequestInit) => - jsonGet(ctx, "sh.tangled.repo.blob", asParams(params), init); - -export const blame = (ctx: BobbinContext, params: Blame.$params, init?: XrpcRequestInit) => - jsonGet(ctx, "sh.tangled.repo.blame", asParams(params), init); - -export const branch = (ctx: BobbinContext, params: Branch.$params, init?: XrpcRequestInit) => - jsonGet(ctx, "sh.tangled.repo.branch", asParams(params), init); - -export const getDefaultBranch = ( - ctx: BobbinContext, - params: GetDefaultBranch.$params, - init?: XrpcRequestInit -) => - jsonGet( - ctx, - "sh.tangled.repo.getDefaultBranch", - asParams(params), - init - ); - -export const describeRepo = ( - ctx: BobbinContext, - params: DescribeRepo.$params, - init?: XrpcRequestInit -) => jsonGet(ctx, "sh.tangled.repo.describeRepo", asParams(params), init); - -export const languages = (ctx: BobbinContext, params: Languages.$params, init?: XrpcRequestInit) => - jsonGet(ctx, "sh.tangled.repo.languages", asParams(params), init); - -export const listSecrets = ( - ctx: BobbinContext, - params: ListSecrets.$params, - init?: XrpcRequestInit -) => jsonGet(ctx, "sh.tangled.repo.listSecrets", asParams(params), init); - -// schema-less json passthroughs; callers supply the shape. - -export const log = (ctx: BobbinContext, params: Log.$params, init?: XrpcRequestInit) => - jsonGet(ctx, "sh.tangled.repo.log", asParams(params), init); - -export const diff = ( - ctx: BobbinContext, - params: Diff.$params, - init?: XrpcRequestInit -) => jsonGet(ctx, "sh.tangled.repo.diff", asParams(params), init); - -export const compare = ( - ctx: BobbinContext, - params: Compare.$params, - init?: XrpcRequestInit -) => jsonGet(ctx, "sh.tangled.repo.compare", asParams(params), init); - -export const branches = ( - ctx: BobbinContext, - params: Branches.$params, - init?: XrpcRequestInit -) => jsonGet(ctx, "sh.tangled.repo.branches", asParams(params), init); - -export const tags = ( - ctx: BobbinContext, - params: Tags.$params, - init?: XrpcRequestInit -) => jsonGet(ctx, "sh.tangled.repo.tags", asParams(params), init); - -export const tag = (ctx: BobbinContext, params: Tag.$params, init?: XrpcRequestInit) => - jsonGet(ctx, "sh.tangled.repo.tag", asParams(params), init); - -// raw response for streaming/download. - -export const archive = (ctx: BobbinContext, params: Archive.$params, init?: XrpcRequestInit) => - rawGet(ctx, "sh.tangled.repo.archive", asParams(params), init); - -// the knot picks the content type and only serves images, video and text, so a -// reader can go straight here instead of us proxying it -export const blobRawUrl = ( - serviceUrl: string, - // not `Blob_.$params`, the record wrappers hand back plain strings and - // nothing here would satisfy the generated at-uri brand - params: { repo: string; ref: string; path: string } -): string => buildUrl(serviceUrl, "sh.tangled.repo.blob", { ...params, raw: true }).toString(); diff --git a/web/src/lib/api/knotmirror.ts b/web/src/lib/api/knotmirror.ts index 15dd502c..945506cc 100644 --- a/web/src/lib/api/knotmirror.ts +++ b/web/src/lib/api/knotmirror.ts @@ -54,6 +54,12 @@ export const formatPatchUrl = ( params: { repo: string; from: string; to: string } ): string => buildUrl(serviceUrl, FORMAT_PATCH_NSID, params).toString(); +export const formatPatch = ( + ctx: BobbinContext, + params: { repo: string; from: string; to: string }, + init?: XrpcRequestInit +) => rawGet(ctx, FORMAT_PATCH_NSID, params, init); + type LastCommit = { hash: string; message: string; diff --git a/web/src/lib/api/rawdiff.test.ts b/web/src/lib/api/rawdiff.test.ts deleted file mode 100644 index df1cb1e1..00000000 --- a/web/src/lib/api/rawdiff.test.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { LineOp, type NiceDiff } from "$lib/api/diff"; -import { renderFormatPatch, renderUnifiedDiff } from "$lib/api/rawdiff"; - -const modifiedFile: NiceDiff = { - diff: [ - { - name: { old: "foo.go", new: "foo.go" }, - text_fragments: [ - { - Comment: "func main()", - OldPosition: 1, - OldLines: 3, - NewPosition: 1, - NewLines: 3, - Lines: [ - { Op: LineOp.Context, Line: "package main\n" }, - { Op: LineOp.Delete, Line: "// old comment\n" }, - { Op: LineOp.Add, Line: "// new comment\n" }, - { Op: LineOp.Context, Line: "func main() {}\n" } - ] - } - ] - } - ] -}; - -describe("renderUnifiedDiff", () => { - it("renders nothing for a missing diff", () => { - expect(renderUnifiedDiff(null)).toBe(""); - expect(renderUnifiedDiff(undefined)).toBe(""); - }); - - it("renders a modified file with the hunk comment", () => { - expect(renderUnifiedDiff(modifiedFile)).toBe( - "diff --git a/foo.go b/foo.go\n" + - "--- a/foo.go\n" + - "+++ b/foo.go\n" + - "@@ -1,3 +1,3 @@ func main()\n" + - " package main\n" + - "-// old comment\n" + - "+// new comment\n" + - " func main() {}\n" - ); - }); - - it("renders a new file", () => { - const got = renderUnifiedDiff({ - diff: [ - { - name: { old: "", new: "new.go" }, - is_new: true, - text_fragments: [ - { - OldPosition: 0, - OldLines: 0, - NewPosition: 1, - NewLines: 2, - Lines: [ - { Op: LineOp.Add, Line: "package main\n" }, - { Op: LineOp.Add, Line: "func main() {}\n" } - ] - } - ] - } - ] - }); - expect(got).toBe( - "diff --git a/new.go b/new.go\n" + - "new file mode 100644\n" + - "--- /dev/null\n" + - "+++ b/new.go\n" + - "@@ -0,0 +1,2 @@\n" + - "+package main\n" + - "+func main() {}\n" - ); - }); - - it("renders a deleted file", () => { - const got = renderUnifiedDiff({ - diff: [ - { - name: { old: "old.go", new: "" }, - is_delete: true, - text_fragments: [ - { - OldPosition: 1, - OldLines: 2, - NewPosition: 0, - NewLines: 0, - Lines: [ - { Op: LineOp.Delete, Line: "package main\n" }, - { Op: LineOp.Delete, Line: "func main() {}\n" } - ] - } - ] - } - ] - }); - expect(got).toBe( - "diff --git a/old.go b/old.go\n" + - "deleted file mode 100644\n" + - "--- a/old.go\n" + - "+++ /dev/null\n" + - "@@ -1,2 +0,0 @@\n" + - "-package main\n" + - "-func main() {}\n" - ); - }); - - it("renders a renamed file with multiple fragments", () => { - const got = renderUnifiedDiff({ - diff: [ - { - name: { old: "old.go", new: "renamed.go" }, - is_rename: true, - text_fragments: [ - { - OldPosition: 1, - OldLines: 2, - NewPosition: 1, - NewLines: 2, - Lines: [ - { Op: LineOp.Context, Line: "package main\n" }, - { Op: LineOp.Delete, Line: "func old() {}\n" }, - { Op: LineOp.Add, Line: "func renamed() {}\n" } - ] - }, - { - Comment: "func init()", - OldPosition: 10, - OldLines: 1, - NewPosition: 10, - NewLines: 1, - Lines: [{ Op: LineOp.Context, Line: "var x = 1\n" }] - } - ] - } - ] - }); - expect(got).toBe( - "diff --git a/old.go b/renamed.go\n" + - "rename from old.go\n" + - "rename to renamed.go\n" + - "--- a/old.go\n" + - "+++ b/renamed.go\n" + - "@@ -1,2 +1,2 @@\n" + - " package main\n" + - "-func old() {}\n" + - "+func renamed() {}\n" + - "@@ -10,1 +10,1 @@ func init()\n" + - " var x = 1\n" - ); - }); - - it("renders multiple files", () => { - const file = (name: string, oldLine: string, newLine: string) => ({ - name: { old: name, new: name }, - text_fragments: [ - { - OldPosition: 1, - OldLines: 1, - NewPosition: 1, - NewLines: 1, - Lines: [ - { Op: LineOp.Delete, Line: `${oldLine}\n` }, - { Op: LineOp.Add, Line: `${newLine}\n` } - ] - } - ] - }); - const got = renderUnifiedDiff({ - diff: [file("a.go", "old a", "new a"), file("b.go", "old b", "new b")] - }); - expect(got).toBe( - "diff --git a/a.go b/a.go\n" + - "--- a/a.go\n" + - "+++ b/a.go\n" + - "@@ -1,1 +1,1 @@\n" + - "-old a\n" + - "+new a\n" + - "diff --git a/b.go b/b.go\n" + - "--- a/b.go\n" + - "+++ b/b.go\n" + - "@@ -1,1 +1,1 @@\n" + - "-old b\n" + - "+new b\n" - ); - }); - - it("marks a missing trailing newline on delete and context lines", () => { - const got = renderUnifiedDiff({ - diff: [ - { - name: { old: "foo.go", new: "foo.go" }, - text_fragments: [ - { - OldPosition: 1, - OldLines: 2, - NewPosition: 1, - NewLines: 2, - Lines: [ - { Op: LineOp.Delete, Line: "old" }, - { Op: LineOp.Add, Line: "new\n" }, - { Op: LineOp.Context, Line: "tail" } - ] - } - ] - } - ] - }); - expect(got).toBe( - "diff --git a/foo.go b/foo.go\n" + - "--- a/foo.go\n" + - "+++ b/foo.go\n" + - "@@ -1,2 +1,2 @@\n" + - "-old\n" + - "\\ No newline at end of file\n" + - "+new\n" + - " tail\n" + - "\\ No newline at end of file\n" - ); - }); -}); - -describe("renderFormatPatch", () => { - it("renders nothing for a missing diff", () => { - expect(renderFormatPatch(null)).toBe(""); - expect(renderFormatPatch(undefined)).toBe(""); - }); - - it("renders the full patch for a rename and a delete", () => { - const got = renderFormatPatch({ - commit: { - hash: [ - 0xab, 0xc1, 0x23, 0x45, 0x67, 0x89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ], - message: "Fix the bug\n\nThis patch resolves the long-standing issue.\n\n\n", - author: { - Name: "Alice Dev", - Email: "alice@example.com", - When: "2024-03-15T10:30:00Z" - } - }, - stat: { files_changed: 2, insertions: 1, deletions: 3 }, - diff: [ - { - name: { old: "old.go", new: "renamed.go" }, - is_rename: true, - text_fragments: [ - { - OldPosition: 1, - OldLines: 2, - NewPosition: 1, - NewLines: 2, - LinesAdded: 1, - LinesDeleted: 1, - Lines: [ - { Op: LineOp.Context, Line: "package main\n" }, - { Op: LineOp.Delete, Line: "func old() {}\n" }, - { Op: LineOp.Add, Line: "func renamed() {}\n" } - ] - } - ] - }, - { - name: { old: "gone.go", new: "" }, - is_delete: true, - text_fragments: [ - { - OldPosition: 1, - OldLines: 2, - NewPosition: 0, - NewLines: 0, - LinesAdded: 0, - LinesDeleted: 2, - Lines: [ - { Op: LineOp.Delete, Line: "package main\n" }, - { Op: LineOp.Delete, Line: "func main() {}\n" } - ] - } - ] - } - ] - }); - expect(got).toBe( - "From abc1234567890000000000000000000000000000 Mon Sep 17 00:00:00 2001\n" + - "From: Alice Dev \n" + - "Date: Fri, 15 Mar 2024 10:30:00 +0000\n" + - "Subject: [PATCH] Fix the bug\n" + - "\n" + - "This patch resolves the long-standing issue.\n" + - "---\n" + - " renamed.go | 2 +-\n" + - " gone.go | 2 --\n" + - " 2 file(s) changed, 1 insertion(s)(+), 3 deletion(s)(-)\n" + - "\n" + - "diff --git a/old.go b/renamed.go\n" + - "rename from old.go\n" + - "rename to renamed.go\n" + - "--- a/old.go\n" + - "+++ b/renamed.go\n" + - "@@ -1,2 +1,2 @@\n" + - " package main\n" + - "-func old() {}\n" + - "+func renamed() {}\n" + - "diff --git a/gone.go b/gone.go\n" + - "deleted file mode 100644\n" + - "--- a/gone.go\n" + - "+++ /dev/null\n" + - "@@ -1,2 +0,0 @@\n" + - "-package main\n" + - "-func main() {}\n" + - "\n--\ntangled.sh\n" - ); - }); - - it("omits the body for a single-line message and uses the zero time for a missing When", () => { - const got = renderFormatPatch({ - commit: { - message: "Single line commit", - author: { Name: "", Email: "", When: "" } - } - }); - expect(got).toBe( - "From Mon Sep 17 00:00:00 2001\n" + - "From: <>\n" + - "Date: Mon, 01 Jan 0001 00:00:00 +0000\n" + - "Subject: [PATCH] Single line commit\n" + - "\n" + - "---\n" + - " 0 file(s) changed, 0 insertion(s)(+), 0 deletion(s)(-)\n" + - "\n" + - "\n--\ntangled.sh\n" - ); - }); - - it("uses the zero time for an unparseable When", () => { - const got = renderFormatPatch({ - commit: { - this: "abc123", - message: "x", - author: { Name: "A", Email: "a@b.c", When: "not a date" } - } - }); - expect(got).toContain("Date: Mon, 01 Jan 0001 00:00:00 +0000\n"); - }); - - it("pads the year to four digits", () => { - const got = renderFormatPatch({ - commit: { - this: "abc123", - message: "x", - author: { Name: "A", Email: "a@b.c", When: "0999-06-15T10:30:00Z" } - } - }); - expect(got).toContain("Date: Sat, 15 Jun 0999 10:30:00 +0000\n"); - }); -}); diff --git a/web/src/lib/api/rawdiff.ts b/web/src/lib/api/rawdiff.ts deleted file mode 100644 index 94237528..00000000 --- a/web/src/lib/api/rawdiff.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { diffFileName, type DiffFile, type NiceDiff, type TextFragment } from "$lib/api/diff"; -import { hexFromBytes } from "$lib/api/repo"; - -// port of appview/repo/rawdiff.go - -const OP_CHARS = [" ", "-", "+"] as const; - -const opChar = (op: number): string => OP_CHARS[op] ?? "?"; - -const noEol = (line: string): boolean => line.length === 0 || !line.endsWith("\n"); - -const fragmentHeader = (fragment: TextFragment): string => { - const header = `@@ -${fragment.OldPosition ?? 0},${fragment.OldLines ?? 0} +${fragment.NewPosition ?? 0},${fragment.NewLines ?? 0} @@`; - return fragment.Comment ? `${header} ${fragment.Comment}` : header; -}; - -const fragmentString = (fragment: TextFragment): string => { - let out = `${fragmentHeader(fragment)}\n`; - for (const line of fragment.Lines ?? []) { - out += opChar(line.Op) + line.Line; - if (noEol(line.Line)) out += "\n\\ No newline at end of file\n"; - } - return out; -}; - -export const fileStats = (file: DiffFile): { insertions: number; deletions: number } => { - let insertions = 0; - let deletions = 0; - for (const fragment of file.text_fragments ?? []) { - insertions += fragment.LinesAdded ?? 0; - deletions += fragment.LinesDeleted ?? 0; - } - return { insertions, deletions }; -}; - -export const renderUnifiedDiff = (niceDiff: NiceDiff | null | undefined): string => { - if (!niceDiff) return ""; - let out = ""; - for (const file of niceDiff.diff ?? []) { - const oldName = file.name.old || file.name.new; - const newName = file.name.new || file.name.old; - - out += `diff --git a/${oldName} b/${newName}\n`; - if (file.is_new) { - out += "new file mode 100644\n"; - out += "--- /dev/null\n"; - out += `+++ b/${newName}\n`; - } else if (file.is_delete) { - out += "deleted file mode 100644\n"; - out += `--- a/${oldName}\n`; - out += "+++ /dev/null\n"; - } else if (file.is_rename) { - out += `rename from ${oldName}\n`; - out += `rename to ${newName}\n`; - out += `--- a/${oldName}\n`; - out += `+++ b/${newName}\n`; - } else { - out += `--- a/${oldName}\n`; - out += `+++ b/${newName}\n`; - } - - for (const fragment of file.text_fragments ?? []) { - out += fragmentString(fragment); - } - } - return out; -}; - -const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const; -const MONTHS = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" -] as const; - -const pad = (value: number): string => String(value).padStart(2, "0"); - -// RFC1123Z, always utc -const formatPatchDate = (input: string): string => { - const date = new Date(input); - // a missing When is go's zero time, formats to this exact string - if (Number.isNaN(date.getTime())) return "Mon, 01 Jan 0001 00:00:00 +0000"; - return ( - `${WEEKDAYS[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${MONTHS[date.getUTCMonth()]} ` + - `${String(date.getUTCFullYear()).padStart(4, "0")} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} +0000` - ); -}; - -export const renderFormatPatch = (niceDiff: NiceDiff | null | undefined): string => { - if (!niceDiff) return ""; - const commit = niceDiff.commit ?? {}; - const message = commit.message ?? ""; - - // format-patch takes the first LINE as the subject, unlike splitMessage's - // first paragraph - const newline = message.indexOf("\n"); - const subject = newline === -1 ? message : message.slice(0, newline); - - const separator = message.indexOf("\n\n"); - const body = separator === -1 ? "" : message.slice(separator + 2).replace(/\n+$/, ""); - - const hash = commit.this || hexFromBytes(commit.hash ?? []); - - let out = `From ${hash} Mon Sep 17 00:00:00 2001\n`; - out += `From: ${commit.author?.Name ?? ""} <${commit.author?.Email ?? ""}>\n`; - out += `Date: ${formatPatchDate(commit.author?.When ?? "")}\n`; - out += `Subject: [PATCH] ${subject}\n`; - out += "\n"; - if (body !== "") out += `${body}\n`; - out += "---\n"; - - for (const file of niceDiff.diff ?? []) { - const name = diffFileName(file); - const { insertions, deletions } = fileStats(file); - out += ` ${name} | ${insertions + deletions} ${"+".repeat(insertions)}${"-".repeat(deletions)}\n`; - } - const stat = niceDiff.stat ?? { insertions: 0, deletions: 0, files_changed: 0 }; - out += ` ${stat.files_changed} file(s) changed, ${stat.insertions} insertion(s)(+), ${stat.deletions} deletion(s)(-)\n\n`; - - out += renderUnifiedDiff(niceDiff); - out += "\n--\ntangled.sh\n"; - return out; -}; diff --git a/web/src/lib/api/repo.test.ts b/web/src/lib/api/repo.test.ts index e94a1400..9921ad1d 100644 --- a/web/src/lib/api/repo.test.ts +++ b/web/src/lib/api/repo.test.ts @@ -2,14 +2,12 @@ import { describe, expect, it, vi } from "vitest"; import { coAuthorsFrom, importState, - logFor, repoNameOf, resolveForkRepoLabels, resolveRepoByName, resolveRepoByRepoDid, sortTreeEntries, toBranchSummary, - toCommitDetail, toCommitSummary, toTagSummary, toTreeEntrySummary, @@ -20,7 +18,6 @@ import { type TreeEntrySummary } from "$lib/api/repo"; import { ClientResponseError, createBobbinClient, type BobbinContext } from "$lib/api/client"; -import type { NiceCommit } from "$lib/api/diff"; import type { RecordView, RepoRecord } from "$lib/api/records"; const jsonResponse = (body: unknown): Response => @@ -112,48 +109,6 @@ describe("toCommitSummary", () => { }); }); -describe("toCommitDetail", () => { - const commit: NiceCommit = { - this: "0c4d0e9b07940033721395a434b5873f0fb9e6c8", - author: { Name: "Ada", Email: "ada@example.com", When: "2026-07-01T10:00:00Z" }, - committer: { Name: "Grace", Email: "grace@example.com", When: "2026-07-02T10:00:00Z" }, - message: - "web: add commit page\n\nA longer body.\n\nCo-authored-by: Alan Turing \n" - }; - - it("carries both signatures and stamps co-authors with the committer's When", () => { - expect(toCommitDetail(commit)).toEqual({ - hash: "0c4d0e9b07940033721395a434b5873f0fb9e6c8", - shortHash: "0c4d0e9b", - subject: "web: add commit page", - body: "A longer body.\n\nCo-authored-by: Alan Turing ", - authorName: "Ada", - authorEmail: "ada@example.com", - authorWhen: "2026-07-01T10:00:00Z", - committerName: "Grace", - committerEmail: "grace@example.com", - committerWhen: "2026-07-02T10:00:00Z", - coAuthors: [{ name: "Alan Turing", email: "alan@example.com" }] - }); - }); - - it("fills empty strings for a bare commit", () => { - expect(toCommitDetail({})).toEqual({ - hash: "", - shortHash: "", - subject: "", - body: "", - authorName: "", - authorEmail: "", - authorWhen: "", - committerName: "", - committerEmail: "", - committerWhen: "", - coAuthors: [] - }); - }); -}); - describe("coAuthorsFrom", () => { it("stamps every co-author with the passed When, like go's Commit.CoAuthors", () => { const message = @@ -173,24 +128,10 @@ describe("coAuthorsFrom", () => { }); }); -describe("logFor", () => { - it("passes the cursor through to the knot", async () => { - const fetchMock = vi - .fn() - .mockResolvedValue(jsonResponse({ commits: [], total: 42 })); - await logFor(makeCtx(fetchMock), "at://did:plc:o/sh.tangled.repo/abc", "master", 20, "40"); - const url = new URL(String(fetchMock.mock.calls[0][0])); - expect(url.pathname).toBe("/xrpc/sh.tangled.repo.log"); - expect(url.searchParams.get("ref")).toBe("master"); - expect(url.searchParams.get("limit")).toBe("20"); - expect(url.searchParams.get("cursor")).toBe("40"); - }); -}); - describe("importState", () => { const repo = { uri: "at://did:plc:o/sh.tangled.repo/abc", repoDid: "did:plc:repo" }; const describeResponse = (body: Record) => - jsonResponse({ ownerDid: "did:plc:o", rkey: "abc", ...body }); + jsonResponse({ ownerDid: "did:plc:o", repoDid: repo.repoDid, rkey: "abc", ...body }); it("sends the at-uri for bobbin's proxy and repoDid for the knot", async () => { const fetchMock = vi.fn().mockResolvedValue(describeResponse({})); diff --git a/web/src/lib/api/repo.ts b/web/src/lib/api/repo.ts index 30f69d56..7d193020 100644 --- a/web/src/lib/api/repo.ts +++ b/web/src/lib/api/repo.ts @@ -1,17 +1,15 @@ import { isDid } from "@atcute/lexicons/syntax"; import { ClientResponseError, type BobbinContext, type XrpcRequestInit } from "$lib/api/client"; -import type { LineStat, NiceCommit } from "$lib/api/diff"; +import type { LineStat } from "$lib/api/diff"; import { REPO_OWNER_DOCS } from "$lib/api/descriptors"; import { authorOf, enrich, target } from "$lib/api/enrich"; import { getRepoByName, type RecordList, type RecordView, type RepoRecord } from "$lib/api/records"; +import { ok } from "@atcute/client"; import { - branches as knotBranches, - describeRepo as knotDescribeRepo, - log as knotLog, - tag as knotTag, - tags as knotTags -} from "$lib/api/knot"; -import type * as DescribeRepo from "$lib/api/lexicons/types/sh/tangled/repo/describeRepo"; + mainSchema as describeRepoSchema, + type $output as DescribeRepoOutput, + type $params as DescribeRepoParams +} from "$lib/api/lexicons/types/sh/tangled/repo/describeRepo"; import { httpStatusFor } from "$lib/api/load"; import { didFromUri, rkeyFromUri } from "$lib/api/uri"; import type * as Tree from "$lib/api/lexicons/types/sh/tangled/repo/tree"; @@ -293,31 +291,6 @@ export interface CommitDetail { authorHandle?: string; } -// the diff endpoint's commit carries both signatures, unlike the log's -// summary shape -export const toCommitDetail = (commit: NiceCommit): CommitDetail => { - const [subject, body] = splitMessage(commit.message ?? ""); - const hash = commit.this ?? ""; - const committerWhen = commit.committer?.When ?? ""; - return { - hash, - shortHash: hash.slice(0, 8), - subject, - body, - authorName: commit.author?.Name ?? "", - authorEmail: commit.author?.Email ?? "", - authorDid: didFromSignature(commit.author?.Email ?? ""), - authorWhen: commit.author?.When ?? "", - committerName: commit.committer?.Name ?? "", - committerEmail: commit.committer?.Email ?? "", - committerWhen, - coAuthors: coAuthorsFrom(commit.message ?? "", committerWhen).map(({ Name, Email }) => ({ - name: Name, - email: Email - })) - }; -}; - // the tree endpoint uses lexicon casing where the log ones pass through go field // names, so this cannot share `toCommitSummary` export const toTreeCommitSummary = (commit: { @@ -450,35 +423,6 @@ export const sortTreeEntries = (entries: TreeEntrySummary[]): TreeEntrySummary[] return a.name.localeCompare(b.name); }); -// the log cursor is a numeric offset encoded as a string -export const logFor = ( - ctx: BobbinContext, - repo: string, - ref: string, - limit: number, - cursor?: string, - init?: XrpcRequestInit -) => knotLog(ctx, { repo, ref, limit, cursor }, init); - -export const branchesFor = ( - ctx: BobbinContext, - repo: string, - limit: number, - cursor?: string, - init?: XrpcRequestInit -) => knotBranches(ctx, { repo, limit, cursor }, init); - -export const tagsFor = ( - ctx: BobbinContext, - repo: string, - limit: number, - cursor?: string, - init?: XrpcRequestInit -) => knotTags(ctx, { repo, limit, cursor }, init); - -export const tagFor = (ctx: BobbinContext, repo: string, tag: string, init?: XrpcRequestInit) => - knotTag(ctx, { repo, tag }, init); - type ImportContent = "fetching" | "partial" | "failed"; export interface ImportState { @@ -492,11 +436,12 @@ const describeRepo = ( ctx: BobbinContext, repo: { uri: string; repoDid?: string }, init?: XrpcRequestInit -): Promise => - knotDescribeRepo( - ctx, - { repo: repo.uri, repoDid: repo.repoDid ?? "" } as DescribeRepo.$params, - init +): Promise => + ok( + ctx.xrpc.call(describeRepoSchema, { + params: { repo: repo.uri, repoDid: repo.repoDid ?? "" } as DescribeRepoParams, + ...init + }) ); export const importState = async ( diff --git a/web/src/lib/api/repoDiff.test.ts b/web/src/lib/api/repoDiff.test.ts index ca6c68c0..b7b4ff4f 100644 --- a/web/src/lib/api/repoDiff.test.ts +++ b/web/src/lib/api/repoDiff.test.ts @@ -46,7 +46,14 @@ const harness = (diffs: MirrorFileDiff[], contents: Record) => { const key = `${url.searchParams.get("ref")}:${url.searchParams.get("path")}`; const content = contents[key]; if (content === undefined) return new Response("nope", { status: 404 }); - return json({ path: url.searchParams.get("path"), content, encoding: "utf-8", size: 1 }); + if (url.pathname.endsWith("getEntry")) { + return json({ + name: url.searchParams.get("path"), + mode: "100644", + size: content.length + }); + } + return new Response(content); }); const deps = { @@ -54,7 +61,7 @@ const harness = (diffs: MirrorFileDiff[], contents: Record) => { }; const git = gitTarget( { bobbinUrl: "https://bobbin.test" }, - { uri: "at://did:plc:alice/sh.tangled.repo/core" }, + { uri: "at://did:plc:alice/sh.tangled.repo/core", repoDid: "did:plc:repo" }, fetchMock ); return { deps: { ...deps, git }, calls }; @@ -67,7 +74,7 @@ const interdiffSpec = { version: { base: "v2base", head: "v2head" } } as const; -const blobCalls = (calls: string[]) => calls.filter((path) => path.endsWith("repo.blob")); +const blobCalls = (calls: string[]) => calls.filter((path) => path.endsWith("getBlob")); describe("loadDiff", () => { it("describes every file without reading a single blob", async () => { diff --git a/web/src/lib/components/repo/CommitView.stories.svelte b/web/src/lib/components/repo/CommitView.stories.svelte index 44441dec..2aa14a3f 100644 --- a/web/src/lib/components/repo/CommitView.stories.svelte +++ b/web/src/lib/components/repo/CommitView.stories.svelte @@ -1,19 +1,19 @@