diff --git a/web/src/lib/api/gitclient.test.ts b/web/src/lib/api/gitclient.test.ts index 3d09edd5a..503cbd769 100644 --- a/web/src/lib/api/gitclient.test.ts +++ b/web/src/lib/api/gitclient.test.ts @@ -70,18 +70,18 @@ describe("endpoint routing", () => { const calledUrl = (fetchMock: ReturnType>, call = 0) => new URL(String(fetchMock.mock.calls[call][0])); - it("sends log to the mirror's listCommits keyed by DID, cursor intact", async () => { + it("sends log to the mirror's listCommits keyed by DID, skip intact", async () => { const fetchMock = vi .fn() .mockResolvedValue(jsonResponse({ commits: [] })); - await log(mirrorTarget(fetchMock), { ref: "main", limit: 10, cursor: "abc" }); + await log(mirrorTarget(fetchMock), { ref: "main", limit: 10, skip: 20 }); const url = calledUrl(fetchMock); expect(url.origin).toBe("https://bobbin.example"); - expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.listCommits"); + expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp2.listCommits"); expect(url.searchParams.get("repo")).toBe(repo.repoDid); - expect(url.searchParams.get("cursor")).toBe("abc"); + expect(url.searchParams.get("skip")).toBe("20"); }); it("sends tree to bobbin's getTree with the path param", async () => { diff --git a/web/src/lib/api/gitclient.ts b/web/src/lib/api/gitclient.ts index ae6ef4e4a..bfa10cf97 100644 --- a/web/src/lib/api/gitclient.ts +++ b/web/src/lib/api/gitclient.ts @@ -5,11 +5,11 @@ import { blob as knotMirrorBlob, blobRawUrl as knotMirrorBlobRawUrl, languages as knotMirrorLanguages, - log as knotMirrorLog, getRef as knotMirrorGetRef, tags as knotMirrorTags, tree as knotMirrorTree } from "$lib/api/knotmirror"; +import { listCommits } from "$lib/api/gitmirror"; import type { Did } from "@atcute/lexicons"; export interface GitServiceConfig { @@ -50,8 +50,16 @@ export const resolveDefaultBranch = ( export const tree = (target: GitTarget, params: { ref: string; path?: string }) => knotMirrorTree(target.ctx, { repo: target.repo, ...params }); -export const log = (target: GitTarget, params: { ref: string; limit: number; cursor?: string }) => - knotMirrorLog(target.ctx, { repo: target.repo, ...params }); +export const log = (target: GitTarget, params: { ref: string; limit: number; skip?: number }) => + listCommits(target.ctx, { + repo: target.repo, + ranges: [params.ref], + limit: params.limit, + skip: params.skip + }); + +export const branch = (target: GitTarget, name: string) => + knotMirrorGetRef(target.ctx, { repo: target.repo, ref: name }); export const branches = (target: GitTarget, limit: number, cursor?: string) => knotMirrorBranches(target.ctx, { repo: target.repo, limit, cursor }); diff --git a/web/src/lib/api/gitmirror.ts b/web/src/lib/api/gitmirror.ts index bb3647896..e6778f739 100644 --- a/web/src/lib/api/gitmirror.ts +++ b/web/src/lib/api/gitmirror.ts @@ -138,6 +138,7 @@ export const toMirrorCommitSummary = (commit: MirrorCommit): CommitSummary => { body, authorName: commit.author.name, authorEmail: commit.author.email, + authorDid: didFromSignature(commit.author.email), when: commit.committer.when ?? commit.author.when, // the pull page links a commit row as `firstParent..commit` parent: commit.parents[0], diff --git a/web/src/lib/api/knotmirror.ts b/web/src/lib/api/knotmirror.ts index a8fb08b6d..6ccde3c72 100644 --- a/web/src/lib/api/knotmirror.ts +++ b/web/src/lib/api/knotmirror.ts @@ -8,7 +8,7 @@ import type * as MirrorListBranches from "$lib/api/lexicons/types/org/tangled/te import type * as MirrorListTags from "$lib/api/lexicons/types/org/tangled/temp/git/listTags"; import type * as MirrorTree from "$lib/api/lexicons/types/org/tangled/temp/git/getTree"; import type * as Tree from "$lib/api/lexicons/types/sh/tangled/repo/tree"; -import { treeEntryKind, type LogResponse } from "$lib/api/repo"; +import { treeEntryKind } from "$lib/api/repo"; const TREE_NSID = "org.tangled.temp.git.getTree"; const ENTRY_NSID = "org.tangled.temp.git.getEntry"; @@ -19,7 +19,6 @@ const GET_REF_NSID = "org.tangled.temp.git.getRef"; const BLOB_NSID = "org.tangled.temp.git.getBlob"; const FORMAT_PATCH_NSID = "sh.tangled.git.temp.formatPatch"; -const LOG_NSID = "sh.tangled.git.temp.listCommits"; const LANGUAGES_NSID = "sh.tangled.git.temp.listLanguages"; const MAX_README_BYTES = 1 << 20; @@ -175,12 +174,6 @@ export const tree = async ( } }; -export const log = ( - ctx: BobbinContext, - params: { repo: string; ref: string; limit: number; cursor?: string }, - init?: XrpcRequestInit -) => jsonGet(ctx, LOG_NSID, params, init); - export const branches = ( ctx: BobbinContext, params: { repo: string; limit: number; cursor?: string }, diff --git a/web/src/lib/api/repo.test.ts b/web/src/lib/api/repo.test.ts index 8a52cfe9c..f0432e51d 100644 --- a/web/src/lib/api/repo.test.ts +++ b/web/src/lib/api/repo.test.ts @@ -8,11 +8,9 @@ import { resolveRepoByRepoDid, sortTreeEntries, toBranchSummary, - toCommitSummary, toTagSummary, toTreeEntrySummary, treeEntryKind, - type LogCommit, type TreeEntrySummary } from "$lib/api/repo"; import { ClientResponseError, createBobbinClient, type BobbinContext } from "$lib/api/client"; @@ -77,36 +75,6 @@ describe("sortTreeEntries", () => { }); }); -describe("toCommitSummary", () => { - // `this` is where the hex hash lives, `hash` is a byte array on the wire - const commit: LogCommit = { - this: "0c4d0e9b07940033721395a434b5873f0fb9e6c8", - author: { Name: "Ada", Email: "ada@example.com", When: "2026-07-01T10:00:00Z" }, - committer: { Name: "Ada", Email: "ada@example.com", When: "2026-07-02T10:00:00Z" }, - message: "web: add repo index\n\nWith a longer body.\n", - change_id: "abc123" - }; - - it("splits the subject from the body and shortens the hash", () => { - const summary = toCommitSummary(commit); - expect(summary).toMatchObject({ - hash: "0c4d0e9b07940033721395a434b5873f0fb9e6c8", - shortHash: "0c4d0e9b", - subject: "web: add repo index", - body: "With a longer body.", - authorName: "Ada", - when: "2026-07-02T10:00:00Z", - changeId: "abc123" - }); - }); - - it("leaves the body empty for single-line messages", () => { - const summary = toCommitSummary({ ...commit, message: "one liner\n" }); - expect(summary.body).toBe(""); - expect(summary.subject).toBe("one liner"); - }); -}); - describe("coAuthorsFrom", () => { it("stamps every co-author with the passed When, like go's Commit.CoAuthors", () => { const message = diff --git a/web/src/lib/api/repo.ts b/web/src/lib/api/repo.ts index e358c74f2..f7d54ec18 100644 --- a/web/src/lib/api/repo.ts +++ b/web/src/lib/api/repo.ts @@ -16,10 +16,6 @@ import type * as Tree from "$lib/api/lexicons/types/sh/tangled/repo/tree"; import type * as ListBranches from "$lib/api/lexicons/types/org/tangled/temp/git/listBranches"; import type * as ListTags from "$lib/api/lexicons/types/org/tangled/temp/git/listTags"; -// log, branches and tags are `*/*` in the lexicons, so these shapes are copied -// from core/types by hand. anything go-git touches keeps its go field names and -// writes hashes as byte arrays, so the hex comes from a sibling field - export interface GitSignature { Name: string; Email: string; @@ -39,24 +35,6 @@ export const parseRawCommit = (spec: string): { ref: string; format: "patch" | " return FULL_HASH_RE.test(ref) ? { ref, format } : null; }; -// `this` and `parent` are the hex hashes, `hash` is the byte array -export interface LogCommit { - this?: string; - parent?: string; - author?: GitSignature; - committer?: GitSignature; - message?: string; - tree?: string; - change_id?: string; -} - -export interface LogResponse { - commits?: LogCommit[]; - ref?: string; - total?: number; - page?: number; -} - // newer repos get tid rkeys and keep their display name in the record export const repoNameOf = (view: RecordView): string => view.value.name ?? rkeyFromUri(view.uri); @@ -216,23 +194,6 @@ export const signOffsFrom = (body: string): SignOff[] => { export const hasBodyBeyondSignOffs = (body: string): boolean => body.split("\n").some((raw) => raw.trim() !== "" && !signOffLine.test(raw.trim())); -export const toCommitSummary = (commit: LogCommit): CommitSummary => { - const [subject, body] = splitMessage(commit.message ?? ""); - const hash = commit.this ?? ""; - const authorEmail = commit.author?.Email ?? ""; - return { - hash, - shortHash: hash.slice(0, 8), - subject, - body, - authorName: commit.author?.Name ?? "", - authorEmail, - authorDid: didFromSignature(authorEmail), - when: commit.committer?.When ?? commit.author?.When ?? "", - changeId: commit.change_id - }; -}; - export interface CommitDetail { hash: string; shortHash: string; @@ -250,8 +211,6 @@ export interface CommitDetail { authorHandle?: string; } -// the tree endpoint uses lexicon casing where the log ones pass through go field -// names, so this cannot share `toCommitSummary` export const toTreeCommitSummary = (commit: { hash: string; message?: string; diff --git a/web/src/lib/api/repoIndex.ts b/web/src/lib/api/repoIndex.ts index ad84720fb..c270cdc30 100644 --- a/web/src/lib/api/repoIndex.ts +++ b/web/src/lib/api/repoIndex.ts @@ -3,6 +3,7 @@ import { ClientResponseError, createBobbinClient } from "$lib/api/client"; import { resolveEmailToDid } from "$lib/api/emailDid"; import { resolveMiniDoc } from "$lib/api/identity"; import { + branch as gitBranch, branches as gitBranches, gitTarget, type GitTarget, @@ -11,12 +12,12 @@ import { tags as gitTags, tree as gitTree } from "$lib/api/gitclient"; +import { toMirrorCommitSummary } from "$lib/api/gitmirror"; import { orNull, parallel, stream } from "$lib/api/load"; import { branchSummaries, sortTreeEntries, tagsByCommitHash, - toCommitSummary, toTagSummary, toTreeCommitSummary, toTreeEntrySummary @@ -201,6 +202,7 @@ export const loadRepoIndex = ( const results = await parallel({ tree: attempt(gitTree(git, { ref: targetRef })), log: attempt(gitLog(git, { ref: targetRef, limit: COMMIT_LIMIT })), + branch: attempt(gitBranch(git, targetRef)), branches: attempt(gitBranches(git, REF_LIMIT)), tags: attempt(gitTags(git, REF_LIMIT)) }); @@ -208,7 +210,7 @@ export const loadRepoIndex = ( const branches = results.branches.value ? branchSummaries(results.branches.value) : []; const tags = (results.tags.value?.tags ?? []).map(toTagSummary); const commits = await withAuthorHandles( - (results.log.value?.commits ?? []).map(toCommitSummary), + (results.log.value?.commits ?? []).map(toMirrorCommitSummary), parent.publicConfig.bobbinUrl, event.fetch ); @@ -232,7 +234,7 @@ export const loadRepoIndex = ( readme: readmeOf(results.tree.value), commits, tagsByCommit: tagsByCommitHash(commits, tags), - totalCommits: results.log.value?.total ?? commits.length, + totalCommits: results.branch.value?.totalCommits ?? commits.length, branches: branches.slice(0, BRANCH_LIMIT), totalBranches: results.branches.value?.total ?? branches.length, tags: tags.slice(0, TAG_LIMIT), diff --git a/web/src/routes/[handle]/[repo]/commits/[ref]/+page.ts b/web/src/routes/[handle]/[repo]/commits/[ref]/+page.ts index da5544666..766ccde65 100644 --- a/web/src/routes/[handle]/[repo]/commits/[ref]/+page.ts +++ b/web/src/routes/[handle]/[repo]/commits/[ref]/+page.ts @@ -1,6 +1,7 @@ -import { branches, gitTarget, log, tags } from "$lib/api/gitclient"; +import { branch, branches, gitTarget, log, tags } from "$lib/api/gitclient"; +import { toMirrorCommitSummary } from "$lib/api/gitmirror"; import { REF_LIMIT, withAuthorHandles } from "$lib/api/repoIndex"; -import { tagsByCommitHash, toBranchSummary, toCommitSummary, toTagSummary } from "$lib/api/repo"; +import { tagsByCommitHash, toBranchSummary, toTagSummary } from "$lib/api/repo"; import { settle, stream } from "$lib/api/load"; import { discoveredPageCount, offsetFor, pageCount, pageParam } from "$lib/pagination"; import type { PageLoad } from "./$types"; @@ -13,25 +14,26 @@ export const load: PageLoad = async (event) => { const ref = event.params.ref; const page = pageParam(event.url); - const offset = offsetFor(page, COMMIT_LIMIT); - const cursor = offset !== undefined ? String(offset) : undefined; + const skip = offsetFor(page, COMMIT_LIMIT); const logPromise = repoPromise.then((repo) => { const git = gitTarget(parent.publicConfig, repo, event.fetch); - return log(git, { ref, limit: COMMIT_LIMIT, cursor }); + return log(git, { ref, limit: COMMIT_LIMIT, skip }); }); + const totalPromise = repoPromise + .then((repo) => branch(gitTarget(parent.publicConfig, repo, event.fetch), ref)) + .then((res) => res.totalCommits) + .catch(() => 0); const commits = logPromise.then((res) => withAuthorHandles( - (res.commits ?? []).map(toCommitSummary), + res.commits.map(toMirrorCommitSummary), parent.publicConfig.bobbinUrl, event.fetch ) ); - const totalCommits = logPromise.then((res) => res.total ?? 0); - const pages = logPromise.then((res) => { - const total = res.total ?? 0; - const count = res.commits?.length ?? 0; + const pages = Promise.all([logPromise, totalPromise]).then(([res, total]) => { + const count = res.commits.length; return total > 0 ? pageCount(total, COMMIT_LIMIT) : discoveredPageCount(page, count === COMMIT_LIMIT); @@ -66,7 +68,7 @@ export const load: PageLoad = async (event) => { page, pageCount: stream(pages), commits: stream(commits), - totalCommits: stream(totalCommits), + totalCommits: stream(totalPromise), tagsByCommit: stream(tagsByCommit) }); }; diff --git a/web/src/routes/[handle]/[repo]/history/[ref]/[...path]/+page.ts b/web/src/routes/[handle]/[repo]/history/[ref]/[...path]/+page.ts index 2e7d4f2f4..d25c78cab 100644 --- a/web/src/routes/[handle]/[repo]/history/[ref]/[...path]/+page.ts +++ b/web/src/routes/[handle]/[repo]/history/[ref]/[...path]/+page.ts @@ -3,7 +3,7 @@ import { gitTarget, log } from "$lib/api/gitclient"; import { authorsOf, mockFileHistory } from "$lib/api/fileHistory"; import { settle, stream } from "$lib/api/load"; import { withAuthorHandles } from "$lib/api/repoIndex"; -import { toCommitSummary } from "$lib/api/repo"; +import { toMirrorCommitSummary } from "$lib/api/gitmirror"; import { pageCount, pageParam } from "$lib/pagination"; import type { PageLoad } from "./$types"; @@ -26,7 +26,7 @@ export const load: PageLoad = async (event) => { const sampled = await log(git, { ref, limit: AUTHOR_SAMPLE }) .then((res) => withAuthorHandles( - (res.commits ?? []).map(toCommitSummary), + res.commits.map(toMirrorCommitSummary), parent.publicConfig.bobbinUrl, event.fetch )