~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
262 lines · 8.4 KB Raw History
//! 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 += "&amp;",
            '<' => out += "&lt;",
            '>' => out += "&gt;",
            '"' => out += "&quot;",
            '\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 &lt;b&gt; &amp; &quot;c&quot;\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>"));
    }
}