use crate::{Fragment, FromRon, PatternWriter, ReadFile, ToRon, Version};
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 {
path.truncate(Box::new(Self::new(message, references).to_ron(2)?))?;
}
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 !self.cli.output_file.exists() {
self.init(None)?;
}
for entry in std::fs::read_dir(&self.cli.input_directory)? {
let entry = entry?.path();
if entry
.extension()
.map_or(false, |e| e.to_str().map_or(false, |e| e == "ron"))
{
if let Ok(fragment) = Fragment::from_ron(&entry.read()?) {
section.add_changes(fragment);
std::fs::remove_file(entry)?;
}
}
}
let mut ronlog = Changelog::from_ron(&self.cli.output_file.read()?)?;
for (link, target) in section.move_references() {
ronlog
.references
.entry(link)
.and_modify(|t| *t = target.clone())
.or_insert(target);
}
ronlog.add_section(section);
self.cli.output_file.truncate(Box::new(ronlog.to_ron(2)?))
} else {
eprintln!("No `--version` information provided for this mode.");
Err(ExitCode::Usage)
}
}
}
pub type References = std::collections::HashMap<String, String>;
#[derive(clap::Parser, Clone)]
pub struct Ronlog {
action: Action,
#[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)]
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 = target.clone())
.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 = other.introduction.clone(),
}
for (link, target) in other.move_references() {
self
.references
.entry(link)
.and_modify(|t| *t = target.clone())
.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 = target.clone())
.or_insert(target);
}
Ok(Self {
references,
version: Version::from_str(version)?,
released: Local::now(),
introduction,
changes,
})
}
}
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))
}
}