diff --git a/web/src/fixtures/pull.ts b/web/src/fixtures/pull.ts index 31f96a77c..c13a39572 100644 --- a/web/src/fixtures/pull.ts +++ b/web/src/fixtures/pull.ts @@ -301,11 +301,11 @@ export const mockPullFetch = (options: PullFetchOptions = {}): typeof fetch => return notFound(); } - case "org.tangled.temp.git.getBranch": + case "org.tangled.temp.git.getRef": return json({ - reference: { name: param("name") }, - hash: - param("name") === sourceBranch + name: param("ref"), + oid: + param("ref") === `refs/heads/${sourceBranch}` ? (options.sourceHead ?? pullHead) : targetHead }); diff --git a/web/src/lib/api/gitclient.test.ts b/web/src/lib/api/gitclient.test.ts index 8018f777b..3d09edd5a 100644 --- a/web/src/lib/api/gitclient.test.ts +++ b/web/src/lib/api/gitclient.test.ts @@ -42,21 +42,16 @@ describe("gitTarget", () => { }); describe("resolveDefaultBranch", () => { - it("reads the is_default flag out of the mirror's branch list", async () => { - const fetchMock = vi.fn().mockResolvedValue( - jsonResponse({ - branches: [ - { reference: { name: "sv-fe", hash: "aaa" } }, - { reference: { name: "master", hash: "bbb" }, is_default: true } - ] - }) - ); + it("reads defaultBranch out of the mirror's branch list", async () => { + const fetchMock = vi + .fn() + .mockResolvedValue(jsonResponse({ branches: [], total: 2, defaultBranch: "master" })); const target = gitTarget(config, repo, fetchMock); await expect(resolveDefaultBranch(target)).resolves.toBe("master"); const url = new URL(String(fetchMock.mock.calls[0][0])); - expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.listBranches"); + expect(url.pathname).toBe("/xrpc/org.tangled.temp.git.listBranches"); expect(url.searchParams.get("repo")).toBe(repo.repoDid); }); @@ -103,7 +98,7 @@ describe("endpoint routing", () => { expect(url.searchParams.get("path")).toBe("src/lib"); }); - it("sends branches and tags to the legacy list endpoints on the mirror", async () => { + it("sends branches and tags to the list endpoints on the mirror", async () => { const fetchMock = vi .fn() .mockImplementation(() => Promise.resolve(jsonResponse({ branches: [], tags: [] }))); @@ -112,17 +107,17 @@ describe("endpoint routing", () => { await branches(target, 5); await tags(target, 5); - expect(calledUrl(fetchMock).pathname).toBe("/xrpc/sh.tangled.git.temp.listBranches"); - expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/sh.tangled.git.temp.listTags"); + expect(calledUrl(fetchMock).pathname).toBe("/xrpc/org.tangled.temp.git.listBranches"); + expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/org.tangled.temp.git.listTags"); }); - it("routes single-tag reads to getTag on the mirror", async () => { + it("routes single-tag reads to getRef on the mirror", async () => { const fetchMock = vi .fn() - .mockImplementation(() => Promise.resolve(jsonResponse({ tag: undefined }))); + .mockImplementation(() => Promise.resolve(jsonResponse({}))); await tag(mirrorTarget(fetchMock), "v1.0.0"); - expect(calledUrl(fetchMock).pathname).toBe("/xrpc/sh.tangled.git.temp.getTag"); + expect(calledUrl(fetchMock).pathname).toBe("/xrpc/org.tangled.temp.git.getRef"); }); it("builds the raw url against the mirror without a raw flag", () => { diff --git a/web/src/lib/api/gitclient.ts b/web/src/lib/api/gitclient.ts index df312baf0..ae6ef4e4a 100644 --- a/web/src/lib/api/gitclient.ts +++ b/web/src/lib/api/gitclient.ts @@ -6,7 +6,7 @@ import { blobRawUrl as knotMirrorBlobRawUrl, languages as knotMirrorLanguages, log as knotMirrorLog, - getTag as knotMirrorGetTag, + getRef as knotMirrorGetRef, tags as knotMirrorTags, tree as knotMirrorTree } from "$lib/api/knotmirror"; @@ -43,19 +43,9 @@ export const resolveDefaultBranch = ( target: GitTarget, init?: { signal?: AbortSignal } ): Promise => - (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); + knotMirrorBranches(target.ctx, { repo: target.repo, limit: 1 }, init) + .then((response) => response.defaultBranch ?? null) + .catch(() => null); export const tree = (target: GitTarget, params: { ref: string; path?: string }) => knotMirrorTree(target.ctx, { repo: target.repo, ...params }); @@ -70,7 +60,7 @@ export const tags = (target: GitTarget, limit: number, cursor?: string) => knotMirrorTags(target.ctx, { repo: target.repo, limit, cursor }); export const tag = (target: GitTarget, name: string) => - knotMirrorGetTag(target.ctx, { repo: target.repo, tag: name }); + knotMirrorGetRef(target.ctx, { repo: target.repo, ref: `refs/tags/${name}` }); export const languages = (target: GitTarget, ref: string) => knotMirrorLanguages(target.ctx, { repo: target.repo, ref }); diff --git a/web/src/lib/api/knotmirror.ts b/web/src/lib/api/knotmirror.ts index 945506cc5..a8fb08b6d 100644 --- a/web/src/lib/api/knotmirror.ts +++ b/web/src/lib/api/knotmirror.ts @@ -1,31 +1,26 @@ import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import { buildUrl, jsonGet, rawGet } from "$lib/api/_request"; -import type { RepoTagResponse } from "$lib/api/repo"; import type * as RepoBlob from "$lib/api/lexicons/types/sh/tangled/repo/blob"; import type * as GitDefs from "$lib/api/lexicons/types/sh/tangled/git/temp/defs"; -import type * as MirrorBranch from "$lib/api/lexicons/types/org/tangled/temp/git/getBranch"; +import type * as MirrorGetRef from "$lib/api/lexicons/types/org/tangled/temp/git/getRef"; import type * as MirrorEntry from "$lib/api/lexicons/types/org/tangled/temp/git/getEntry"; +import type * as MirrorListBranches from "$lib/api/lexicons/types/org/tangled/temp/git/listBranches"; +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 BranchesResponse, - type LogResponse, - type TagsResponse -} from "$lib/api/repo"; +import { treeEntryKind, type LogResponse } from "$lib/api/repo"; const TREE_NSID = "org.tangled.temp.git.getTree"; const ENTRY_NSID = "org.tangled.temp.git.getEntry"; const MERGE_BASE_NSID = "org.tangled.temp.git.getMergeBase"; -const GET_BRANCH_NSID = "org.tangled.temp.git.getBranch"; +const BRANCHES_NSID = "org.tangled.temp.git.listBranches"; +const TAGS_NSID = "org.tangled.temp.git.listTags"; +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 BRANCHES_NSID = "sh.tangled.git.temp.listBranches"; -const TAGS_NSID = "sh.tangled.git.temp.listTags"; const LANGUAGES_NSID = "sh.tangled.git.temp.listLanguages"; -const GET_TAG_NSID = "sh.tangled.git.temp.getTag"; const MAX_README_BYTES = 1 << 20; @@ -190,19 +185,19 @@ export const branches = ( ctx: BobbinContext, params: { repo: string; limit: number; cursor?: string }, init?: XrpcRequestInit -) => jsonGet(ctx, BRANCHES_NSID, params, init); +) => jsonGet(ctx, BRANCHES_NSID, params, init); export const tags = ( ctx: BobbinContext, params: { repo: string; limit: number; cursor?: string }, init?: XrpcRequestInit -) => jsonGet(ctx, TAGS_NSID, params, init); +) => jsonGet(ctx, TAGS_NSID, params, init); -export const getTag = ( +export const getRef = ( ctx: BobbinContext, - params: { repo: string; tag: string }, + params: { repo: string; ref: string }, init?: XrpcRequestInit -) => jsonGet(ctx, GET_TAG_NSID, params, init); +) => jsonGet(ctx, GET_REF_NSID, params, init); export const languages = ( ctx: BobbinContext, @@ -215,9 +210,3 @@ export const getMergeBase = ( params: { repo: string; base: string; head: string }, init?: XrpcRequestInit ) => jsonGet<{ commit: string }>(bobbin, MERGE_BASE_NSID, params, init); - -export const getBranch = ( - bobbin: BobbinContext, - params: { repo: string; name: string }, - init?: XrpcRequestInit -) => jsonGet(bobbin, GET_BRANCH_NSID, params, init); diff --git a/web/src/lib/api/pullCompose.test.ts b/web/src/lib/api/pullCompose.test.ts index aca3dfcc8..3f5ee0bde 100644 --- a/web/src/lib/api/pullCompose.test.ts +++ b/web/src/lib/api/pullCompose.test.ts @@ -7,12 +7,13 @@ import { sortBranchesByRecency, sourceBranchChoices } from "$lib/api/pullCompose"; -import type { BranchEntry } from "$lib/api/repo"; +import type { BranchSummary } from "$lib/api/repo"; -const branch = (name: string, when?: string, isDefault = false): BranchEntry => ({ - reference: { name, hash: `hash-${name}` }, - commit: when ? { Committer: { Name: "n", Email: "e", When: when } } : undefined, - is_default: isDefault +const branch = (name: string, when?: string, isDefault = false): BranchSummary => ({ + name, + hash: `hash-${name}`, + when, + isDefault }); // main is the default; feat is the newest of the rest @@ -22,7 +23,7 @@ const branches = [ branch("feat", "2024-03-01T00:00:00Z") ]; -const names = (list: BranchEntry[]) => list.map((entry) => entry.reference.name); +const names = (list: BranchSummary[]) => list.map((entry) => entry.name); describe("sortBranchesByRecency", () => { it("puts the newest commit first", () => { diff --git a/web/src/lib/api/pullCompose.ts b/web/src/lib/api/pullCompose.ts index 0b0633f63..fc01f9c9f 100644 --- a/web/src/lib/api/pullCompose.ts +++ b/web/src/lib/api/pullCompose.ts @@ -6,14 +6,7 @@ import { branches as listBranches, gitTarget, type GitServiceConfig } from "$lib import { listCommits, toMirrorCommitSummary } from "$lib/api/gitmirror"; import { getMergeBase } from "$lib/api/knotmirror"; import { listRepos } from "$lib/api/records"; -import { - repoNameOf, - sortBranches, - toBranchSummary, - type BranchEntry, - type BranchSummary, - type CommitSummary -} from "$lib/api/repo"; +import { repoNameOf, branchSummaries, type BranchSummary, type CommitSummary } from "$lib/api/repo"; export type PullSource = "patch" | "branch" | "fork"; @@ -24,13 +17,10 @@ export const parseSource = (raw: string | null | undefined): PullSource | null = return value && SOURCES.includes(value) ? value : null; }; -const branchTime = (branch: BranchEntry): number => { - const when = branch.commit?.Committer?.When; - return when ? Date.parse(when) : NaN; -}; +const branchTime = (branch: BranchSummary): number => (branch.when ? Date.parse(branch.when) : NaN); /** newest commit first; branches with no commit sort last, order otherwise kept */ -export const sortBranchesByRecency = (branches: readonly BranchEntry[]): BranchEntry[] => +export const sortBranchesByRecency = (branches: readonly BranchSummary[]): BranchSummary[] => branches .map((branch, index) => ({ branch, index, time: branchTime(branch) })) .sort((a, b) => { @@ -43,27 +33,30 @@ export const sortBranchesByRecency = (branches: readonly BranchEntry[]): BranchE .map((entry) => entry.branch); /** you can't open a PR from the default branch, so it never appears as a source */ -export const sourceBranchChoices = (branches: readonly BranchEntry[]): BranchEntry[] => - sortBranchesByRecency(branches.filter((branch) => !branch.is_default)); +export const sourceBranchChoices = (branches: readonly BranchSummary[]): BranchSummary[] => + sortBranchesByRecency(branches.filter((branch) => !branch.isDefault)); -const named = (branches: readonly BranchEntry[], name: string): boolean => - branches.some((branch) => branch.reference.name === name); +const named = (branches: readonly BranchSummary[], name: string): boolean => + branches.some((branch) => branch.name === name); -export const defaultTargetBranch = (branches: readonly BranchEntry[], current: string): string => { +export const defaultTargetBranch = ( + branches: readonly BranchSummary[], + current: string +): string => { if (current && named(branches, current)) return current; - return branches.find((branch) => branch.is_default)?.reference.name ?? ""; + return branches.find((branch) => branch.isDefault)?.name ?? ""; }; export const defaultSourceBranch = ( source: PullSource, current: string, - branchChoices: readonly BranchEntry[], - forkBranches: readonly BranchEntry[] + branchChoices: readonly BranchSummary[], + forkBranches: readonly BranchSummary[] ): string => { if (source === "patch") return current; const candidates = source === "fork" ? forkBranches : branchChoices; if (current && named(candidates, current)) return current; - return candidates[0]?.reference.name ?? ""; + return candidates[0]?.name ?? ""; }; export interface ComposeSelection { @@ -83,7 +76,7 @@ export const composeQuery = (selection: ComposeSelection): string => { return query.toString(); }; -const BRANCH_LIMIT = 500; +const BRANCH_LIMIT = 100; const COMMIT_LIMIT = 100; // one of the viewer's forks, as the fork picker needs it @@ -102,8 +95,8 @@ export interface ComposeData { // `owner/repo` of the source repo, for the browse-at-commit links sourceRepoPath: string; branches: BranchSummary[]; - sourceBranches: BranchEntry[]; - forkBranches: BranchEntry[]; + sourceBranches: BranchSummary[]; + forkBranches: BranchSummary[]; forks: ForkOption[]; source: PullSource; sourceBranch: string; @@ -135,8 +128,8 @@ export const loadCompose = async (args: { const [branchList, forkList] = await Promise.all([ listBranches(gitTarget(config, repo, fetchFn), BRANCH_LIMIT) - .then((page) => page.branches ?? []) - .catch(() => [] as BranchEntry[]), + .then(branchSummaries) + .catch(() => [] as BranchSummary[]), viewer ? listRepos(ctx, viewer.did, { limit: 100 }) .then((page) => @@ -164,14 +157,14 @@ export const loadCompose = async (args: { const forkRepo = forkList.find((entry) => entry.repoDid === fork); let prefillError = ""; - let forkBranches: BranchEntry[] = []; + let forkBranches: BranchSummary[] = []; if (source === "fork" && forkRepo) { try { const page = await listBranches( gitTarget(config, { uri: forkRepo.uri, repoDid: forkRepo.repoDid }, fetchFn), BRANCH_LIMIT ); - forkBranches = sortBranchesByRecency(page.branches ?? []); + forkBranches = sortBranchesByRecency(branchSummaries(page)); } catch (cause) { prefillError = cause instanceof Error ? cause.message : String(cause); } @@ -221,7 +214,7 @@ export const loadCompose = async (args: { sourceRepoPath: forkRepo ? `${forkRepo.owner}/${forkRepo.name}` : `${repo.ownerHandle}/${repo.name}`, - branches: sortBranches(branchList.map(toBranchSummary)), + branches: branchList, sourceBranches, forkBranches, forks: forkList, diff --git a/web/src/lib/api/repo.test.ts b/web/src/lib/api/repo.test.ts index 9921ad1da..8a52cfe9c 100644 --- a/web/src/lib/api/repo.test.ts +++ b/web/src/lib/api/repo.test.ts @@ -12,9 +12,7 @@ import { toTagSummary, toTreeEntrySummary, treeEntryKind, - type BranchEntry, type LogCommit, - type TagEntry, type TreeEntrySummary } from "$lib/api/repo"; import { ClientResponseError, createBobbinClient, type BobbinContext } from "$lib/api/client"; @@ -167,16 +165,15 @@ describe("importState", () => { }); describe("toBranchSummary", () => { - it("reads the nested reference and the go-git commit fields", () => { - const branch: BranchEntry = { - reference: { name: "master", hash: "ff3a3678" }, - commit: { - Committer: { Name: "Ada", Email: "a@b.c", When: "2026-07-02T10:00:00Z" }, - Message: "the tip commit" - }, - is_default: true + it("reads the tip commit and marks the default branch", () => { + const branch = { + name: "master", + oid: "ff3a3678", + message: "the tip commit", + author: { name: "Ada", email: "a@b.c", when: "2026-07-01T10:00:00Z" as const }, + committer: { name: "Ada", email: "a@b.c", when: "2026-07-02T10:00:00Z" as const } }; - expect(toBranchSummary(branch)).toEqual({ + expect(toBranchSummary(branch, "master")).toEqual({ name: "master", hash: "ff3a3678", when: "2026-07-02T10:00:00Z", @@ -185,28 +182,35 @@ describe("toBranchSummary", () => { }); }); - it("treats a missing is_default as not default", () => { - expect(toBranchSummary({ reference: { name: "topic", hash: "abc" } }).isDefault).toBe( - false - ); + it("treats a missing defaultBranch as not default", () => { + const branch = { + name: "topic", + oid: "abc", + message: "", + author: { name: "Ada", email: "a@b.c", when: "2026-07-02T10:00:00Z" as const }, + committer: { name: "Ada", email: "a@b.c", when: "2026-07-02T10:00:00Z" as const } + }; + expect(toBranchSummary(branch).isDefault).toBe(false); }); }); describe("toTagSummary", () => { - // an annotated tag's own hash is the tag object, the commit is in Target - it("reads the inlined reference and follows an annotated tag to its commit", () => { - const tag: TagEntry = { - name: "v1.0.0", - hash: "63fa1d4b", - message: "release", - tag: { - Tagger: { Name: "Ada", Email: "a@b.c", When: "2026-07-01T10:00:00Z" }, - Target: [75, 78, 254, 37] - } - }; - expect(toTagSummary(tag)).toEqual({ + const author = { name: "Ada", email: "a@b.c", when: "2026-06-30T10:00:00Z" as const }; + + it("reads the tagger and message from the annotation", () => { + expect( + toTagSummary({ + name: "v1.0.0", + oid: "4b4efe25", + message: "tip commit", + author, + annotation: { + message: "release", + tagger: { name: "Ada", email: "a@b.c", when: "2026-07-01T10:00:00Z" } + } + }) + ).toEqual({ name: "v1.0.0", - hash: "63fa1d4b", commitHash: "4b4efe25", when: "2026-07-01T10:00:00Z", message: "release", @@ -215,7 +219,7 @@ describe("toTagSummary", () => { }); it("a lightweight tag is its own commit", () => { - const summary = toTagSummary({ name: "v1.0.0", hash: "eebb477b" }); + const summary = toTagSummary({ name: "v1.0.0", oid: "eebb477b", message: "", author }); expect(summary.commitHash).toBe("eebb477b"); }); }); diff --git a/web/src/lib/api/repo.ts b/web/src/lib/api/repo.ts index 7d1930208..e358c74f2 100644 --- a/web/src/lib/api/repo.ts +++ b/web/src/lib/api/repo.ts @@ -13,6 +13,8 @@ import { 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"; +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 @@ -37,12 +39,6 @@ export const parseRawCommit = (spec: string): { ref: string; format: "patch" | " return FULL_HASH_RE.test(ref) ? { ref, format } : null; }; -export interface GitCommit { - Author?: GitSignature; - Committer?: GitSignature; - Message?: string; -} - // `this` and `parent` are the hex hashes, `hash` is the byte array export interface LogCommit { this?: string; @@ -61,43 +57,6 @@ export interface LogResponse { page?: number; } -export interface GitReference { - name: string; - hash: string; -} - -export interface BranchEntry { - reference: GitReference; - commit?: GitCommit; - is_default?: boolean; -} - -export interface BranchesResponse { - branches?: BranchEntry[]; - total?: number; -} - -export interface TagEntry { - name: string; - hash: string; - message?: string; - tag?: { - Tagger?: GitSignature; - Message?: string; - // the commit an annotated tag points at, bytes like every go-git hash - Target?: number[]; - }; -} - -export interface TagsResponse { - tags?: TagEntry[]; - total?: number; -} - -export interface RepoTagResponse { - tag?: TagEntry; -} - // 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); @@ -320,12 +279,15 @@ export interface BranchSummary { message?: string; } -export const toBranchSummary = (branch: BranchEntry): BranchSummary => ({ - name: branch.reference.name, - hash: branch.reference.hash, - when: branch.commit?.Committer?.When ?? branch.commit?.Author?.When, - isDefault: branch.is_default === true, - message: branch.commit?.Message +export const toBranchSummary = ( + branch: ListBranches.BranchInfo, + defaultBranch?: string +): BranchSummary => ({ + name: branch.name, + hash: branch.oid, + when: branch.committer.when, + isDefault: branch.name === defaultBranch, + message: branch.message }); // canonical order for branch lists: default branch first, then newest commit first @@ -344,10 +306,13 @@ export const sortBranches = ( }) .map(({ branch }) => branch); +export const branchSummaries = (response: ListBranches.$output): BranchSummary[] => + sortBranches( + response.branches.map((branch) => toBranchSummary(branch, response.defaultBranch)) + ); + export interface TagSummary { name: string; - hash: string; - // an annotated tag has its own hash, this is the commit it points at commitHash: string; when?: string; message?: string; @@ -355,20 +320,13 @@ export interface TagSummary { taggerName?: string; } -export const hexFromBytes = (bytes: number[]): string => - bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""); - -export const toTagSummary = (tag: TagEntry): TagSummary => { - const target = tag.tag?.Target; - return { - name: tag.name, - hash: tag.hash, - commitHash: target?.length ? hexFromBytes(target) : tag.hash, - when: tag.tag?.Tagger?.When, - message: tag.message ?? tag.tag?.Message, - taggerName: tag.tag?.Tagger?.Name - }; -}; +export const toTagSummary = (tag: ListTags.TagInfo): TagSummary => ({ + name: tag.name.replace(/^refs\/tags\//, ""), + commitHash: tag.oid, + when: tag.annotation?.tagger?.when, + message: tag.annotation?.message, + taggerName: tag.annotation?.tagger?.name +}); export type TreeEntryKind = "file" | "directory" | "symlink" | "submodule"; diff --git a/web/src/lib/api/repoIndex.ts b/web/src/lib/api/repoIndex.ts index 37f211a56..ad84720fb 100644 --- a/web/src/lib/api/repoIndex.ts +++ b/web/src/lib/api/repoIndex.ts @@ -13,10 +13,9 @@ import { } from "$lib/api/gitclient"; import { orNull, parallel, stream } from "$lib/api/load"; import { - sortBranches, + branchSummaries, sortTreeEntries, tagsByCommitHash, - toBranchSummary, toCommitSummary, toTagSummary, toTreeCommitSummary, @@ -109,12 +108,11 @@ export const toLanguageSlices = (languages: { name: string; size: number }[]): L }; export const repoBranches = async (git: GitTarget) => - sortBranches(((await gitBranches(git, REF_LIMIT)).branches ?? []).map(toBranchSummary)); + branchSummaries(await gitBranches(git, REF_LIMIT)); const refNames = (branches: { name: string }[], tags: { name: string }[]) => ({ branches: branches.map((branch) => branch.name), - tags: tags.map((tag) => tag.name), - capped: branches.length >= REF_LIMIT || tags.length >= REF_LIMIT + tags: tags.map((tag) => tag.name) }); export const renderReadme = ( @@ -203,11 +201,11 @@ export const loadRepoIndex = ( const results = await parallel({ tree: attempt(gitTree(git, { ref: targetRef })), log: attempt(gitLog(git, { ref: targetRef, limit: COMMIT_LIMIT })), - branches: attempt(repoBranches(git)), + branches: attempt(gitBranches(git, REF_LIMIT)), tags: attempt(gitTags(git, REF_LIMIT)) }); - const branches = results.branches.value ?? []; + 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), @@ -236,9 +234,9 @@ export const loadRepoIndex = ( tagsByCommit: tagsByCommitHash(commits, tags), totalCommits: results.log.value?.total ?? commits.length, branches: branches.slice(0, BRANCH_LIMIT), - totalBranches: branches.length, + totalBranches: results.branches.value?.total ?? branches.length, tags: tags.slice(0, TAG_LIMIT), - totalTags: tags.length, + totalTags: results.tags.value?.total ?? tags.length, // the switcher needs every ref, not just the visible slice refs: refNames(branches, tags) }; diff --git a/web/src/lib/components/repo/RepoIndexView.stories.svelte b/web/src/lib/components/repo/RepoIndexView.stories.svelte index 11d4c3029..87d2325da 100644 --- a/web/src/lib/components/repo/RepoIndexView.stories.svelte +++ b/web/src/lib/components/repo/RepoIndexView.stories.svelte @@ -72,9 +72,9 @@ } ], totalBranches: 1, - tags: [{ name: "v1.0.0", hash: "abcdef0123456789", commitHash: "0123456789abcdef" }], + tags: [{ name: "v1.0.0", commitHash: "0123456789abcdef" }], totalTags: 1, - refs: { branches: ["main"], tags: ["v1.0.0"], capped: false } + refs: { branches: ["main"], tags: ["v1.0.0"] } }; const indexData = ( @@ -94,11 +94,6 @@ }); const data = indexData(); - const cappedData = indexData({ - totalBranches: 100, - totalTags: 100, - refs: { ...content.refs, capped: true } - }); // the tree column has to reach the bottom of the commit column, or the grid // stretches it and the skeleton shows a hole under the last row const skeletonFiles: IndexContent["files"] = [ @@ -156,7 +151,7 @@ totalBranches: skeletonBranches.length, tags: skeletonTags, totalTags: skeletonTags.length, - refs: { branches: ["main"], tags: [], capped: false } + refs: { branches: ["main"], tags: [] } }; const skeletonData = indexData(skeletonContent, { readmeHtml: Promise.resolve(skeletonReadme) @@ -184,7 +179,7 @@ totalBranches: 0, tags: [], totalTags: 0, - refs: { branches: [], tags: [], capped: false } + refs: { branches: [], tags: [] } }, { languages: Promise.resolve([]), readmeHtml: Promise.resolve(null) } ); @@ -205,7 +200,6 @@ - - diff --git a/web/src/lib/components/repo/RepoToolbar.svelte b/web/src/lib/components/repo/RepoToolbar.svelte index 6de6851c5..f83a7736e 100644 --- a/web/src/lib/components/repo/RepoToolbar.svelte +++ b/web/src/lib/components/repo/RepoToolbar.svelte @@ -14,7 +14,7 @@ interface Props { repo: RepoInfo; ref: string; - refs: { branches: string[]; tags: string[]; capped: boolean }; + refs: { branches: string[]; tags: string[] }; totalCommits: number; totalBranches: number; totalTags: number; @@ -69,16 +69,8 @@ icon={GitBranch} href={`${base}/branches`} count={totalBranches} - approximate={refs.capped} - /> - + diff --git a/web/src/lib/components/repo/TagCard.stories.svelte b/web/src/lib/components/repo/TagCard.stories.svelte index 2e84dc844..816a2f9a4 100644 --- a/web/src/lib/components/repo/TagCard.stories.svelte +++ b/web/src/lib/components/repo/TagCard.stories.svelte @@ -6,7 +6,6 @@ const annotated: TagSummary = { name: "v1.2.0", - hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", commitHash: "0123456789abcdef0123456789abcdef01234567", when: "2026-07-28T09:00:00Z", taggerName: "dawn", @@ -15,7 +14,6 @@ const lightweight: TagSummary = { name: "nightly", - hash: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", commitHash: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }; diff --git a/web/src/lib/components/repo/TagList.stories.svelte b/web/src/lib/components/repo/TagList.stories.svelte index 6808e4a80..4ce8da0e2 100644 --- a/web/src/lib/components/repo/TagList.stories.svelte +++ b/web/src/lib/components/repo/TagList.stories.svelte @@ -7,14 +7,12 @@ const tags = [ { name: "v1.0.0", - hash: "0123456789abcdef", commitHash: "0123456789abcdef", when: "2026-07-28T09:00:00Z", message: "first stable release" }, { name: "v0.9.0", - hash: "abcdef0123456789", commitHash: "abcdef0123456789", when: "2026-07-20T09:00:00Z" } @@ -29,7 +27,6 @@ const annotated = index % 3 !== 2; return { name: `v1.${index}.0`, - hash, commitHash: hash, when: "2026-08-19T12:00:00Z", ...(annotated diff --git a/web/src/lib/components/repo/pulls/PullCompose.stories.svelte b/web/src/lib/components/repo/pulls/PullCompose.stories.svelte index 758475d4e..2aaaafc2b 100644 --- a/web/src/lib/components/repo/pulls/PullCompose.stories.svelte +++ b/web/src/lib/components/repo/pulls/PullCompose.stories.svelte @@ -17,11 +17,10 @@ } from "$fixtures/pull"; const branch = (name: string, isDefault = false) => ({ - reference: { name, hash: "d34db33fd34db33fd34db33fd34db33fd34db33f" }, - is_default: isDefault, - commit: { - Committer: { Name: "alice", Email: "alice@example.com", When: "2026-08-10T12:00:00Z" } - } + name, + hash: "d34db33fd34db33fd34db33fd34db33fd34db33f", + isDefault, + when: "2026-08-10T12:00:00Z" }); const compose: ComposeData = { diff --git a/web/src/lib/components/repo/pulls/PullCompose.svelte b/web/src/lib/components/repo/pulls/PullCompose.svelte index ff24fbee2..3eacb2ea7 100644 --- a/web/src/lib/components/repo/pulls/PullCompose.svelte +++ b/web/src/lib/components/repo/pulls/PullCompose.svelte @@ -11,7 +11,7 @@ import { awaitIndexedOrSaved } from "$lib/api/awaitRecord"; import { createBobbinClient } from "$lib/api/client"; import { gitTarget } from "$lib/api/gitclient"; - import { formatPatchUrl, getBranch, getMergeBase } from "$lib/api/knotmirror"; + import { formatPatchUrl, getMergeBase, getRef } from "$lib/api/knotmirror"; import { getPatchStream } from "$lib/api/patchStream"; import { keepCommit, putPull } from "$lib/api/pull"; import { pingIndexNow } from "$lib/api/indexnow"; @@ -39,7 +39,7 @@ import PullCommitList from "$lib/components/repo/pulls/PullCommitList.svelte"; import PullCommitListHeader from "$lib/components/repo/pulls/PullCommitListHeader.svelte"; import type { PullRecord } from "$lib/api/records"; - import type { BranchEntry } from "$lib/api/repo"; + import 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"; @@ -142,7 +142,7 @@ }); }; - const defaultHint = (branch: BranchEntry) => (branch.is_default ? "default" : undefined); + const defaultHint = (branch: BranchSummary) => (branch.isDefault ? "default" : undefined); const targetOptions = $derived( view.branches.map((branch): SelectOption => { @@ -161,10 +161,9 @@ const branchOptions = $derived( view.sourceBranches.map((branch, index): SelectOption => { - const isTarget = - Boolean(view.targetBranch) && branch.reference.name === view.targetBranch; + const isTarget = Boolean(view.targetBranch) && branch.name === view.targetBranch; return { - value: branch.reference.name, + value: branch.name, // the list is recency sorted, so the head of it is the newest branch hint: isTarget ? "target" : index === 0 ? "new" : defaultHint(branch), disabled: isTarget @@ -174,7 +173,7 @@ const forkBranchOptions = $derived( view.forkBranches.map((branch, index): SelectOption => ({ - value: branch.reference.name, + value: branch.name, hint: index === 0 ? "new" : defaultHint(branch) })) ); @@ -251,13 +250,13 @@ base: view.targetBranch, head: view.sourceBranch }), - getBranch(bobbin, { repo: view.sourceRepoDid, name: view.sourceBranch }) + getRef(bobbin, { repo: view.sourceRepoDid, ref: `refs/heads/${view.sourceBranch}` }) ]); // pin before the record lands, so gc can't drop the head in between await keepCommit(agent, view.sourceKnot, { repo: view.sourceRepoDid, - oid: head.hash, + oid: head.oid, record: uri }); @@ -272,7 +271,7 @@ branch: view.sourceBranch } as PullRecord["source"], rounds: [], - versions: [{ base: mergeBase.commit, head: head.hash, createdAt }], + versions: [{ base: mergeBase.commit, head: head.oid, createdAt }], createdAt: createdAt as PullRecord["createdAt"] }; const saved = await putPull(agent, rkey, record); diff --git a/web/src/routes/[handle]/[repo]/branches/+page.ts b/web/src/routes/[handle]/[repo]/branches/+page.ts index 80eb52e2b..dba1f05c7 100644 --- a/web/src/routes/[handle]/[repo]/branches/+page.ts +++ b/web/src/routes/[handle]/[repo]/branches/+page.ts @@ -1,6 +1,6 @@ import { branches, gitTarget } from "$lib/api/gitclient"; import { REF_LIMIT } from "$lib/api/repoIndex"; -import { sortBranches, toBranchSummary } from "$lib/api/repo"; +import { branchSummaries } from "$lib/api/repo"; import { settle, stream } from "$lib/api/load"; import { discoveredPageCount, offsetFor, pageCount, pageParam } from "$lib/pagination"; import type { PageLoad } from "./$types"; @@ -15,10 +15,10 @@ export const load: PageLoad = async (event) => { const git = gitTarget(parent.publicConfig, repo, event.fetch); return branches(git, REF_LIMIT, cursor); }); - const list = results.then((res) => sortBranches((res.branches ?? []).map(toBranchSummary))); + const list = results.then((res) => branchSummaries(res)); const pages = results.then((res) => { - const total = res.total ?? 0; - const len = res.branches?.length ?? 0; + const total = res.total; + const len = res.branches.length; return total > 0 ? pageCount(total, REF_LIMIT) : discoveredPageCount(page, len === REF_LIMIT); diff --git a/web/src/routes/[handle]/[repo]/commits/[ref]/+page.ts b/web/src/routes/[handle]/[repo]/commits/[ref]/+page.ts index ade747d19..da5544666 100644 --- a/web/src/routes/[handle]/[repo]/commits/[ref]/+page.ts +++ b/web/src/routes/[handle]/[repo]/commits/[ref]/+page.ts @@ -54,7 +54,7 @@ export const load: PageLoad = async (event) => { ([commitList, tagRes, branchRes]) => { const map = tagsByCommitHash(commitList, (tagRes.tags ?? []).map(toTagSummary)); const shown = new Set(commitList.map((commit) => commit.hash)); - for (const branch of (branchRes.branches ?? []).map(toBranchSummary)) { + for (const branch of (branchRes.branches ?? []).map((b) => toBranchSummary(b))) { if (shown.has(branch.hash)) (map[branch.hash] ??= []).push(branch.name); } return map; diff --git a/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte b/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte index 78fb6e71a..5c586b0f2 100644 --- a/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte +++ b/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte @@ -38,7 +38,7 @@ import { loadDiff, type RepoDiffDeps, type RepoDiffSpec } from "$lib/api/repoDiff"; import { fileAnchor } from "$lib/components/repo/fileAnchor.svelte"; import type { PullView as PullRouteView } from "$lib/api/pullRoute"; - import { formatPatchUrl, getBranch, getMergeBase } from "$lib/api/knotmirror"; + import { formatPatchUrl, getMergeBase, getRef } from "$lib/api/knotmirror"; import { createAction, createLoad } from "$lib/action.svelte"; import ResizeHandle from "$lib/components/ui/ResizeHandle.svelte"; import { getAuth } from "$lib/auth.svelte"; @@ -178,13 +178,13 @@ fetch: globalThis.fetch }); // resolve target branch to raw commit ID - const target = await getBranch(bobbin, { + const target = await getRef(bobbin, { repo: data.repo.repoDid, - name: pull.targetBranch + ref: `refs/heads/${pull.targetBranch}` }); return await mergeCheck(bobbin, { targetRepo: data.repo.repoDid as Did, - targetCommit: target.hash, + targetCommit: target.oid, sourceRepo: data.sourceRepoDid as Did, sourceCommit: head }); @@ -247,7 +247,10 @@ fetch: globalThis.fetch }); try { - await getBranch(bobbin, { repo: data.repo.repoDid, name: pull.targetBranch }); + await getRef(bobbin, { + repo: data.repo.repoDid, + ref: `refs/heads/${pull.targetBranch}` + }); return true; } catch { return false; @@ -506,12 +509,12 @@ serviceUrl: data.publicConfig.bobbinUrl, fetch: globalThis.fetch }); - const branch = await getBranch(bobbin, { + const branch = await getRef(bobbin, { repo: data.repo.repoDid, - name: pull.targetBranch + ref: `refs/heads/${pull.targetBranch}` }); await mergeCommit(agent, data.publicConfig.bobbinUrl, data.repo.knot, { - target: { repo: data.repo.repoDid, branch: pull.targetBranch, commit: branch.hash }, + target: { repo: data.repo.repoDid, branch: pull.targetBranch, commit: branch.oid }, source: { repo: data.sourceRepoDid, commit: head }, style: "rebase" }); @@ -575,11 +578,11 @@ serviceUrl: data.publicConfig.bobbinUrl, fetch: globalThis.fetch }); - const branch = await getBranch(bobbin, { + const branch = await getRef(bobbin, { repo: data.sourceRepoDid, - name: sourceBranch + ref: `refs/heads/${sourceBranch}` }).catch(() => undefined); - return branch?.hash; + return branch?.oid; }); const canResubmit = $derived(Boolean(sourceHead.data) && sourceHead.data !== head); diff --git a/web/src/routes/[handle]/[repo]/settings/+page.ts b/web/src/routes/[handle]/[repo]/settings/+page.ts index 835648e2c..595e82feb 100644 --- a/web/src/routes/[handle]/[repo]/settings/+page.ts +++ b/web/src/routes/[handle]/[repo]/settings/+page.ts @@ -2,7 +2,7 @@ import { createBobbinClient } from "$lib/api/client"; import { branches, gitTarget } from "$lib/api/gitclient"; import { DEFAULT_LABEL_OWNER, listLabelDefinitions, type LabelView } from "$lib/api/labels"; import { settle, stream } from "$lib/api/load"; -import { sortBranches, toBranchSummary } from "$lib/api/repo"; +import { branchSummaries } from "$lib/api/repo"; import { REF_LIMIT } from "$lib/api/repoIndex"; import type { PageLoad } from "./$types"; @@ -20,7 +20,7 @@ export const load: PageLoad = async (event) => { const repo = parent.repo; const branchNames = branches(gitTarget(parent.publicConfig, repo, event.fetch), REF_LIMIT) - .then((res) => sortBranches((res.branches ?? []).map(toBranchSummary)).map((b) => b.name)) + .then((res) => branchSummaries(res).map((b) => b.name)) .catch(() => []); const labels = Promise.all([ diff --git a/web/src/routes/[handle]/[repo]/tags/+page.ts b/web/src/routes/[handle]/[repo]/tags/+page.ts index 0b1281d19..d86170f37 100644 --- a/web/src/routes/[handle]/[repo]/tags/+page.ts +++ b/web/src/routes/[handle]/[repo]/tags/+page.ts @@ -15,10 +15,10 @@ export const load: PageLoad = async (event) => { const git = gitTarget(parent.publicConfig, repo, event.fetch); return tags(git, REF_LIMIT, cursor); }); - const list = results.then((res) => (res.tags ?? []).map(toTagSummary)); + const list = results.then((res) => res.tags.map(toTagSummary)); const pages = results.then((res) => { - const total = res.total ?? 0; - const len = res.tags?.length ?? 0; + const total = res.total; + const len = res.tags.length; return total > 0 ? pageCount(total, REF_LIMIT) : discoveredPageCount(page, len === REF_LIMIT); diff --git a/web/src/routes/[handle]/[repo]/tags/[tag]/+page.ts b/web/src/routes/[handle]/[repo]/tags/[tag]/+page.ts index b265c92de..85640dc69 100644 --- a/web/src/routes/[handle]/[repo]/tags/[tag]/+page.ts +++ b/web/src/routes/[handle]/[repo]/tags/[tag]/+page.ts @@ -2,6 +2,7 @@ import { error } from "@sveltejs/kit"; import { gitTarget, tag, tags } from "$lib/api/gitclient"; import { settle, stream } from "$lib/api/load"; import { toTagSummary } from "$lib/api/repo"; +import type * as ListTags from "$lib/api/lexicons/types/org/tangled/temp/git/listTags"; import type { PageLoad } from "./$types"; export const load: PageLoad = async (event) => { @@ -11,12 +12,10 @@ export const load: PageLoad = async (event) => { const name = event.params.tag; const tagPromise = repoPromise.then(async (repo) => { const git = gitTarget(parent.publicConfig, repo, event.fetch); - let entry = await tag(git, name) - .then((result) => result.tag) - .catch(() => undefined); + let entry: ListTags.TagInfo | undefined = await tag(git, name).catch(() => undefined); if (!entry && name === "latest") { entry = await tags(git, 1) - .then((results) => results.tags?.[0]) + .then((results) => results.tags[0]) .catch(() => undefined); } if (!entry) error(404, `no tag named ${name}`);