Krolyxon
Commit 8f664fb

ferreniumsrchandlerslog.rs

169 lines · 5.1 KB · Rust

1//! `/{repo}/log/{rev}[/{path…}]` — history, newest first, grouped by day.
2//! `?after=<oid>` pages, `?q=` filters by message and author.
3
4use askama::Template;
5use axum::http::StatusCode;
6use axum::response::Response;
7use gix::bstr::ByteSlice;
8
9use super::repo::{Active, Crumb, Ctx, RepoNav, crumbs, set_cache_headers, short};
10use super::{Layout, render};
11use crate::cache::Key;
12use crate::git::ObjectId;
13use crate::git::log::{self, LogError, LogQuery, Newer};
14use crate::render::{encode_path, time};
15
16pub struct LogRow {
17 pub subject: String,
18 pub url: String,
19 pub author: String,
20 pub age: String,
21 pub iso: String,
22 pub id: String,
23}
24
25pub struct Day {
26 pub label: String,
27 pub rows: Vec<LogRow>,
28}
29
30#[derive(Template)]
31#[template(path = "log.html")]
32pub struct LogPage {
33 pub layout: Layout,
34 pub nav: RepoNav,
35 pub crumbs: Vec<Crumb>,
36 pub here: Option<String>,
37 pub days: Vec<Day>,
38 /// The form posts here (the log URL without query).
39 pub action: String,
40 pub q: String,
41 pub newer_url: Option<String>,
42 /// `Newer`, or `Newest` when the link jumps back to the first page.
43 pub newer_label: &'static str,
44 pub older_url: Option<String>,
45 pub limit_reached: bool,
46 pub scan_limit: usize,
47}
48
49pub fn page(ctx: &Ctx, segments: &[Vec<u8>]) -> Response {
50 let (rev, path) = match ctx.resolve(segments) {
51 Ok(found) => found,
52 Err(res) => return *res,
53 };
54 let after = match ctx.query.after.as_deref().filter(|a| !a.is_empty()) {
55 None => None,
56 Some(hex) => match ObjectId::from_hex(hex.as_bytes()) {
57 Ok(id) => Some(id),
58 Err(_) => return ctx.not_found("INVALID CURSOR"),
59 },
60 };
61 let grep = ctx
62 .query
63 .q
64 .as_deref()
65 .map(str::trim)
66 .filter(|q| !q.is_empty())
67 .map(str::to_string);
68 let query = LogQuery {
69 path: path.clone(),
70 grep,
71 after,
72 page: ctx.config.ui.log_page,
73 scan_limit: ctx.config.limits.log_scan,
74 };
75 let etag = ctx.etag(("log", rev.commit, &query));
76 let immutable = rev.is_immutable();
77 if let Some(res) = ctx.not_modified(&etag, immutable) {
78 return res;
79 }
80 let key = Key::Log {
81 tip: rev.commit,
82 query: query.clone(),
83 };
84 let result = ctx
85 .state
86 .cache
87 .log(key, || log::page(&ctx.git, rev.commit, &query));
88 let found = match result {
89 Ok(found) => found,
90 Err(LogError::UnknownCursor) => return ctx.not_found("COMMIT NOT IN THIS HISTORY"),
91 Err(LogError::Git(e)) => return ctx.internal_error(e),
92 };
93
94 let action = ctx.url("log", &rev.name, &path);
95 let link = |after: Option<ObjectId>| {
96 let mut params = Vec::new();
97 if let Some(q) = &query.grep {
98 params.push(format!("q={}", encode_path(q)));
99 }
100 if let Some(id) = after {
101 params.push(format!("after={id}"));
102 }
103 if params.is_empty() {
104 action.clone()
105 } else {
106 format!("{action}?{}", params.join("&"))
107 }
108 };
109 let (newer_url, newer_label) = match found.newer {
110 Newer::None => (None, "Newer"),
111 Newer::First => (Some(link(None)), "Newest"),
112 Newer::After(id) => (Some(link(Some(id))), "Newer"),
113 };
114
115 let mailmap = ctx.mailmap();
116 let now = time::now();
117 let mut days: Vec<Day> = Vec::new();
118 for commit in &found.commits {
119 let label = time::day(commit.committer_time);
120 let id = commit.id.to_string();
121 let row = LogRow {
122 subject: if commit.subject.is_empty() {
123 "(no message)".into()
124 } else {
125 commit.subject.clone()
126 },
127 url: ctx.commit_url(&commit.id),
128 author: mailmap.resolve(&commit.author_name, &commit.author_email).0,
129 age: time::relative(commit.committer_time, now),
130 iso: time::iso(commit.committer_time),
131 id: short(&id),
132 };
133 match days.last_mut() {
134 Some(day) if day.label == label => day.rows.push(row),
135 _ => days.push(Day {
136 label,
137 rows: vec![row],
138 }),
139 }
140 }
141
142 let (crumbs, last) = crumbs(ctx, &rev.name, &path);
143 let title = if path.is_empty() {
144 format!("Log · {}", ctx.repo.name)
145 } else {
146 format!("History of {} · {}", path.to_str_lossy(), ctx.repo.name)
147 };
148 let older_url = found.older.map(|id| link(Some(id)));
149 let page = LogPage {
150 layout: Layout {
151 feed: Some(ctx.feed_url()),
152 ..Layout::new(ctx.state, ctx.config, Some(&title))
153 },
154 nav: ctx.nav(Active::Log, Some(&rev), |name| ctx.url("log", name, &path)),
155 crumbs,
156 here: last,
157 days,
158 action,
159 q: query.grep.clone().unwrap_or_default(),
160 newer_url,
161 newer_label,
162 older_url,
163 limit_reached: found.limit_reached,
164 scan_limit: query.scan_limit,
165 };
166 let mut res = render(StatusCode::OK, &page);
167 set_cache_headers(&mut res, &etag, immutable);
168 res
169}