//! the cards a generation serves: a set per repo, with its card, one per ticket and pull and
//! one per commit svfe renders, and one per owner
use std::path::Path;
use knot_types::Oid;
use serde::Serialize;
use super::{CARDS_DIR, COMMITS, Card, Lazy, Plan, Planned, REPO_CARD, Ribbon, SETS_DIR, Set, key};
use crate::store::RepoStore;
/// a hosted repo's records, as the social reads show them
#[derive(Serialize)]
pub(crate) struct SocialRepo {
pub(crate) did: String,
pub(crate) owner_did: String,
pub(crate) owner_handle: String,
pub(crate) name: String,
pub(crate) description: String,
pub(crate) topics: Vec<String>,
pub(crate) open_tickets: usize,
pub(crate) open_pulls: usize,
pub(crate) tickets: Vec<SocialTicket>,
}
#[derive(Serialize)]
pub(crate) struct SocialTicket {
pub(crate) rkey: String,
pub(crate) title: String,
/// open, closed, completed or merged
pub(crate) state: String,
pub(crate) author_did: String,
pub(crate) author_handle: String,
pub(crate) created_at: String,
pub(crate) body: String,
pub(crate) pull: Option<SocialPull>,
}
#[derive(Serialize)]
pub(crate) struct SocialPull {
pub(crate) from: String,
pub(crate) into: String,
/// the newest round's range
pub(crate) range: Option<(Oid, Oid)>,
}
pub(crate) struct SocialUser {
pub(crate) did: String,
pub(crate) handle: String,
pub(crate) about: String,
pub(crate) repos: Vec<String>,
/// the biggest of the avatar's sized copies, and its cid
pub(crate) avatar: Option<(std::path::PathBuf, String)>,
}
/// where a repo's git is read from, and its git reads' seal
pub(crate) struct GitRepo<'a> {
pub(crate) did: &'a str,
pub(crate) export: &'a Path,
pub(crate) served: RepoStore,
pub(crate) seal: &'a str,
}
/// a set for each repo, drawn only when no earlier write left one under its name, and the
/// owners' cards
pub(crate) fn plan(
output_root: &Path,
host: &str,
repos: Vec<SocialRepo>,
users: Vec<SocialUser>,
git: &[GitRepo],
) -> anyhow::Result<Plan> {
let sets_dir = output_root.join(CARDS_DIR).join(SETS_DIR);
let mut sets = Vec::new();
for repo in repos {
let git = git.iter().find(|git| git.did == repo.did);
// everything the cards show is read from these, the git through the seal's refs
let name = key(&(host, &repo, git.map(|git| git.seal)))?;
let did = repo.did.clone();
let cards = match sets_dir.join(&name).is_dir() {
true => Vec::new(),
false => repo_cards(host, repo, git)?,
};
sets.push(Set { did, name, cards });
}
let mut owners = Vec::new();
for user in users {
let card = Card::User {
host: host.to_string(),
handle: format!("@{}", user.handle),
about: user.about,
repo_count: user.repos.len(),
repos: user.repos,
avatar: None,
};
let key = key(&(&card, user.avatar.as_ref().map(|(_, cid)| cid)))?;
owners.push(Planned {
path: format!("{}.png", user.did),
key,
card: Lazy::Avatar {
card,
file: user.avatar.map(|(file, _)| file),
},
});
}
Ok(Plan {
sets,
users: owners,
})
}
/// a repo's cards at their paths in its set
fn repo_cards(host: &str, repo: SocialRepo, git: Option<&GitRepo>) -> anyhow::Result<Vec<Planned>> {
let ribbon = Ribbon {
host: host.to_string(),
path: format!("{} / {}", repo.owner_handle, repo.name),
};
let Log {
branch,
tip,
commits,
} = git.map(log).unwrap_or_default();
let card = Card::Repo {
ribbon: Ribbon {
host: host.to_string(),
path: repo.owner_handle.clone(),
},
name: repo.name.clone(),
description: repo.description.clone(),
topics: repo.topics.clone(),
open_tickets: repo.open_tickets,
open_pulls: repo.open_pulls,
branch: branch.clone(),
languages: Vec::new(),
};
let tip = tip.map(|(commit, tree)| (commit.to_hex(), tree.to_hex()));
// the languages are read from the tree, so a push that keeps it keeps the card
let repo_key = key(&(&card, tip.as_ref().map(|(_, tree)| tree)))?;
let lazy = match (git, tip) {
(Some(git), Some((commit, _))) => Lazy::Languages {
card,
served: git.served.clone(),
seal: git.seal.to_string(),
commit,
},
_ => Lazy::Ready(card),
};
let mut planned = vec![Planned {
path: REPO_CARD.to_string(),
key: repo_key,
card: lazy,
}];
for ticket in repo.tickets {
planned.push(ticket_card(&ribbon, ticket, git)?);
}
let Some(git) = git else {
return Ok(planned);
};
for oid in commits {
planned.push(Planned {
path: format!("commit/{}.png", oid.to_hex()),
key: key(&(&ribbon, &branch, oid.to_hex()))?,
card: Lazy::Commit {
export: git.export.to_path_buf(),
oid,
ribbon: ribbon.clone(),
branch: branch.clone(),
},
});
}
Ok(planned)
}
fn ticket_card(
ribbon: &Ribbon,
ticket: SocialTicket,
git: Option<&GitRepo>,
) -> anyhow::Result<Planned> {
let author = match ticket.author_handle.starts_with("did:") {
true => ticket.author_handle.clone(),
false => format!("@{}", ticket.author_handle),
};
let date = super::day(&ticket.created_at);
let body = super::prose(&ticket.body);
let Some(pull) = ticket.pull else {
let card = Card::Ticket {
ribbon: ribbon.clone(),
number: ticket.rkey.clone(),
title: ticket.title,
state: ticket.state,
author,
date,
body,
};
return Ok(Planned {
path: format!("issues/{}.png", ticket.rkey),
key: key(&card)?,
card: Lazy::Ready(card),
});
};
let card = Card::Pull {
ribbon: ribbon.clone(),
number: ticket.rkey.clone(),
title: ticket.title,
state: ticket.state,
author,
hue: crate::index_page::hue_index(&ticket.author_did),
seed: ticket.rkey.clone(),
date,
from: pull.from,
into: pull.into,
body,
diff: None,
};
let range = pull
.range
.map(|(base, head)| (base.to_hex(), head.to_hex()));
let key = key(&(&card, &range))?;
let card = match (pull.range, git) {
(Some((base, head)), Some(git)) => Lazy::PullDiff {
card,
export: git.export.to_path_buf(),
base,
head,
},
_ => Lazy::Ready(card),
};
Ok(Planned {
path: format!("pulls/{}.png", ticket.rkey),
key,
card,
})
}
#[derive(Default)]
struct Log {
branch: String,
/// the default branch's commit and its tree
tip: Option<(Oid, Oid)>,
/// its newest commits, as svfe's log lists them
commits: Vec<Oid>,
}
fn log(git: &GitRepo) -> Log {
let read = || -> anyhow::Result<Log> {
let repo = knot_git::Repo::open(git.export)?;
let Some(head) = repo.head() else {
return Ok(Log::default());
};
let tip = repo.find_commit(repo.peel_to_commit(head.target)?)?;
let window = repo.log_window_bounded(
tip.id,
knot_git::LogSkip::new(0),
knot_git::LogLimit::new(COMMITS),
)?;
Ok(Log {
branch: head.name.as_str().to_string(),
tip: Some((tip.id, tip.tree)),
commits: window.iter().map(|commit| commit.id).collect(),
})
};
read().unwrap_or_else(|error| {
tracing::warn!(repo = git.did, "no commit cards: {error:#}");
Log::default()
})
}