forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
7.3 KiB
1//! Pages: branch and tag lists at `/{owner}/{repo}/branches` and `/{owner}/{repo}/tags`.
2//! Also the ref helpers shared with the tree page (the ref selector).
3
4use std::path::PathBuf;
5
6use crate::state::AppState;
7use forge_core::Repo as RepoRow;
8use forge_git::browse::{self, CommitInfo, RefInfo, RefKind};
9use topcoat::Result;
10use topcoat::context::{Cx, app_context};
11use topcoat::router::{page, path_param};
12use topcoat::view::{View, component, view};
13
14use crate::i18n::code::{self as g, K};
15use crate::i18n::{self, Lang};
16use crate::pages::repo::repo_head;
17use crate::pages::repo_ctx::{RepoCtx, load_repo};
18use crate::pages::tree::{LastCommit, last_commit_view};
19use crate::views::{RefLink, now_unix, repo_href, shell};
20
21path_param!(owner);
22path_param!(repo);
23
24/// Every branch and tag of the repo. Blocking work runs on the blocking pool.
25pub async fn load_refs(path: PathBuf) -> Result<Vec<RefInfo>> {
26 Ok(tokio::task::spawn_blocking(move || browse::list_refs(&path)).await??)
27}
28
29/// Branch and tag links for the ref selector. The default branch sorts first.
30pub fn ref_links(
31 refs: &[RefInfo],
32 current: &str,
33 default_branch: &str,
34 repo: &RepoRow,
35) -> (Vec<RefLink>, Vec<RefLink>) {
36 let branches = sorted(refs, RefKind::Branch, default_branch)
37 .into_iter()
38 .map(|r| ref_link(repo, &r.name, current));
39 let tags = sorted(refs, RefKind::Tag, default_branch)
40 .into_iter()
41 .map(|r| ref_link(repo, &r.name, current));
42 (branches.collect(), tags.collect())
43}
44
45fn ref_link(repo: &RepoRow, name: &str, current: &str) -> RefLink {
46 RefLink {
47 name: name.to_owned(),
48 href: repo_href(&repo.owner_name, &repo.name, "tree", name),
49 current: name == current,
50 }
51}
52
53/// Refs of one kind: the default branch first, then by name.
54fn sorted<'a>(refs: &'a [RefInfo], kind: RefKind, default_branch: &str) -> Vec<&'a RefInfo> {
55 let mut out: Vec<&RefInfo> = refs.iter().filter(|r| r.kind == kind).collect();
56 out.sort_by(|a, b| {
57 (a.name != default_branch, a.name.as_str())
58 .cmp(&(b.name != default_branch, b.name.as_str()))
59 });
60 out
61}
62
63/// One row of the branch or tag table.
64pub struct RefRow {
65 pub name: String,
66 pub href: String,
67 pub is_default: bool,
68 pub last: Option<LastCommit>,
69}
70
71/// Refs of `kind` with the commit each points at, in table order.
72async fn ref_rows(
73 path: PathBuf,
74 kind: RefKind,
75 repo: &RepoRow,
76) -> Result<Vec<(RefInfo, Option<CommitInfo>)>> {
77 let rows = tokio::task::spawn_blocking(move || -> forge_git::Result<_> {
78 let mut out = Vec::new();
79 for r in browse::list_refs(&path)?
80 .into_iter()
81 .filter(|r| r.kind == kind)
82 {
83 let commit = browse::read_commit(&path, &r.target)?;
84 out.push((r, commit));
85 }
86 Ok(out)
87 })
88 .await??;
89 let mut rows = rows;
90 rows.sort_by(|(a, _), (b, _)| {
91 (a.name != repo.default_branch, a.name.as_str())
92 .cmp(&(b.name != repo.default_branch, b.name.as_str()))
93 });
94 Ok(rows)
95}
96
97#[component]
98async fn refs_table(rows: Vec<RefRow>, kind_label: &str, lang: Lang) -> Result<impl View> {
99 let empty = rows.is_empty();
100 Ok(view! {
101 if empty {
102 <p class="muted">(g::t(lang, K::NothingHere))</p>
103 } else {
104 <div class="table-wrap">
105 <table class="table refs-table">
106 <thead>
107 <tr>
108 <th>(kind_label)</th>
109 <th>(g::t(lang, K::LastCommitHeader))</th>
110 <th class="age">(g::t(lang, K::AgeHeader))</th>
111 </tr>
112 </thead>
113 <tbody>
114 for row in rows.into_iter() {
115 <tr>
116 <td class="tree-name">
117 <span class="tree-label">
118 <a href=(row.href)>(row.name)</a>
119 if row.is_default {
120 <span class="badge public">(g::t(lang, K::DefaultBadge))</span>
121 }
122 </span>
123 </td>
124 if let Some(last) = row.last {
125 <td class="tree-commit"><a href=(last.href)>(last.summary)</a></td>
126 <td class="age">
127 <time datetime=(last.iso.clone()) title=(last.absolute)>(last.age)</time>
128 </td>
129 } else {
130 <td class="tree-commit"></td>
131 <td class="age"></td>
132 }
133 </tr>
134 }
135 </tbody>
136 </table>
137 </div>
138 }
139 })
140}
141
142/// Rows and titles for the branches or tags page.
143struct RefsPage {
144 ctx: RepoCtx,
145 rows: Vec<RefRow>,
146}
147
148async fn refs_data(cx: &Cx, kind: RefKind) -> Result<RefsPage> {
149 let owner = path_param::<Owner>(cx);
150 let name = path_param::<Repo>(cx);
151 let ctx = load_repo(cx, owner, name).await?;
152 let now = now_unix();
153 let lang = i18n::lang(cx, ctx.viewer.as_ref().map(|(user, _)| user));
154 let rows = ref_rows(ctx.path.clone(), kind, &ctx.repo)
155 .await?
156 .into_iter()
157 .map(|(info, commit)| RefRow {
158 href: repo_href(&ctx.repo.owner_name, &ctx.repo.name, "tree", &info.name),
159 is_default: kind == RefKind::Branch && info.name == ctx.repo.default_branch,
160 last: commit
161 .as_ref()
162 .map(|c| last_commit_view(lang, &ctx.repo, c, now, ctx.viewer_tz())),
163 name: info.name,
164 })
165 .collect();
166 Ok(RefsPage { ctx, rows })
167}
168
169#[page("/{owner}/{repo}/branches")]
170async fn branches_page(cx: &Cx) -> Result<impl View> {
171 let page = refs_data(cx, RefKind::Branch).await?;
172 let ctx = page.ctx;
173 let rows = page.rows;
174 let lang = i18n::lang(cx, ctx.viewer.as_ref().map(|(user, _)| user));
175 let title = format!(
176 "{}/{} \u{b7} {}",
177 ctx.repo.owner_name,
178 ctx.repo.name,
179 g::t(lang, K::BranchesTitle)
180 );
181 let header = ctx.head(app_context::<AppState>(cx), "branches").await?;
182 Ok(view! {
183 shell(title: title.as_str(), viewer: ctx.viewer.as_ref(),
184 repo_head(head: header)
185 refs_table(rows: rows, kind_label: g::t(lang, K::BranchLabel), lang: lang)
186 )
187 })
188}
189
190#[page("/{owner}/{repo}/tags")]
191async fn tags_page(cx: &Cx) -> Result<impl View> {
192 let page = refs_data(cx, RefKind::Tag).await?;
193 let ctx = page.ctx;
194 let rows = page.rows;
195 let lang = i18n::lang(cx, ctx.viewer.as_ref().map(|(user, _)| user));
196 let title = format!(
197 "{}/{} \u{b7} {}",
198 ctx.repo.owner_name,
199 ctx.repo.name,
200 g::t(lang, K::TagsTitle)
201 );
202 let header = ctx.head(app_context::<AppState>(cx), "tags").await?;
203 Ok(view! {
204 shell(title: title.as_str(), viewer: ctx.viewer.as_ref(),
205 repo_head(head: header)
206 refs_table(rows: rows, kind_label: g::t(lang, K::TagKindLabel), lang: lang)
207 )
208 })
209}