use crate::{FragmentExportFormat, ToMd, ToRon, ToRst, ToXml};
use aeruginous_io::PathBufLikeAppendix;
use git2::{Oid, Repository};
use sysexits::{ExitCode, Result};
#[derive(clap::Parser, Clone)]
#[command(visible_aliases = ["changelog"])]
pub struct CommentChanges {
#[arg(long, short)]
body: bool,
#[arg(long, short)]
category: Vec<String>,
#[arg(long, short)]
delimiter: String,
#[arg(long, short = 'n', visible_aliases = ["count"])]
depth: Option<usize>,
#[arg(
default_value = "rst",
long,
short = 'f',
visible_aliases = ["format"]
)]
extension: FragmentExportFormat,
#[arg(long, short = 'C')]
fallback_category: Option<String>,
#[arg(long, short = 'F')]
force: bool,
#[arg(
default_value = "3",
long,
short = 'H',
value_parser = clap::value_parser!(u8).range(1..=3),
visible_aliases = ["level"]
)]
heading: u8,
#[arg(long, short)]
keep_a_changelog: bool,
#[arg(long, short, visible_aliases = ["hyperlink"])]
link: Vec<String>,
#[arg(
default_value = ".",
long = "output",
short,
visible_aliases = ["dir", "directory"]
)]
output_directory: String,
#[arg(long, short = '@')]
stop: Vec<String>,
#[arg(long, short = 'S')]
#[deprecated(since = "3.7.4", note = "use `Self::stop` instead")]
stop_at: Option<Oid>,
#[arg(long, short = 'T')]
#[deprecated(since = "3.7.4", note = "use `Self::stop` instead")]
tag: Option<String>,
#[arg(long, short)]
target: Vec<String>,
}
impl CommentChanges {
pub fn main(&self) -> Result<()> {
self.wrap().main()
}
#[allow(deprecated)]
#[must_use]
pub fn new(delimiter: String) -> Self {
Self {
body: false,
category: Vec::new(),
delimiter,
depth: None,
extension: FragmentExportFormat::Rst,
fallback_category: None,
force: false,
heading: 3,
keep_a_changelog: false,
link: Vec::new(),
output_directory: ".".to_string(),
stop: Vec::new(),
stop_at: None,
tag: None,
target: Vec::new(),
}
}
fn wrap(&self) -> Logic {
Logic {
branch: String::new(),
categories: Vec::new(),
cli: self.clone(),
fragment: crate::Fragment::default(),
repository: None,
stop_conditions: Vec::new(),
user: String::new(),
}
}
}
struct Logic {
branch: String,
categories: Vec<String>,
cli: CommentChanges,
fragment: crate::Fragment,
repository: Option<Repository>,
stop_conditions: Vec<Oid>,
user: String,
}
impl Logic {
fn analyse_stop_condition(&mut self) -> Result<()> {
for stop in &self.cli.stop {
if let Some(repository) = &self.repository {
if let Ok(target) =
repository.resolve_reference_from_short_name(stop)
{
if let Some(oid) = target.target() {
self.stop_conditions.push(oid);
} else {
eprintln!("`{stop}` cannot be used as stop condition.");
return Err(ExitCode::Usage);
}
} else {
let oid = git2::Oid::from_str(stop).map_or_else(
|_| {
eprintln!("`{stop}` does not seem to exist.");
Err(ExitCode::Usage)
},
Ok,
)?;
if repository.find_commit(oid).is_ok() {
self.stop_conditions.push(oid);
} else {
eprintln!("There is no such commit `{stop}`.");
return Err(ExitCode::Usage);
}
}
} else {
return Err(ExitCode::Software);
}
}
Ok(())
}
fn get_branch(&mut self) -> Result<()> {
if let Some(repository) = &self.repository {
self.branch = repository.head().map_or_else(
|error| {
eprintln!("{error}");
Err(ExitCode::Unavailable)
},
|reference| {
reference
.name()
.map_or(Err(ExitCode::Unavailable), |name| {
Ok(name.to_string())
})
},
)?;
Ok(())
} else {
Err(ExitCode::Software)
}
}
fn get_user(&mut self) -> Result<()> {
if let Some(repository) = &self.repository {
self.user =
repository
.config()
.map_or_else(
|error| {
eprintln!("{error}");
Err(ExitCode::Unavailable)
},
|config| {
config.get_string("user.name").map_or_else(
|_| {
eprintln!("There is no Git username configured, yet.");
Err(ExitCode::DataErr)
},
Ok,
)
},
)?
.replace(' ', "_");
Ok(())
} else {
Err(ExitCode::Software)
}
}
fn harvest_message(&self, message: &str) -> Option<(String, String)> {
if message.is_empty() {
None
} else if let Some((category, change)) =
message.trim().split_once(&self.cli.delimiter)
{
let category = category.trim().to_string();
let change = change.trim().to_string();
let valid_category = self.categories.iter().any(|c| c == &category);
if self.categories.is_empty() || valid_category {
Some((category, change))
} else if !valid_category {
self.cli
.fallback_category
.as_ref()
.map(|fallback| (fallback.to_string(), change))
} else {
None
}
} else {
self.cli.fallback_category.as_ref().map(|fallback| {
(fallback.to_string(), message.trim().to_string())
})
}
}
fn main(&mut self) -> Result<()> {
self.preprocess()?;
self.query()?;
self.report()
}
#[allow(deprecated)]
fn preprocess(&mut self) -> Result<()> {
if self.cli.keep_a_changelog {
self.categories.append(
&mut [
"Added",
"Changed",
"Deprecated",
"Fixed",
"Removed",
"Security",
]
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>(),
);
}
self.categories.append(&mut self.cli.category.clone());
self.fragment.reference(
self.cli
.link
.iter()
.zip(self.cli.target.iter())
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect(),
);
Repository::open(".").map_or_else(
|_| {
eprintln!("This is not a Git repository.");
Err(ExitCode::Usage)
},
|r| {
self.repository = Some(r);
self.analyse_stop_condition()?;
if let Some(oid) = &self.cli.stop_at {
self.stop_conditions.push(*oid);
}
if let Some(tag) = &self.cli.tag {
if let Some(repository) = &self.repository {
if let Ok(target) =
repository.resolve_reference_from_short_name(tag)
{
if target.is_tag() {
if let Some(oid) = target.target() {
self.stop_conditions.push(oid);
Ok(())
} else {
eprintln!("`{tag}` cannot be used as stop condition."); Err(ExitCode::Usage)
}
} else {
eprintln!("{tag} does not seem to be a tag.");
Err(ExitCode::Usage)
}
} else {
eprintln!("Tag {tag} does not seem to exist.");
Err(ExitCode::Usage)
}
} else {
Err(ExitCode::Software)
}
} else {
Ok(())
}
},
)
}
#[allow(deprecated)]
fn query(&mut self) -> Result<()> {
if let Some(repository) = &self.repository {
match repository.revwalk() {
Ok(mut revwalk) => match revwalk.push_head() {
Ok(()) => {
let mut count = 1;
for oid in revwalk {
if let Some(depth) = self.cli.depth {
if count > depth {
break;
}
}
if let Ok(oid) = oid {
if self.stop_conditions.contains(&oid) {
break;
}
if let Ok(commit) = repository.find_commit(oid)
{
if let Some(message) = commit.message() {
let (summary, body) = message
.split_once('\n')
.unwrap_or((message, ""));
if let Some((category, change)) = self
.harvest_message(if self.cli.body {
body.trim()
} else {
summary.trim()
})
{
self.fragment
.insert(&category, &change);
} else if self.cli.force {
if let Some((category, change)) =
self.harvest_message(
if self.cli.body {
summary.trim()
} else {
body.trim()
},
)
{
self.fragment
.insert(&category, &change);
}
}
}
} else {
eprintln!(
"Commit {oid} does not seem to exist."
);
return Err(ExitCode::DataErr);
}
} else {
eprintln!(
"Too few commits were fetched on checkout."
);
return Err(ExitCode::Usage);
}
count += 1;
}
Ok(())
}
Err(error) => {
eprintln!("{error}");
Err(ExitCode::Unavailable)
}
},
Err(error) => {
eprintln!("{error}");
Err(ExitCode::Unavailable)
}
}
} else {
Err(ExitCode::Software)
}
}
fn report(&mut self) -> Result<()> {
self.fragment.sort();
let content = match self.cli.extension {
FragmentExportFormat::Md => self.fragment.to_md(self.cli.heading),
FragmentExportFormat::Ron => self.fragment.to_ron(2),
FragmentExportFormat::Rst => self.fragment.to_rst(self.cli.heading),
FragmentExportFormat::Xml => self.fragment.to_xml(),
}?;
if !std::path::Path::new(&self.cli.output_directory).try_exists()? {
std::fs::create_dir_all(&self.cli.output_directory)?;
}
self.get_branch()?;
self.get_user()?;
content.append_loudly(format!(
"{}/{}_{}_{}.{}",
self.cli.output_directory,
chrono::Local::now().format("%Y%m%d_%H%M%S"),
self.user,
self.branch.split('/').last().unwrap_or("HEAD"),
self.cli.extension
))
}
}