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) {