diff --git a/web/src/lib/api/repo.test.ts b/web/src/lib/api/repo.test.ts index 8ef1311d..45aeb4cd 100644 --- a/web/src/lib/api/repo.test.ts +++ b/web/src/lib/api/repo.test.ts @@ -181,14 +181,18 @@ 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" } }, + commit: { + Committer: { Name: "Ada", Email: "a@b.c", When: "2026-07-02T10:00:00Z" }, + Message: "the tip commit" + }, is_default: true }; expect(toBranchSummary(branch)).toEqual({ name: "master", hash: "ff3a3678", when: "2026-07-02T10:00:00Z", - isDefault: true + isDefault: true, + message: "the tip commit" }); }); @@ -214,7 +218,8 @@ describe("toTagSummary", () => { hash: "63fa1d4b", commitHash: "4b4efe25", when: "2026-07-01T10:00:00Z", - message: "release" + message: "release", + taggerName: "Ada" }); }); diff --git a/web/src/lib/api/repo.ts b/web/src/lib/api/repo.ts index 77aab92d..d804a360 100644 --- a/web/src/lib/api/repo.ts +++ b/web/src/lib/api/repo.ts @@ -224,13 +224,16 @@ export interface BranchSummary { hash: string; when?: string; isDefault: boolean; + // full commit message + 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 + isDefault: branch.is_default === true, + message: branch.commit?.Message }); export interface TagSummary { @@ -240,6 +243,8 @@ export interface TagSummary { commitHash: string; when?: string; message?: string; + // annotated tags carry their tagger + taggerName?: string; } export const hexFromBytes = (bytes: number[]): string => @@ -252,7 +257,8 @@ export const toTagSummary = (tag: TagEntry): TagSummary => { hash: tag.hash, commitHash: target?.length ? hexFromBytes(target) : tag.hash, when: tag.tag?.Tagger?.When, - message: tag.message ?? tag.tag?.Message + message: tag.message ?? tag.tag?.Message, + taggerName: tag.tag?.Tagger?.Name }; }; diff --git a/web/src/lib/components/repo/BranchTable.stories.svelte b/web/src/lib/components/repo/BranchTable.stories.svelte new file mode 100644 index 00000000..f2a0d7c5 --- /dev/null +++ b/web/src/lib/components/repo/BranchTable.stories.svelte @@ -0,0 +1,53 @@ + + + + diff --git a/web/src/lib/components/repo/BranchTable.svelte b/web/src/lib/components/repo/BranchTable.svelte new file mode 100644 index 00000000..8d0513f9 --- /dev/null +++ b/web/src/lib/components/repo/BranchTable.svelte @@ -0,0 +1,107 @@ + + +
+

Branches

+ + {#if branches.length === 0} +

This repository has no branches.

+ {:else} + + +
+ {#each branches as branch, index (branch.name)} +
+
+ + {branch.name} + + {#if branch.isDefault} + Default + {/if} +
+
+ + {branch.hash.slice(0, 8)} + + {#if branch.when} + + + {/if} +
+
+ {/each} +
+ {/if} +
diff --git a/web/src/lib/components/repo/TagCard.stories.svelte b/web/src/lib/components/repo/TagCard.stories.svelte new file mode 100644 index 00000000..c06e2ad0 --- /dev/null +++ b/web/src/lib/components/repo/TagCard.stories.svelte @@ -0,0 +1,53 @@ + + + + diff --git a/web/src/lib/components/repo/TagCard.svelte b/web/src/lib/components/repo/TagCard.svelte new file mode 100644 index 00000000..b6f006ab --- /dev/null +++ b/web/src/lib/components/repo/TagCard.svelte @@ -0,0 +1,81 @@ + + +
+
+
+ + +
+ {shortHash} + {#if tag.taggerName} + + {tag.taggerName} + {/if} + {#if tag.when} + + + {/if} +
+
+ + +
+ +
+ {#if messageParts.length > 0} +

{messageParts[0]}

+ {#if messageParts.length > 1} +

{messageParts[1]}

+ {/if} + {:else} +

No message

+ {/if} +
+
diff --git a/web/src/lib/components/repo/TagList.svelte b/web/src/lib/components/repo/TagList.svelte index 27ed4c62..354fb6c5 100644 --- a/web/src/lib/components/repo/TagList.svelte +++ b/web/src/lib/components/repo/TagList.svelte @@ -26,14 +26,16 @@ {tag.name} -
- {#if tag.when} - - {/if} - {#if index === 0} - Latest - {/if} -
+ {#if tag.when || index === 0} +
+ {#if tag.when} + + {/if} + {#if index === 0} + Latest + {/if} +
+ {/if} {/each} diff --git a/web/src/routes/[handle]/[repo]/branches/+page.svelte b/web/src/routes/[handle]/[repo]/branches/+page.svelte new file mode 100644 index 00000000..c725d28b --- /dev/null +++ b/web/src/routes/[handle]/[repo]/branches/+page.svelte @@ -0,0 +1,33 @@ + + + + +{#if hasPrev || hasNext} +
+ {#if hasPrev} + + {/if} + {#if hasNext} + + {/if} +
+{/if} diff --git a/web/src/routes/[handle]/[repo]/branches/+page.ts b/web/src/routes/[handle]/[repo]/branches/+page.ts new file mode 100644 index 00000000..1a3d2850 --- /dev/null +++ b/web/src/routes/[handle]/[repo]/branches/+page.ts @@ -0,0 +1,23 @@ +import { branches, gitTarget } from "$lib/api/gitclient"; +import { REF_LIMIT } from "$lib/api/repoIndex"; +import { toBranchSummary } from "$lib/api/repo"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async (event) => { + const parent = await event.parent(); + const git = gitTarget(parent.publicConfig, parent.repo, event.fetch); + const rawPage = event.url.searchParams.get("page") ?? ""; + const parsed = /^\d+$/.test(rawPage) ? Number(rawPage) : 1; + const page = parsed >= 1 ? parsed : 1; + const cursor = page > 1 ? String((page - 1) * REF_LIMIT) : undefined; + const results = await branches(git, REF_LIMIT, cursor); + const list = (results.branches ?? []).map(toBranchSummary); + const total = results.total ?? 0; + const pageCount = + total > 0 ? Math.ceil(total / REF_LIMIT) : page + (list.length === REF_LIMIT ? 1 : 0); + return { + branches: list, + page, + pageCount + }; +}; diff --git a/web/src/routes/[handle]/[repo]/tags/+page.svelte b/web/src/routes/[handle]/[repo]/tags/+page.svelte new file mode 100644 index 00000000..459292e2 --- /dev/null +++ b/web/src/routes/[handle]/[repo]/tags/+page.svelte @@ -0,0 +1,41 @@ + + + +

Tags

+
+ {#each data.tags as tag (tag.name)} + + {:else} +

+ This repository does not contain any tags. +

+ {/each} +
+
+ +{#if hasPrev || hasNext} +
+ {#if hasPrev} + + {/if} + {#if hasNext} + + {/if} +
+{/if} diff --git a/web/src/routes/[handle]/[repo]/tags/+page.ts b/web/src/routes/[handle]/[repo]/tags/+page.ts new file mode 100644 index 00000000..699a90a1 --- /dev/null +++ b/web/src/routes/[handle]/[repo]/tags/+page.ts @@ -0,0 +1,23 @@ +import { gitTarget, tags } from "$lib/api/gitclient"; +import { REF_LIMIT } from "$lib/api/repoIndex"; +import { toTagSummary } from "$lib/api/repo"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async (event) => { + const parent = await event.parent(); + const git = gitTarget(parent.publicConfig, parent.repo, event.fetch); + const rawPage = event.url.searchParams.get("page") ?? ""; + const parsed = /^\d+$/.test(rawPage) ? Number(rawPage) : 1; + const page = parsed >= 1 ? parsed : 1; + const cursor = page > 1 ? String((page - 1) * REF_LIMIT) : undefined; + const results = await tags(git, REF_LIMIT, cursor); + const list = (results.tags ?? []).map(toTagSummary); + const total = results.total ?? 0; + const pageCount = + total > 0 ? Math.ceil(total / REF_LIMIT) : page + (list.length === REF_LIMIT ? 1 : 0); + return { + tags: list, + page, + pageCount + }; +}; diff --git a/web/src/routes/[handle]/[repo]/tags/[tag]/+page.svelte b/web/src/routes/[handle]/[repo]/tags/[tag]/+page.svelte new file mode 100644 index 00000000..4c892d18 --- /dev/null +++ b/web/src/routes/[handle]/[repo]/tags/[tag]/+page.svelte @@ -0,0 +1,11 @@ + + +
+
+ +
+
diff --git a/web/src/routes/[handle]/[repo]/tags/[tag]/+page.ts b/web/src/routes/[handle]/[repo]/tags/[tag]/+page.ts new file mode 100644 index 00000000..b9f86c3c --- /dev/null +++ b/web/src/routes/[handle]/[repo]/tags/[tag]/+page.ts @@ -0,0 +1,21 @@ +import { error } from "@sveltejs/kit"; +import { gitTarget, tag, tags } from "$lib/api/gitclient"; +import { toTagSummary } from "$lib/api/repo"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async (event) => { + const parent = await event.parent(); + const git = gitTarget(parent.publicConfig, parent.repo, event.fetch); + // the params arrive decoded, the wire names are raw + const name = event.params.tag; + let entry = await tag(git, name) + .then((result) => result.tag) + .catch(() => undefined); + if (!entry && name === "latest") { + entry = await tags(git, 1) + .then((results) => results.tags?.[0]) + .catch(() => undefined); + } + if (!entry) error(404, `no tag named ${name}`); + return { tag: toTagSummary(entry) }; +};