use crate::{Fragment, FromRon, FromRst, FromXml, ToRon, Version};
use aeruginous_io::{PathBufLikeReader, PathBufLikeTruncation};
use chrono::{DateTime, Local};
use std::{path::PathBuf, str::FromStr};
use sysexits::{ExitCode, Result};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
Init,
Release,
}
crate::enum_trait!(Action {
Init <-> "init",
Release <-> "release"
});
#[derive(serde::Deserialize, serde::Serialize)]
struct Changelog {
references: References,
introduction: Option<String>,
sections: Vec<Section>,
}
impl Changelog {
fn add_section(&mut self, section: Section) {
for s in &mut self.sections {
if s == §ion {
s.merge(section);
return;
}
}
self.sections
.insert(self.sections.partition_point(|s| s > §ion), section);
}
fn init(
path: &PathBuf,
message: Option<String>,
references: References,
force: bool,
) -> Result<bool> {
let result = !path.exists();
if result || force {
Self::new(message, references)
.to_ron(2)?
.truncate_loudly(path)?;
}
Ok(result)
}
#[must_use]
const fn new(introduction: Option<String>, references: References) -> Self {
Self {
references,
introduction,
sections: Vec::new(),
}
}
}
struct Logic {
cli: Ronlog,
hyperlinks: References,
}
impl Logic {
fn init(&self, message: Option<String>) -> Result<()> {
if Changelog::init(
&self.cli.output_file,
message,
self.hyperlinks.clone(),
self.cli.force,
)? {
println!(
"Successfully initialised new CHANGELOG in '{}'.",
self.cli.output_file.display()
);
Ok(())
} else if self.cli.force {
println!(
"Successfully re-initialised CHANGELOG in '{}'.",
self.cli.output_file.display()
);
Ok(())
} else {
println!(
"Use `--force` to overwrite the existing CHANGELOG in '{}'.",
self.cli.output_file.display()
);
Err(ExitCode::Usage)
}
}
fn main(&mut self) -> Result<()> {
self.hyperlinks = self
.cli
.link
.iter()
.zip(self.cli.target.iter())
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect();
match self.cli.action {
Action::Init => self.init(self.cli.message.clone()),
Action::Release => self.release(),
}
}
fn release(&self) -> Result<()> {
if let Some(version) = &self.cli.version {
let mut section = Section::new(
Fragment::default(),
version,
self.cli.message.clone(),
if self.hyperlinks.is_empty() {
None
} else {
Some(self.hyperlinks.clone())
},
)?;
if let Some(timestamp) = &self.cli.timestamp {
section.release_at(
DateTime::parse_from_str(timestamp, "%Y%m%dT%H%M%S%z")
.map_or(Err(ExitCode::DataErr), Ok)?,
);
}
if !self.cli.output_file.exists() {
self.init(None)?;
}
if std::path::Path::new(&self.cli.input_directory).exists()
|| self.cli.crash_if_empty
{
for entry in std::fs::read_dir(&self.cli.input_directory)? {
let entry = entry?.path();
if let Some(extension) = entry.extension() {
match extension.to_str() {
Some("ron") => {
section.add_changes(Fragment::from_ron(
&entry.read_loudly()?,
)?);
std::fs::remove_file(entry)?;
}
Some("rst") => {
section.add_changes(Fragment::from_rst(
&entry.read_loudly()?,
)?);
std::fs::remove_file(entry)?;
}
Some("xml") => {
section.add_changes(Fragment::from_xml(
&entry.read_loudly()?,
)?);
std::fs::remove_file(entry)?;
}
Some(_) | None => {}
}
}
}
}
let mut ronlog =
Changelog::from_ron(&self.cli.output_file.read_loudly()?)?;
for section in &mut ronlog.sections {
section.changes.sort();
}
for (link, target) in section.move_references() {
ronlog
.references
.entry(link)
.and_modify(|t| t.clone_from(&target))
.or_insert(target);
}
section.changes.sort();
ronlog.add_section(section);
ronlog
.to_ron(2)?
.truncate_loudly(self.cli.output_file.clone())
} else {
eprintln!("No `--version` information provided for this mode.");
Err(ExitCode::Usage)
}
}
}
pub type References = indexmap::IndexMap<String, String>;
#[derive(clap::Parser, Clone)]
pub struct Ronlog {
action: Action,
#[arg(long)]
crash_if_empty: bool,
#[arg(long, short)]
force: bool,
#[arg(default_value = ".", long = "input", short)]
input_directory: String,
#[arg(long, short)]
message: Option<String>,
#[arg(long, short, visible_aliases = ["hyperlink"])]
link: Vec<String>,
#[arg(default_value = "CHANGELOG.ron", long = "output", short)]
output_file: PathBuf,
#[arg(long, short)]
target: Vec<String>,
#[arg(long, short = 'T', visible_aliases = ["when"])]
timestamp: Option<String>,
#[arg(long, short)]
version: Option<String>,
}
impl Ronlog {
pub fn main(&self) -> Result<()> {
self.wrap().main()
}
fn wrap(&self) -> Logic {
Logic {
cli: self.clone(),
hyperlinks: References::new(),
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Section {
references: References,
version: Version,
released: DateTime<Local>,
introduction: Option<String>,
changes: Fragment,
}
impl Section {
crate::getters!(@fn @ref
references: References,
version: Version,
released: DateTime<Local>,
introduction: Option<String>,
changes: Fragment
);
pub fn add_changes(&mut self, changes: Fragment) {
self.changes.merge(changes);
for (link, target) in self.changes.move_references() {
self.references
.entry(link)
.and_modify(|t| t.clone_from(&target))
.or_insert(target);
}
}
pub fn merge(&mut self, mut other: Self) {
if self.version == other.version {
self.add_changes(other.changes.clone());
match &self.introduction {
Some(introduction_1) => {
if let Some(introduction_2) = &other.introduction {
let mut introduction_1 = introduction_1.clone();
introduction_1.push('\n');
introduction_1.push_str(introduction_2.as_str());
self.introduction = Some(introduction_1);
}
}
None => self.introduction.clone_from(&other.introduction),
}
for (link, target) in other.move_references() {
self.references
.entry(link)
.and_modify(|t| t.clone_from(&target))
.or_insert(target);
}
self.released = self.released.max(other.released);
}
}
#[must_use]
pub fn move_references(&mut self) -> References {
let result = self.references.clone();
self.references.clear();
result
}
pub fn new(
mut changes: Fragment,
version: &str,
introduction: Option<String>,
references: Option<References>,
) -> sysexits::Result<Self> {
let mut references = references.unwrap_or_default();
for (link, target) in changes.move_references() {
references
.entry(link)
.and_modify(|t| t.clone_from(&target))
.or_insert(target);
}
Ok(Self {
references,
version: Version::from_str(version)?,
released: Local::now(),
introduction,
changes,
})
}
pub fn release_at<T>(&mut self, when: DateTime<T>)
where
DateTime<Local>: From<DateTime<T>>,
T: chrono::TimeZone,
{
self.released = when.into();
}
}
impl Eq for Section {}
impl Ord for Section {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.version.cmp(&other.version)
}
}
impl PartialEq for Section {
fn eq(&self, other: &Self) -> bool {
self.version == other.version
}
}
impl PartialOrd for Section {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}