use crate::{FragmentExportFormat, PatternWriter, ToMd, ToRon, ToRst};
use git2::Repository;
use std::collections::HashMap;
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(
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 = 'S')]
stop_at: Option<git2::Oid>,
#[arg(long, short)]
target: Vec<String>,
}
impl CommentChanges {
pub fn main(&self) -> Result<()> {
self.wrap().main()
}
#[must_use]
pub fn new(delimiter: String) -> Self {
Self {
body: false,
category: Vec::new(),
delimiter,
depth: None,
extension: FragmentExportFormat::Rst,
fallback_category: None,
heading: 3,
keep_a_changelog: false,
link: Vec::new(),
output_directory: ".".to_string(),
stop_at: None,
target: Vec::new(),
}
}
fn wrap(&self) -> Logic {
Logic {
branch: String::new(),
categories: Vec::new(),
changes: HashMap::new(),
cli: self.clone(),
hyperlinks: HashMap::new(),
repository: None,
user: String::new(),
}
}
}
struct Logic {
branch: String,
categories: Vec<String>,
changes: HashMap<String, Vec<String>>,
cli: CommentChanges,
hyperlinks: crate::RonlogReferences,
repository: Option<Repository>,
user: String,
}
impl Logic {
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 insert(
map: &mut HashMap<String, Vec<String>>,
category: String,
change: String,
) {
map.entry(category.clone()).or_default();
let mut changes = map[&category].clone();
changes.push(change);
map.insert(category, changes);
}
fn main(&mut self) -> Result<()> {
self.preprocess()?;
self.query()?;
self.report()
}
fn preprocess(&mut self) -> Result<()> {
if self.cli.keep_a_changelog {
self.categories.append(
&mut vec![
"Added",
"Changed",
"Deprecated",
"Fixed",
"Removed",
"Security",
]
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>(),
);
}
self.categories.append(&mut self.cli.category.clone());
self.hyperlinks = 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);
Ok(())
},
)
}
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;
let mut result = HashMap::new();
for oid in revwalk {
if let Some(depth) = self.cli.depth {
if count > depth {
break;
}
}
if let Ok(oid) = oid {
if let Some(stop_at) = self.cli.stop_at {
if stop_at == oid {
break;
}
}
if let Ok(commit) = repository.find_commit(oid) {
if let Some(message) = if self.cli.body {
commit.body()
} else {
commit.summary()
} {
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 {
Self::insert(&mut result, category, change);
} else if !valid_category {
if let Some(fallback) = &self.cli.fallback_category {
Self::insert(
&mut result,
fallback.to_string(),
change,
);
}
}
} else if let Some(fallback) = &self.cli.fallback_category {
Self::insert(
&mut result,
fallback.to_string(),
message.trim().to_string(),
);
}
}
} else {
eprintln!("Commit {oid} does not seem to exist.");
return Err(ExitCode::DataErr);
}
} else {
eprintln!("There were not enough commits fetched on checkout.");
return Err(ExitCode::Usage);
}
count += 1;
}
self.changes = result;
Ok(())
}
Err(error) => {
eprintln!("{error}");
Err(ExitCode::Unavailable)
}
},
Err(error) => {
eprintln!("{error}");
Err(ExitCode::Unavailable)
}
}
} else {
Err(ExitCode::Software)
}
}
fn report(&mut self) -> Result<()> {
let fragment = crate::Fragment::new(&self.hyperlinks, &self.changes);
let content = match self.cli.extension {
FragmentExportFormat::Md => fragment.to_md(self.cli.heading),
FragmentExportFormat::Ron => fragment.to_ron(2),
FragmentExportFormat::Rst => fragment.to_rst(self.cli.heading),
}?;
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()?;
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
)
.append(Box::new(content))
}
}