use crate::{error::Result, versioning};
use conventional_commit_parser::commit::CommitType;
use std::collections::HashMap;
const TEMPLATE: &str = include_str!("templates/changelog_entry.md");
#[derive(Debug, serde::Serialize)]
pub struct Entry<'a> {
pub version: &'a str,
pub commits: HashMap<String, Vec<Commit>>,
pub description: Option<String>,
}
#[derive(Clone, Debug)]
pub struct Commit {
pub scope: Option<String>,
pub summary: String,
pub hash: String,
pub author: String,
}
impl std::fmt::Display for Commit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.scope {
Some(ref scope) => write!(
f,
"**({0})** {1} - ({2}) - {3}",
scope, self.summary, self.hash, self.author
),
None => write!(f, "{0} - ({1}) - {2}", self.summary, self.hash, self.author),
}
}
}
impl serde::Serialize for Commit {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[must_use]
pub fn display_commit_type(commit_type: &CommitType) -> String {
match commit_type {
CommitType::Feature => "features".to_string(),
CommitType::BugFix => "bug fixes".to_string(),
CommitType::Refactor => "refactors".to_string(),
CommitType::Chore => "chores".to_string(),
CommitType::Documentation => "documentation".to_string(),
CommitType::Style => "style".to_string(),
CommitType::Test => "tests".to_string(),
CommitType::Build => "build system".to_string(),
CommitType::Revert => "reverts".to_string(),
CommitType::Ci => "continuous integration".to_string(),
CommitType::Performances => "performance".to_string(),
CommitType::Custom(custom) => custom.to_string(),
}
}
pub fn generate_changelog_entry<
'a,
I: IntoIterator<Item = &'a versioning::Commit>,
S: ::std::hash::BuildHasher,
>(
repo: &gix::Repository,
commits: I,
version: &str,
description: Option<String>,
authors: Option<&HashMap<String, String, S>>,
) -> Result<String> {
let mut env = minijinja::Environment::new();
env.add_template("changelog_entry", TEMPLATE)?;
let url = gix_repo_url(repo)?;
let version = match &url {
Some((host, path)) => &format!("[{version}](https://{host}/{path}/releases/tag/{version})"),
None => version,
};
let version = &format!("{} - {}", version, chrono::Local::now().format("%Y-%m-%d"));
let typed_commits: HashMap<String, Vec<Commit>> =
commits.into_iter().fold(HashMap::new(), |mut acc, commit| {
let key = display_commit_type(&commit.conventional_commit.commit_type);
let entry = acc.entry(key).or_default();
let author = author_name(commit.signature.name.to_string(), authors, url.as_ref());
let commit_id = commit.commit_id.to_string();
let hash = match &url {
Some((host, path)) => format!(
"[{}](https://{host}/{path}/commit/{commit_id})",
&commit_id[..7]
),
None => commit_id,
};
let commit = Commit {
scope: commit.conventional_commit.scope.clone(),
summary: commit.conventional_commit.summary.clone(),
hash,
author,
};
entry.push(commit);
acc
});
let entry = Entry {
version,
commits: typed_commits,
description,
};
let template = env.get_template("changelog_entry")?;
template
.render(minijinja::context!(
entry => entry,
))
.map_err(Into::into)
}
fn gix_repo_url(repo: &gix::Repository) -> Result<Option<(String, String)>> {
let remote = match repo.find_default_remote(gix::remote::Direction::Push) {
Some(remote) => remote?,
None => return Ok(None),
};
match remote.url(gix::remote::Direction::Push) {
Some(url) => {
let host = url.host_argument_safe();
let path = url.path_argument_safe();
match (host, path) {
(Some(host), Some(path)) => Ok(Some((
host.to_string(),
remove_suffix(&path.to_string(), ".git").to_string(),
))),
_ => Ok(None),
}
}
None => Ok(None),
}
}
fn remove_suffix<'a>(input: &'a str, suffix: &str) -> &'a str {
if let Some(stripped) = input.strip_suffix(suffix) {
stripped
} else {
input
}
}
fn author_name<S: ::std::hash::BuildHasher>(
commit_author: String,
authors: Option<&HashMap<String, String, S>>,
url: Option<&(String, String)>,
) -> String {
match url {
Some((host, _)) => {
if let Some(authors) = authors {
authors
.get(&commit_author)
.cloned()
.map(|author| format!("[@{author}](https://{host}/{author})"))
.unwrap_or(commit_author)
} else {
commit_author
}
}
None => commit_author,
}
}