fabrica

hanna/fabrica

feat(web): render the full landing when browsing a ref's root

5c069c7 · hanna committed on 2026-07-26

Viewing a tag or branch at the repository root now shows the same rich page as
the home view — search box, language bar, root tree, and README — with the ref
switcher reflecting the selected tag, instead of a bare file list. Extract the
shared body into `landing_body`, used by both the home and root tree views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
1 files changed · +61 −37UnifiedSplit
crates/web/src/repo.rs +61 −37
@@ -806,43 +806,8 @@ async fn repo_home(
806 let body = match resolved {806 let body = match resolved {
807 Err(_) => empty_repo_body(state, &ctx),807 Err(_) => empty_repo_body(state, &ctx),
808 Ok(rev) => {808 Ok(rev) => {
809 let entries = git_read(state, &ctx.repo.id, {
810 let rev = rev.clone();
811 move |repo| repo.tree_entries(&rev, FsPath::new(""))
812 })
813 .await?;
814 let readme = load_readme(state, &ctx.repo.id, &rev, &entries).await;
815 let annotations = git_read(state, &ctx.repo.id, {
816 let rev = rev.clone();
817 move |repo| repo.tree_annotations(&rev, FsPath::new(""))
818 })
819 .await
820 .unwrap_or_default();
821 let branches = branch_names(state, &ctx.repo.id).await;809 let branches = branch_names(state, &ctx.repo.id).await;
822 // The language breakdown of the default branch (best-effort).810 landing_body(state, &ctx, &rev_name, &rev, &branches).await?
823 let langs = git_read(state, &ctx.repo.id, {
824 let rev = rev.clone();
825 move |repo| repo.blob_sizes(&rev)
826 })
827 .await
828 .map(|files| crate::languages::breakdown(&files))
829 .unwrap_or_default();
830 let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path);
831 html! {
832 (repo_header(state, &ctx, Tab::Code, &rev_name, &branches))
833 form class="search-row repo-codesearch" method="get" action=(format!("{base}/-/search")) {
834 input type="search" name="q" placeholder="Search this repository…"
835 aria-label="Search this repository";
836 button class="btn" type="submit" { (icon(Icon::Search)) span { "Search" } }
837 }
838 @if !langs.is_empty() {
839 (language_bar(&langs))
840 }
841 (tree_table(&ctx.owner.username, &ctx.repo.path, &rev_name, "", &entries, &annotations))
842 @if let Some(rendered) = readme {
843 div class="card markdown readme" { (rendered) }
844 }
845 }
846 }811 }
847 };812 };
848813
@@ -851,6 +816,55 @@ async fn repo_home(
851 Ok((jar, page(&chrome, &title, body)).into_response())816 Ok((jar, page(&chrome, &title, body)).into_response())
852}817}
853818
819/// The rich repository landing for a resolved ref: search box, language bar, the
820/// root tree, and the README. Shared by the repo home (default branch) and the
821/// root tree view, so selecting a branch or tag in the ref switcher lands on the
822/// same page for that ref rather than a bare file list.
823async fn landing_body(
824 state: &AppState,
825 ctx: &RepoCtx,
826 rev_name: &str,
827 rev: &git::Resolved,
828 branches: &[String],
829) -> AppResult<Markup> {
830 let entries = git_read(state, &ctx.repo.id, {
831 let rev = rev.clone();
832 move |repo| repo.tree_entries(&rev, FsPath::new(""))
833 })
834 .await?;
835 let readme = load_readme(state, &ctx.repo.id, rev, &entries).await;
836 let annotations = git_read(state, &ctx.repo.id, {
837 let rev = rev.clone();
838 move |repo| repo.tree_annotations(&rev, FsPath::new(""))
839 })
840 .await
841 .unwrap_or_default();
842 // The language breakdown of this ref (best-effort).
843 let langs = git_read(state, &ctx.repo.id, {
844 let rev = rev.clone();
845 move |repo| repo.blob_sizes(&rev)
846 })
847 .await
848 .map(|files| crate::languages::breakdown(&files))
849 .unwrap_or_default();
850 let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path);
851 Ok(html! {
852 (repo_header(state, ctx, Tab::Code, rev_name, branches))
853 form class="search-row repo-codesearch" method="get" action=(format!("{base}/-/search")) {
854 input type="search" name="q" placeholder="Search this repository…"
855 aria-label="Search this repository";
856 button class="btn" type="submit" { (icon(Icon::Search)) span { "Search" } }
857 }
858 @if !langs.is_empty() {
859 (language_bar(&langs))
860 }
861 (tree_table(&ctx.owner.username, &ctx.repo.path, rev_name, "", &entries, &annotations))
862 @if let Some(rendered) = readme {
863 div class="card markdown readme" { (rendered) }
864 }
865 })
866}
867
854/// Find and render the first matching README in the root tree.868/// Find and render the first matching README in the root tree.
855async fn load_readme(869async fn load_readme(
856 state: &AppState,870 state: &AppState,
@@ -909,6 +923,17 @@ async fn tree_view(
909 move |repo| repo.resolve_ref(&rev_name)923 move |repo| repo.resolve_ref(&rev_name)
910 })924 })
911 .await?;925 .await?;
926 let branches = branch_names(state, &ctx.repo.id).await;
927
928 // At the repo root, render the full landing (search, language bar, README)
929 // for this ref, so switching to a tag/branch mirrors the home page.
930 if path.is_empty() {
931 let body = landing_body(state, &ctx, &rev_name, &rev, &branches).await?;
932 let (jar, chrome) = build_chrome(state, jar, viewer, uri.path().to_string());
933 let title = format!("{}/{}", ctx.owner.username, ctx.repo.path);
934 return Ok((jar, page(&chrome, &title, body)).into_response());
935 }
936
912 let entries = git_read(state, &ctx.repo.id, {937 let entries = git_read(state, &ctx.repo.id, {
913 let rev = rev.clone();938 let rev = rev.clone();
914 let path = path.clone();939 let path = path.clone();
@@ -923,7 +948,6 @@ async fn tree_view(
923 .await948 .await
924 .unwrap_or_default();949 .unwrap_or_default();
925950
926 let branches = branch_names(state, &ctx.repo.id).await;
927 let body = html! {951 let body = html! {
928 (repo_header(state, &ctx, Tab::Code, &rev_name, &branches))952 (repo_header(state, &ctx, Tab::Code, &rev_name, &branches))
929 (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path))953 (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path))