//! Pages: branch and tag lists at `/{owner}/{repo}/branches` and `/{owner}/{repo}/tags`. //! Also the ref helpers shared with the tree page (the ref selector). use std::path::PathBuf; use crate::state::AppState; use forge_core::Repo as RepoRow; use forge_git::browse::{self, CommitInfo, RefInfo, RefKind}; use topcoat::Result; use topcoat::context::{Cx, app_context}; use topcoat::router::{page, path_param}; use topcoat::view::{View, component, view}; use crate::i18n::code::{self as g, K}; use crate::i18n::{self, Lang}; use crate::pages::repo::repo_head; use crate::pages::repo_ctx::{RepoCtx, load_repo}; use crate::pages::tree::{LastCommit, last_commit_view}; use crate::views::{RefLink, now_unix, repo_href, shell}; path_param!(owner); path_param!(repo); /// Every branch and tag of the repo. Blocking work runs on the blocking pool. pub async fn load_refs(path: PathBuf) -> Result> { Ok(tokio::task::spawn_blocking(move || browse::list_refs(&path)).await??) } /// Branch and tag links for the ref selector. The default branch sorts first. pub fn ref_links( refs: &[RefInfo], current: &str, default_branch: &str, repo: &RepoRow, ) -> (Vec, Vec) { let branches = sorted(refs, RefKind::Branch, default_branch) .into_iter() .map(|r| ref_link(repo, &r.name, current)); let tags = sorted(refs, RefKind::Tag, default_branch) .into_iter() .map(|r| ref_link(repo, &r.name, current)); (branches.collect(), tags.collect()) } fn ref_link(repo: &RepoRow, name: &str, current: &str) -> RefLink { RefLink { name: name.to_owned(), href: repo_href(&repo.owner_name, &repo.name, "tree", name), current: name == current, } } /// Refs of one kind: the default branch first, then by name. fn sorted<'a>(refs: &'a [RefInfo], kind: RefKind, default_branch: &str) -> Vec<&'a RefInfo> { let mut out: Vec<&RefInfo> = refs.iter().filter(|r| r.kind == kind).collect(); out.sort_by(|a, b| { (a.name != default_branch, a.name.as_str()) .cmp(&(b.name != default_branch, b.name.as_str())) }); out } /// One row of the branch or tag table. pub struct RefRow { pub name: String, pub href: String, pub is_default: bool, pub last: Option, } /// Refs of `kind` with the commit each points at, in table order. async fn ref_rows( path: PathBuf, kind: RefKind, repo: &RepoRow, ) -> Result)>> { let rows = tokio::task::spawn_blocking(move || -> forge_git::Result<_> { let mut out = Vec::new(); for r in browse::list_refs(&path)? .into_iter() .filter(|r| r.kind == kind) { let commit = browse::read_commit(&path, &r.target)?; out.push((r, commit)); } Ok(out) }) .await??; let mut rows = rows; rows.sort_by(|(a, _), (b, _)| { (a.name != repo.default_branch, a.name.as_str()) .cmp(&(b.name != repo.default_branch, b.name.as_str())) }); Ok(rows) } #[component] async fn refs_table(rows: Vec, kind_label: &str, lang: Lang) -> Result { let empty = rows.is_empty(); Ok(view! { if empty {

(g::t(lang, K::NothingHere))

} else {
for row in rows.into_iter() { if let Some(last) = row.last { } else { } }
(kind_label) (g::t(lang, K::LastCommitHeader)) (g::t(lang, K::AgeHeader))
(row.name) if row.is_default { (g::t(lang, K::DefaultBadge)) } (last.summary)
} }) } /// Rows and titles for the branches or tags page. struct RefsPage { ctx: RepoCtx, rows: Vec, } async fn refs_data(cx: &Cx, kind: RefKind) -> Result { let owner = path_param::(cx); let name = path_param::(cx); let ctx = load_repo(cx, owner, name).await?; let now = now_unix(); let lang = i18n::lang(cx, ctx.viewer.as_ref().map(|(user, _)| user)); let rows = ref_rows(ctx.path.clone(), kind, &ctx.repo) .await? .into_iter() .map(|(info, commit)| RefRow { href: repo_href(&ctx.repo.owner_name, &ctx.repo.name, "tree", &info.name), is_default: kind == RefKind::Branch && info.name == ctx.repo.default_branch, last: commit .as_ref() .map(|c| last_commit_view(lang, &ctx.repo, c, now, ctx.viewer_tz())), name: info.name, }) .collect(); Ok(RefsPage { ctx, rows }) } #[page("/{owner}/{repo}/branches")] async fn branches_page(cx: &Cx) -> Result { let page = refs_data(cx, RefKind::Branch).await?; let ctx = page.ctx; let rows = page.rows; let lang = i18n::lang(cx, ctx.viewer.as_ref().map(|(user, _)| user)); let title = format!( "{}/{} \u{b7} {}", ctx.repo.owner_name, ctx.repo.name, g::t(lang, K::BranchesTitle) ); let header = ctx.head(app_context::(cx), "branches").await?; Ok(view! { shell(title: title.as_str(), viewer: ctx.viewer.as_ref(), repo_head(head: header) refs_table(rows: rows, kind_label: g::t(lang, K::BranchLabel), lang: lang) ) }) } #[page("/{owner}/{repo}/tags")] async fn tags_page(cx: &Cx) -> Result { let page = refs_data(cx, RefKind::Tag).await?; let ctx = page.ctx; let rows = page.rows; let lang = i18n::lang(cx, ctx.viewer.as_ref().map(|(user, _)| user)); let title = format!( "{}/{} \u{b7} {}", ctx.repo.owner_name, ctx.repo.name, g::t(lang, K::TagsTitle) ); let header = ctx.head(app_context::(cx), "tags").await?; Ok(view! { shell(title: title.as_str(), viewer: ctx.viewer.as_ref(), repo_head(head: header) refs_table(rows: rows, kind_label: g::t(lang, K::TagKindLabel), lang: lang) ) }) }