use anyhow::Context;
use git_cliff_core::config::{Bump, ChangelogConfig, RemoteConfig};
use regex::Regex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug, Clone, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ChangelogCfg {
pub header: Option<String>,
pub body: Option<String>,
pub trim: Option<bool>,
pub commit_preprocessors: Option<Vec<TextProcessor>>,
pub postprocessors: Option<Vec<TextProcessor>>,
pub sort_commits: Option<Sorting>,
pub link_parsers: Option<Vec<LinkParser>>,
pub commit_parsers: Option<Vec<CommitParser>>,
pub protect_breaking_commits: Option<bool>,
pub tag_pattern: Option<String>,
}
impl ChangelogCfg {
pub fn is_default(&self) -> bool {
let default_config = ChangelogCfg::default();
&default_config == self
}
}
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug, Clone, JsonSchema)]
pub struct TextProcessor {
pub pattern: String,
pub replace: Option<String>,
pub replace_command: Option<String>,
}
impl TryFrom<TextProcessor> for git_cliff_core::config::TextProcessor {
fn try_from(cfg: TextProcessor) -> Result<Self, Self::Error> {
Ok(Self {
pattern: to_regex(&cfg.pattern, "pattern")?,
replace: cfg.replace,
replace_command: cfg.replace_command,
})
}
type Error = anyhow::Error;
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sorting {
Oldest,
Newest,
}
impl std::fmt::Display for Sorting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Sorting::Oldest => write!(f, "oldest"),
Sorting::Newest => write!(f, "newest"),
}
}
}
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug, Clone, JsonSchema)]
pub struct LinkParser {
pub pattern: String,
pub href: String,
pub text: Option<String>,
}
impl TryFrom<LinkParser> for git_cliff_core::config::LinkParser {
type Error = anyhow::Error;
fn try_from(value: LinkParser) -> Result<Self, Self::Error> {
Ok(Self {
pattern: to_regex(&value.pattern, "pattern")?,
href: value.href,
text: value.text,
})
}
}
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug, Clone, JsonSchema)]
pub struct CommitParser {
pub message: Option<String>,
pub body: Option<String>,
pub group: Option<String>,
pub default_scope: Option<String>,
pub scope: Option<String>,
pub skip: Option<bool>,
pub field: Option<String>,
pub pattern: Option<String>,
pub sha: Option<String>,
}
impl TryFrom<CommitParser> for git_cliff_core::config::CommitParser {
type Error = anyhow::Error;
fn try_from(cfg: CommitParser) -> Result<Self, Self::Error> {
Ok(Self {
message: to_opt_regex(cfg.message.as_deref(), "message")?,
body: to_opt_regex(cfg.body.as_deref(), "body")?,
group: cfg.group,
default_scope: cfg.default_scope,
scope: cfg.scope,
skip: cfg.skip,
field: cfg.field,
pattern: to_opt_regex(cfg.pattern.as_deref(), "pattern")?,
sha: cfg.sha,
footer: None,
})
}
}
fn to_regex(input: &str, element_name: &str) -> anyhow::Result<Regex> {
Regex::new(input).with_context(|| format!("failed to parse `{element_name}` regex"))
}
fn to_opt_regex(input: Option<&str>, element_name: &str) -> anyhow::Result<Option<Regex>> {
input.map(|i| to_regex(i, element_name)).transpose()
}
fn to_opt_vec<T, U>(vec: Option<Vec<T>>, element_name: &str) -> anyhow::Result<Vec<U>>
where
T: TryInto<U, Error = anyhow::Error>,
{
vec.map(|v| vec_try_into(v, element_name))
.transpose()
.map(|v| v.unwrap_or_default())
}
fn vec_try_into<T, U>(vec: Vec<T>, element_name: &str) -> anyhow::Result<Vec<U>>
where
T: TryInto<U, Error = anyhow::Error>,
{
vec.into_iter()
.map(|cp| {
cp.try_into()
.with_context(|| format!("failed to parse {element_name}"))
})
.collect()
}
pub fn to_git_cliff_config(
cfg: ChangelogCfg,
pr_link: Option<&str>,
) -> anyhow::Result<git_cliff_core::config::Config> {
let commit_preprocessors: Vec<git_cliff_core::config::TextProcessor> =
to_opt_vec(cfg.commit_preprocessors, "commit_preprocessors")?;
let postprocessors: Vec<git_cliff_core::config::TextProcessor> =
to_opt_vec(cfg.postprocessors, "postprocessors")?;
let link_parsers: Vec<git_cliff_core::config::LinkParser> =
to_opt_vec(cfg.link_parsers, "link_parsers")?;
let tag_pattern = to_opt_regex(cfg.tag_pattern.as_deref(), "tag_pattern")?;
let sort_commits = cfg.sort_commits.map(|s| format!("{s}"));
let commit_parsers: Vec<git_cliff_core::config::CommitParser> =
to_opt_vec(cfg.commit_parsers, "commit_parsers")?;
let default_changelog_config = release_plz_core::default_changelog_config(cfg.header.clone());
let default_git_config = release_plz_core::default_git_config(pr_link);
Ok(git_cliff_core::config::Config {
changelog: ChangelogConfig {
header: default_changelog_config.header,
body: cfg.body.unwrap_or(default_changelog_config.body),
trim: cfg.trim.unwrap_or(default_changelog_config.trim),
postprocessors,
footer: None,
..ChangelogConfig::default()
},
git: git_cliff_core::config::GitConfig {
conventional_commits: default_git_config.conventional_commits,
filter_unconventional: default_git_config.filter_unconventional,
split_commits: default_git_config.split_commits,
commit_preprocessors,
commit_parsers,
protect_breaking_commits: cfg
.protect_breaking_commits
.unwrap_or(default_git_config.protect_breaking_commits),
link_parsers,
filter_commits: default_git_config.filter_commits,
tag_pattern,
skip_tags: None,
ignore_tags: None,
topo_order: default_git_config.topo_order,
sort_commits: sort_commits.unwrap_or(default_git_config.sort_commits),
limit_commits: None,
..Default::default()
},
remote: RemoteConfig::default(),
bump: Bump::default(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
#[test]
fn test_deserialize_toml() {
let toml = r#"
[changelog]
header = "Changelog"
body = "Body"
trim = true
protect_breaking_commits = true
commit_preprocessors = [
{ pattern = "pattern", replace = "replace", replace_command = "replace_command" },
{ pattern = "pattern2", replace = "replace2", replace_command = "replace_command2" }
]
postprocessors = [
{ pattern = ".*", replace = "replace", replace_command = "replace_command" },
]
commit_parsers = [
{ message = "message", body = "body", group = "group", default_scope = "default_scope", scope = "scope", skip = true, field = "field", pattern = "pattern"}
]
link_parsers = [
{ pattern = "pattern", href = "href", text = "text" }
]
"#;
let cfg: Config = toml::from_str(toml).unwrap();
let actual_cliff_config: git_cliff_core::config::Config =
to_git_cliff_config(cfg.changelog, None).unwrap();
let expected_cliff_config = git_cliff_core::config::Config {
changelog: ChangelogConfig {
header: Some("Changelog".to_string()),
body: "Body".to_string(),
trim: true,
postprocessors: vec![git_cliff_core::config::TextProcessor {
pattern: regex::Regex::new(".*").unwrap(),
replace: Some("replace".to_string()),
replace_command: Some("replace_command".to_string()),
}],
footer: None,
..ChangelogConfig::default()
},
git: git_cliff_core::config::GitConfig {
protect_breaking_commits: true,
commit_preprocessors: vec![
git_cliff_core::config::TextProcessor {
pattern: regex::Regex::new("pattern").unwrap(),
replace: Some("replace".to_string()),
replace_command: Some("replace_command".to_string()),
},
git_cliff_core::config::TextProcessor {
pattern: regex::Regex::new("pattern2").unwrap(),
replace: Some("replace2".to_string()),
replace_command: Some("replace_command2".to_string()),
},
],
commit_parsers: vec![git_cliff_core::config::CommitParser {
message: Some(regex::Regex::new("message").unwrap()),
body: Some(regex::Regex::new("body").unwrap()),
group: Some("group".to_string()),
default_scope: Some("default_scope".to_string()),
scope: Some("scope".to_string()),
skip: Some(true),
field: Some("field".to_string()),
pattern: Some(regex::Regex::new("pattern").unwrap()),
sha: None,
footer: None,
}],
link_parsers: vec![git_cliff_core::config::LinkParser {
pattern: regex::Regex::new("pattern").unwrap(),
href: "href".to_string(),
text: Some("text".to_string()),
}],
filter_commits: false,
tag_pattern: None,
skip_tags: None,
ignore_tags: None,
topo_order: false,
sort_commits: "newest".to_string(),
limit_commits: None,
conventional_commits: true,
filter_unconventional: false,
split_commits: false,
..Default::default()
},
remote: RemoteConfig::default(),
bump: Bump::default(),
};
let expected_cliff_toml = toml::to_string(&expected_cliff_config).unwrap();
dbg!(&actual_cliff_config);
let actual_cliff_toml = toml::to_string(&actual_cliff_config).unwrap();
assert_eq!(expected_cliff_toml, actual_cliff_toml);
}
}