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}
+
+
+
+ Name
+
+
+ Commit
+
+
+ Message
+
+
+ Date
+
+
+ {#each branches as branch (branch.name)}
+
+
+
+
{subject(branch.message)}
+
+ {#if branch.when}
+
+ {/if}
+
+
+ {/each}
+
+
+
+ {#each branches as branch, index (branch.name)}
+
+ {/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 @@
+
+
+
+
+
+
+ {#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) };
+};