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(
806806 let body = match resolved {
807807 Err(_) => empty_repo_body(state, &ctx),
808808 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();
821809 let branches = branch_names(state, &ctx.repo.id).await;
822- // The language breakdown of the default branch (best-effort).
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- }
810+ landing_body(state, &ctx, &rev_name, &rev, &branches).await?
846811 }
847812 };
848813
@@ -851,6 +816,55 @@ async fn repo_home(
851816 Ok((jar, page(&chrome, &title, body)).into_response())
852817 }
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.
823+async 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+
854868 /// Find and render the first matching README in the root tree.
855869 async fn load_readme(
856870 state: &AppState,
@@ -909,6 +923,17 @@ async fn tree_view(
909923 move |repo| repo.resolve_ref(&rev_name)
910924 })
911925 .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+
912937 let entries = git_read(state, &ctx.repo.id, {
913938 let rev = rev.clone();
914939 let path = path.clone();
@@ -923,7 +948,6 @@ async fn tree_view(
923948 .await
924949 .unwrap_or_default();
925950
926- let branches = branch_names(state, &ctx.repo.id).await;
927951 let body = html! {
928952 (repo_header(state, &ctx, Tab::Code, &rev_name, &branches))
929953 (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path))