const PREAMBLE: &str = "# Changelog\n\n\
All notable changes to this project are documented in this file.\n\n\
The format follows \
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project \
adheres to \
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n";
#[derive(
Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize,
)]
pub struct Changelog {
pub configuration: crate::Configuration,
pub introduction: Option<String>,
pub references: std::collections::BTreeMap<String, String>,
pub sections: Vec<crate::Section>,
}
impl Changelog {
pub fn to_ron(&self) -> sysexits::Result<String> {
let pretty = ron::ser::PrettyConfig::new().indentor(" ".to_owned());
match ron::ser::to_string_pretty(self, pretty) {
Ok(body) => Ok(format!("{body}\n")),
Err(reason) => {
eprintln!(
"git-harvest: cannot serialise the CHANGELOG: {reason}"
);
Err(sysexits::ExitCode::Software)
}
}
}
#[must_use]
pub fn to_markdown(&self) -> String {
let mut out = String::from(PREAMBLE);
if let Some(lead) = &self.introduction {
out.push_str(lead);
out.push_str("\n\n");
}
for section in &self.sections {
if section.released.is_some() {
out.push_str(§ion_markdown(&self.configuration, section));
}
}
out.push_str(&reference_block(self));
format!("{}\n", out.trim_end())
}
}
fn bucket_markdown(bucket: &str, entries: &[crate::Entry]) -> String {
if entries.is_empty() {
return String::new();
}
let mut out = format!("### {bucket}\n\n");
for entry in entries {
out.push_str("- ");
out.push_str(entry.text());
out.push('\n');
}
out.push('\n');
out
}
fn reference_block(changelog: &Changelog) -> String {
let mut references = changelog.references.clone();
for section in &changelog.sections {
if section.released.is_some() {
for (label, url) in §ion.references {
references
.entry(label.clone())
.or_insert_with(|| url.clone());
}
}
}
let mut out = String::new();
for (label, url) in &references {
out.push('[');
out.push_str(label);
out.push_str("]: ");
out.push_str(url);
out.push('\n');
}
out
}
fn section_markdown(
configuration: &crate::Configuration,
section: &crate::Section,
) -> String {
let date = section
.released
.map(|moment| moment.format("%Y-%m-%d").to_string())
.unwrap_or_default();
let mut out = format!(
"## [{}.{}.{}] - {date}\n\n",
section.version.major, section.version.minor, section.version.patch,
);
if let Some(lead) = §ion.introduction {
out.push_str(lead);
out.push_str("\n\n");
}
let mut seen = std::collections::BTreeSet::new();
for bucket in &configuration.buckets {
if let Some(entries) = section.changes.get(bucket) {
out.push_str(&bucket_markdown(bucket, entries));
seen.insert(bucket);
}
}
for (bucket, entries) in §ion.changes {
if !seen.contains(bucket) {
out.push_str(&bucket_markdown(bucket, entries));
}
}
out
}