Krolyxon
Tag v0.1.0

ferreniumsrchandlersfeed.rs

127 lines · 3.9 KB · Rust

1//! `/{repo}/atom` — Atom feed of the newest commits on the default branch
2//! (`?h=branch` for another one).
3
4use askama::Template;
5use axum::http::{HeaderValue, StatusCode, header};
6use axum::response::Response;
7
8use super::render;
9use super::repo::{Ctx, set_cache_headers};
10use crate::cache::Key;
11use crate::git::log::{self, LogQuery};
12use crate::git::refs::{RefKind, Rev};
13use crate::git::{self, refs};
14use crate::render::time;
15
16const ENTRIES: usize = 20;
17
18pub struct Entry {
19 pub title: String,
20 pub url: String,
21 pub updated: String,
22 pub published: String,
23 pub author: String,
24 pub email: String,
25 pub message: String,
26}
27
28#[derive(Template)]
29#[template(path = "atom.xml")]
30pub struct Feed {
31 pub title: String,
32 pub subtitle: Option<String>,
33 pub self_url: String,
34 pub html_url: String,
35 pub updated: String,
36 pub version: &'static str,
37 pub entries: Vec<Entry>,
38}
39
40pub fn serve(ctx: &Ctx) -> Response {
41 let rev = match ctx.query.h.as_deref().filter(|h| !h.is_empty()) {
42 Some(branch) => match refs::resolve(&ctx.git, branch) {
43 Some(rev) if rev.kind == RefKind::Branch => rev,
44 _ => return ctx.not_found("UNKNOWN BRANCH"),
45 },
46 None => {
47 let meta = &ctx.repo.meta;
48 match (&meta.default_branch, meta.head) {
49 (Some(name), Some(commit)) => Rev {
50 name: name.clone(),
51 kind: RefKind::Branch,
52 commit,
53 },
54 _ => return ctx.not_found("REPOSITORY IS EMPTY"),
55 }
56 }
57 };
58 let etag = ctx.etag(("atom", rev.commit));
59 if let Some(res) = ctx.not_modified(&etag, false) {
60 return res;
61 }
62 let query = LogQuery {
63 page: ENTRIES,
64 scan_limit: ctx.config.limits.log_scan,
65 ..Default::default()
66 };
67 let key = Key::Log {
68 tip: rev.commit,
69 query: query.clone(),
70 };
71 let page = match ctx
72 .state
73 .cache
74 .log(key, || log::page(&ctx.git, rev.commit, &query))
75 {
76 Ok(page) => page,
77 Err(e) => return ctx.internal_error(anyhow::anyhow!("{e}")),
78 };
79
80 let origin = ctx.origin();
81 let mailmap = ctx.mailmap();
82 let mut entries = Vec::with_capacity(page.commits.len());
83 for commit in &page.commits {
84 let detail = match git::commit_detail(&ctx.git, commit.id) {
85 Ok(detail) => detail,
86 Err(e) => return ctx.internal_error(e),
87 };
88 let (author, email) = mailmap.resolve(&detail.author.name, &detail.author.email);
89 entries.push(Entry {
90 title: commit.subject.clone(),
91 url: format!("{origin}{}", ctx.commit_url(&commit.id)),
92 updated: time::iso(detail.committer.time),
93 published: time::iso(detail.author.time),
94 author,
95 email,
96 message: detail.message.trim_end().to_string(),
97 });
98 }
99 let self_url = match ctx.query.h.as_deref().filter(|h| !h.is_empty()) {
100 Some(_) => format!(
101 "{origin}{}/atom?h={}",
102 ctx.repo_url(),
103 crate::render::encode_path(&rev.name)
104 ),
105 None => format!("{origin}{}/atom", ctx.repo_url()),
106 };
107 let feed = Feed {
108 title: format!("{}: {}", ctx.repo.name, rev.name),
109 subtitle: ctx.repo.meta.description.clone(),
110 html_url: format!("{origin}{}", ctx.url("log", &rev.name, b"")),
111 self_url,
112 updated: page
113 .commits
114 .first()
115 .map(|c| time::iso(c.committer_time))
116 .unwrap_or_default(),
117 version: env!("CARGO_PKG_VERSION"),
118 entries,
119 };
120 let mut res = render(StatusCode::OK, &feed);
121 res.headers_mut().insert(
122 header::CONTENT_TYPE,
123 HeaderValue::from_static("application/atom+xml; charset=utf-8"),
124 );
125 set_cache_headers(&mut res, &etag, false);
126 res
127}