From 1c318d0a6b0241396a6e297f5262482f52a32498 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sat, 11 Apr 2026 22:21:48 -0700 Subject: [PATCH] Add default branch support in repo handling. Updated repoTags and schema to include defaultBranch field. Enhanced RepoPage to navigate to default branch when no tags are present, and adjusted tag options to include default branch if not already a tag. --- convex/repoTags.ts | 5 +++ convex/schema.ts | 2 ++ src/pages/RepoPage.tsx | 69 ++++++++++++++++++++++++------------------ 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/convex/repoTags.ts b/convex/repoTags.ts index 62e4794..4169cf4 100644 --- a/convex/repoTags.ts +++ b/convex/repoTags.ts @@ -46,6 +46,7 @@ export const upsertFromGitHub = internalMutation({ description: v.string(), homepage: v.string(), meshforgeConfig: v.optional(v.any()), + defaultBranch: v.optional(v.string()), }, handler: async (ctx, args) => { const existing = await ctx.db @@ -62,6 +63,7 @@ export const upsertFromGitHub = internalMutation({ description: args.description, homepage: args.homepage, meshforgeConfig: args.meshforgeConfig, + defaultBranch: args.defaultBranch, } if (existing) { await ctx.db.patch(existing._id, doc) @@ -88,9 +90,11 @@ export const refresh = action({ const repoJson = (await repoRes.json()) as { description: string | null homepage: string | null + default_branch: string | null } const description = (repoJson.description ?? "").trim() const homepage = (repoJson.homepage ?? "").trim() + const defaultBranch = (repoJson.default_branch ?? "").trim() || undefined // Fetch meshforge.yaml from the default branch (no ref = default branch). let meshforgeConfig: MeshforgeConfig | null = null @@ -134,6 +138,7 @@ export const refresh = action({ description, homepage, meshforgeConfig: meshforgeConfig ?? undefined, + defaultBranch, }) return { ok: true as const, tagCount: tags.length } }, diff --git a/convex/schema.ts b/convex/schema.ts index 75cf52b..dd50e54 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -20,6 +20,8 @@ export const repoTagListFields = { homepage: v.optional(v.string()), /** Parsed meshforge.yaml from the repo's default branch, if present. */ meshforgeConfig: v.optional(v.any()), + /** GitHub's configured default branch (e.g. "main", "master"). Used as fallback when repo has no tags. */ + defaultBranch: v.optional(v.string()), } export const repoRefScanFields = { diff --git a/src/pages/RepoPage.tsx b/src/pages/RepoPage.tsx index 9e3859d..2c55209 100644 --- a/src/pages/RepoPage.tsx +++ b/src/pages/RepoPage.tsx @@ -74,14 +74,20 @@ export default function RepoPage() { if (sourceRef) return if (tagData === undefined || !tagData.row) return const tags = tagData.row.tags - if (tags.length === 0) return - const allSorted = sortTagNames(tags.map(t => t.name)) - const cfg = tagData.row.meshforgeConfig as MeshforgeConfig | null | undefined - const candidates = cfg ? filterTagNames(allSorted, cfg) : allSorted - // Fall back to unfiltered list when the profile leaves nothing (e.g. no matching tags yet) - const latest = (candidates.length > 0 ? candidates : allSorted)[0] - if (!latest) return - navigate(`/${ownerParam}/${repoParam}/tree/${buildTreeSplatPath(latest, null)}`, { replace: true }) + if (tags.length > 0) { + const allSorted = sortTagNames(tags.map(t => t.name)) + const cfg = tagData.row.meshforgeConfig as MeshforgeConfig | null | undefined + const candidates = cfg ? filterTagNames(allSorted, cfg) : allSorted + // Fall back to unfiltered list when the profile leaves nothing (e.g. no matching tags yet) + const latest = (candidates.length > 0 ? candidates : allSorted)[0] + if (!latest) return + navigate(`/${ownerParam}/${repoParam}/tree/${buildTreeSplatPath(latest, null)}`, { replace: true }) + } else { + // No tags — redirect to the repo's default branch if known + const defaultBranch = (tagData.row as { defaultBranch?: string }).defaultBranch + if (!defaultBranch) return + navigate(`/${ownerParam}/${repoParam}/tree/${buildTreeSplatPath(defaultBranch, null)}`, { replace: true }) + } }, [owner, repo, sourceRef, tagData, navigate, ownerParam, repoParam]) const [resolvedSha, setResolvedSha] = useState(null) @@ -194,12 +200,19 @@ export default function RepoPage() { const tagOptions = useMemo(() => { const tags = tagData?.row?.tags - if (!tags?.length) return [] - const raw = tags.map(t => t.name) - const needExtra = sourceRef && !raw.some(n => n === sourceRef || n.toLowerCase() === sourceRef.toLowerCase()) - const merged = needExtra ? [...raw, sourceRef] : [...raw] - return sortTagNames(merged) - }, [tagData?.row?.tags, sourceRef]) + const defaultBranch = (tagData?.row as { defaultBranch?: string } | null | undefined)?.defaultBranch + const raw = tags?.length ? tags.map(t => t.name) : [] + const extra: string[] = [] + // Include defaultBranch if not already a tag name + if (defaultBranch && !raw.some(n => n === defaultBranch || n.toLowerCase() === defaultBranch.toLowerCase())) { + extra.push(defaultBranch) + } + // Include sourceRef (current URL ref) if not already present + if (sourceRef && ![...raw, ...extra].some(n => n === sourceRef || n.toLowerCase() === sourceRef.toLowerCase())) { + extra.push(sourceRef) + } + return sortTagNames([...raw, ...extra]) + }, [tagData?.row, sourceRef]) // meshforgeConfig comes from the default branch (stored on the tag list, available before // a tag is selected so the tag dropdown itself can be filtered). @@ -208,10 +221,16 @@ export default function RepoPage() { string, string[] > | null - const filteredTagOptions = useMemo( - () => filterTagNames(tagOptions, meshforgeConfig ?? {}), - [tagOptions, meshforgeConfig] - ) + const filteredTagOptions = useMemo(() => { + const filtered = filterTagNames(tagOptions, meshforgeConfig ?? {}) + // Always keep sourceRef and defaultBranch selectable even if the profile filter drops them + const defaultBranch = (tagData?.row as { defaultBranch?: string } | null | undefined)?.defaultBranch + const reinjected = new Set(filtered.map(n => n.toLowerCase())) + const extras: string[] = [] + if (sourceRef && !reinjected.has(sourceRef.toLowerCase())) extras.push(sourceRef) + if (defaultBranch && !reinjected.has(defaultBranch.toLowerCase())) extras.push(defaultBranch) + return extras.length > 0 ? sortTagNames([...filtered, ...extras]) : filtered + }, [tagOptions, meshforgeConfig, sourceRef, tagData?.row]) const filteredEnvNames = useMemo( () => filterEnvNames(envNames, meshforgeConfig, envCapabilities ?? {}, tagDraft), [envNames, meshforgeConfig, envCapabilities, tagDraft] @@ -380,17 +399,9 @@ export default function RepoPage() { ) } - if (tagData.row.tags.length === 0) { - return ( -
-

- This repository has no tags. Mesh Forge needs at least one tag to pick a source revision. -

- -
- ) + // No tags and no explicit ref in URL — wait for redirect (useLayoutEffect will redirect to defaultBranch) + if (tagData.row.tags.length === 0 && !sourceRef) { + return
Opening repository…
} if (!sourceRef) {