johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! Atom feeds of a repository, for feed readers: the commits on its
//! default branch, its tags and its releases, newest first.
use super::releases::Published;
use super::*;
use knotless::read::Commit;
use knotless::records::ReleaseRecord;
/// The entries a feed holds.
const ENTRIES: usize = 30;
/// One entry of a feed.
struct Entry {
title: String,
/// What names it for good.
id: String,
/// Its page.
link: String,
updated: String,
author: Option<String>,
/// Text, or with `html` HTML.
content: String,
html: bool,
}
/// `/{authority}/{repo}/commits.atom`: the commits on the default branch.
pub async fn commits(
State(app): State<Arc<App>>,
headers: HeaderMap,
path: RepoPath,
) -> Result<Response> {
read_as_viewer(&app, &headers, made(&app, path, Kind::Commits)).await
}
/// `/{authority}/{repo}/tags.atom`: the tags that name a commit.
pub async fn tags(
State(app): State<Arc<App>>,
headers: HeaderMap,
path: RepoPath,
) -> Result<Response> {
read_as_viewer(&app, &headers, made(&app, path, Kind::Tags)).await
}
/// `/{authority}/{repo}/releases.atom`: each version of each package.
pub async fn releases(
State(app): State<Arc<App>>,
headers: HeaderMap,
path: RepoPath,
) -> Result<Response> {
read_as_viewer(&app, &headers, made(&app, path, Kind::Releases)).await
}
#[derive(Clone, Copy)]
enum Kind {
Commits,
Tags,
Releases,
}
/// `answer` read as whoever the cookie names, as the raw route reads: a
/// private repository's feed is its members' alone, and kept by no cache.
async fn read_as_viewer(
app: &App,
headers: &HeaderMap,
answer: impl Future<Output = Result<Response>>,
) -> Result<Response> {
let viewer = crate::login::viewer(app, headers);
let (made, touched) = crate::private::reading(viewer, None, answer).await;
let mut res = made?;
if touched {
res = crate::private::unshared(res);
}
res.headers_mut()
.append(header::VARY, HeaderValue::from_static("cookie"));
Ok(res)
}
async fn made(app: &App, Path((authority, name)): RepoPath, kind: Kind) -> Result<Response> {
let repo = Repo::open(app, &authority, &name).await?;
let named = format!("{}/{}", repo.authority, repo.name);
let (title, page, file, entries) = match kind {
Kind::Commits => {
let title = format!("Commits to {named}");
let page = repo.href(&format!("/commits/{}", url_path(&repo.head)));
(title, page, "commits", commit_entries(&repo)?)
}
Kind::Tags => (
format!("Tags of {named}"),
repo.href("/tags"),
"tags",
tag_entries(&repo)?,
),
Kind::Releases => {
let entries = release_entries(app, &repo);
(
format!("Releases of {named}"),
repo.href("/releases"),
"releases",
entries,
)
}
};
let feed = repo.href(&format!("/{file}.atom"));
Ok((
[
(header::CONTENT_TYPE, "application/atom+xml; charset=utf-8"),
(header::CACHE_CONTROL, "no-cache"),
],
atom(&title, &absolute(feed), &absolute(page), entries),
)
.into_response())
}
/// The newest commits on the default branch, each by its author, its
/// message as text; none on a repository with nothing pushed.
fn commit_entries(repo: &Repo) -> Result<Vec<Entry>> {
let objects = repo.objects()?;
// HEAD names its branch before anything is pushed to it.
if objects.resolve(&repo.head).is_err() {
return Ok(Vec::new());
}
let log = objects
.log(&repo.head, "", 0, ENTRIES)
.map_err(AppError::internal)?;
Ok(log.into_iter().map(|c| commit_entry(repo, c)).collect())
}
fn commit_entry(repo: &Repo, c: Commit) -> Entry {
let subject = c.message.lines().next().unwrap_or_default().to_string();
let link = absolute(repo.href(&format!("/commit/{}", c.sha)));
Entry {
title: subject,
id: link.clone(),
link,
updated: c.committer.date,
author: Some(c.author.name),
content: c.message,
html: false,
}
}
/// The newest tags, each named by the commit it names, with that commit's
/// subject.
fn tag_entries(repo: &Repo) -> Result<Vec<Entry>> {
let tags = repo.objects()?.tags().map_err(AppError::internal)?;
// Named by the tag, not its commit: two tags of one commit are two
// entries.
let entry = |(name, c): (String, Commit)| Entry {
id: absolute(repo.href(&format!("/tree/{}", url_path(&name)))),
link: absolute(repo.href(&format!("/tree/{}", url_path(&name)))),
title: name,
updated: c.committer.date,
author: Some(c.author.name),
content: c.message.lines().next().unwrap_or_default().to_string(),
html: false,
};
Ok(tags.into_iter().take(ENTRIES).map(entry).collect())
}
/// The newest releases, each named by its record and linked to its
/// package's releases, with its notes as the releases page draws them.
fn release_entries(app: &App, repo: &Repo) -> Vec<Entry> {
let base = repo.href("");
let page = |package: &str| {
let query = format!("/releases?package={}", url_path(package));
absolute(repo.href(&query))
};
let entry = |(uri, r, _): (String, ReleaseRecord, bool)| Entry {
title: format!("{} {}", r.package, r.version),
id: uri,
link: page(&r.package),
updated: r.created_at,
author: None,
content: r
.notes
.as_deref()
.map(|n| markdown(n, &base, &base, "", None))
.unwrap_or_default(),
html: true,
};
let all = Published::of(app, repo).all;
all.into_iter().take(ENTRIES).map(entry).collect()
}
/// An Atom feed, `title`, at `id`, of the page `page`: updated when its
/// newest entry was, or at the epoch when it has none.
fn atom(title: &str, id: &str, page: &str, entries: Vec<Entry>) -> String {
let updated = entries
.first()
.map_or("1970-01-01T00:00:00Z", |e| &e.updated);
let mut out = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
out += "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n";
let _ = writeln!(out, "<id>{}</id>", xml(id));
let _ = writeln!(out, "<title>{}</title>", xml(title));
let _ = writeln!(out, "<updated>{}</updated>", xml(updated));
let _ = writeln!(out, "<link rel=\"self\" href=\"{}\"/>", xml(id));
let _ = writeln!(
out,
"<link rel=\"alternate\" type=\"text/html\" href=\"{}\"/>",
xml(page)
);
for e in &entries {
out += "<entry>\n";
let _ = writeln!(out, "<id>{}</id>", xml(&e.id));
let _ = writeln!(out, "<title>{}</title>", xml(&e.title));
let _ = writeln!(out, "<updated>{}</updated>", xml(&e.updated));
let _ = writeln!(
out,
"<link rel=\"alternate\" type=\"text/html\" href=\"{}\"/>",
xml(&e.link)
);
let by = e.author.as_deref().unwrap_or(title);
let _ = writeln!(out, "<author><name>{}</name></author>", xml(by));
let kind = if e.html { "html" } else { "text" };
let _ = writeln!(
out,
"<content type=\"{kind}\">{}</content>",
xml(&e.content)
);
out += "</entry>\n";
}
out += "</feed>\n";
out
}
/// `s` as XML text: its markup escaped, and the control characters XML
/// cannot hold left out.
fn xml(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out += "&",
'<' => out += "<",
'>' => out += ">",
'"' => out += """,
'\t' | '\n' | '\r' => out.push(c),
c if c < ' ' || c == '\u{fffe}' || c == '\u{ffff}' => {}
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xml_escapes_markup_and_drops_what_it_cannot_hold() {
assert_eq!(
xml("a <b> & \"c\"\u{1}\u{fffe}\u{ffff}\n"),
"a <b> & "c"\n"
);
}
#[test]
fn an_empty_feed_is_dated_at_the_epoch() {
let feed = atom("T", "https://x/f.atom", "https://x/p", Vec::new());
assert!(feed.contains("<updated>1970-01-01T00:00:00Z</updated>"));
assert!(!feed.contains("<entry>"));
}
}