use std::{
fmt,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{error::Error, git::GitHelper};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Type {
pub(crate) r#type: String,
#[serde(default)]
pub(crate) section: String,
#[serde(default)]
pub(crate) hidden: bool,
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.r#type)
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Config {
#[serde(default = "default_header")]
pub(crate) header: String,
#[serde(default = "default_types")]
pub(crate) types: Vec<Type>,
#[serde(default)]
pre_major: bool,
#[serde(default = "default_commit_url_format")]
pub(crate) commit_url_format: String,
#[serde(default = "default_compare_url_format")]
pub(crate) compare_url_format: String,
#[serde(default = "default_issue_url_format")]
pub(crate) issue_url_format: String,
#[serde(default = "default_user_url_format")]
pub(crate) user_url_format: String,
#[serde(default = "default_release_commit_message_format")]
pub(crate) release_commit_message_format: String,
#[serde(default = "default_issue_prefixes")]
pub(crate) issue_prefixes: Vec<String>,
pub(crate) host: Option<String>,
pub(crate) owner: Option<String>,
pub(crate) repository: Option<String>,
pub(crate) template: Option<PathBuf>,
#[serde(default = "default_scope_regex")]
pub(crate) scope_regex: String,
}
impl Default for Config {
fn default() -> Self {
Self {
header: default_header(),
types: default_types(),
pre_major: false,
commit_url_format: default_commit_url_format(),
compare_url_format: default_compare_url_format(),
issue_url_format: default_issue_url_format(),
user_url_format: default_user_url_format(),
release_commit_message_format: default_release_commit_message_format(),
issue_prefixes: default_issue_prefixes(),
host: None,
owner: None,
repository: None,
template: None,
scope_regex: "[[:alnum:]]+(?:[-_/][[:alnum:]]+)*".to_string(),
}
}
}
fn default_header() -> String {
"# Changelog\n\n".into()
}
fn default_types() -> Vec<Type> {
vec![
Type {
r#type: "feat".into(),
section: "Features".into(),
hidden: false,
},
Type {
r#type: "fix".into(),
section: "Fixes".into(),
hidden: false,
},
Type {
r#type: "build".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "chore".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "ci".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "docs".into(),
section: "Documentation".into(),
hidden: true,
},
Type {
r#type: "style".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "refactor".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "perf".into(),
section: "Other".into(),
hidden: true,
},
Type {
r#type: "test".into(),
section: "Other".into(),
hidden: true,
},
]
}
fn default_commit_url_format() -> String {
"{{host}}/{{owner}}/{{repository}}/commit/{{hash}}".into()
}
fn default_compare_url_format() -> String {
"{{host}}/{{owner}}/{{repository}}/compare/{{previousTag}}...{{currentTag}}".into()
}
fn default_issue_url_format() -> String {
"{{host}}/{{owner}}/{{repository}}/issues/{{id}}".into()
}
fn default_user_url_format() -> String {
"{{host}}/{{user}}".into()
}
fn default_release_commit_message_format() -> String {
"chore(release): {{currentTag}}".into()
}
fn default_issue_prefixes() -> Vec<String> {
vec!["#".into()]
}
fn default_scope_regex() -> String {
"[[:alnum:]]+(?:[-_/][[:alnum:]]+)*".to_string()
}
type HostOwnerRepo = (Option<String>, Option<String>, Option<String>);
pub(crate) fn host_info(git: &GitHelper) -> Result<HostOwnerRepo, Error> {
if let Some(mut url) = git.url()? {
if !url.contains("://") {
if let Some(colon) = url.find(':') {
match url.as_bytes()[colon + 1] {
b'0'..=b'9' => url = format!("scheme://{}", url),
_ => url = format!("scheme://{}/{}", &url[..colon], &url[colon + 1..]),
}
}
}
let url = Url::parse(url.as_str())?;
let host = url.host().map(|h| format!("https://{}", h));
let mut owner = None;
let mut repository = None;
if let Some(mut segments) = url.path_segments() {
owner = segments.next().map(|s| s.to_string());
repository = segments
.next()
.map(|s: &str| s.trim_end_matches(".git").to_string());
}
Ok((host, owner, repository))
} else {
Ok((None, None, None))
}
}
pub(crate) fn make_cl_config(git: &GitHelper, path: impl AsRef<Path>) -> Config {
let mut config: Config = (std::fs::read(path))
.ok()
.and_then(|versionrc| (serde_yaml::from_reader(versionrc.as_slice())).ok())
.unwrap_or_default();
if let Config {
host: None,
owner: None,
repository: None,
..
} = config
{
if let Ok((host, owner, repository)) = host_info(git) {
config.host = host;
config.owner = owner;
config.repository = repository;
}
}
config
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test() {
let json = r#"{
"types": [
{"type": "chore", "section":"Others", "hidden": false},
{"type": "revert", "section":"Reverts", "hidden": false},
{"type": "feat", "section": "Features", "hidden": false},
{"type": "fix", "section": "Bug Fixes", "hidden": false},
{"type": "improvement", "section": "Feature Improvements", "hidden": false},
{"type": "docs", "section":"Docs", "hidden": false},
{"type": "style", "section":"Styling", "hidden": false},
{"type": "refactor", "section":"Code Refactoring", "hidden": false},
{"type": "perf", "section":"Performance Improvements", "hidden": false},
{"type": "test", "section":"Tests", "hidden": false},
{"type": "build", "section":"Build System", "hidden": false},
{"type": "ci", "section":"CI", "hidden":false}
],
}"#;
let value: Config = serde_yaml::from_str(json).unwrap();
assert_eq!(
value,
Config {
header: "# Changelog\n\n".to_string(),
types: vec![
Type {
r#type: "chore".into(),
section: "Others".into(),
hidden: false
},
Type {
r#type: "revert".into(),
section: "Reverts".into(),
hidden: false
},
Type {
r#type: "feat".into(),
section: "Features".into(),
hidden: false
},
Type {
r#type: "fix".into(),
section: "Bug Fixes".into(),
hidden: false
},
Type {
r#type: "improvement".into(),
section: "Feature Improvements".into(),
hidden: false
},
Type {
r#type: "docs".into(),
section: "Docs".into(),
hidden: false
},
Type {
r#type: "style".into(),
section: "Styling".into(),
hidden: false
},
Type {
r#type: "refactor".into(),
section: "Code Refactoring".into(),
hidden: false
},
Type {
r#type: "perf".into(),
section: "Performance Improvements".into(),
hidden: false
},
Type {
r#type: "test".into(),
section: "Tests".into(),
hidden: false
},
Type {
r#type: "build".into(),
section: "Build System".into(),
hidden: false
},
Type {
r#type: "ci".into(),
section: "CI".into(),
hidden: false
}
],
pre_major: false,
commit_url_format: "{{host}}/{{owner}}/{{repository}}/commit/{{hash}}".to_string(),
compare_url_format:
"{{host}}/{{owner}}/{{repository}}/compare/{{previousTag}}...{{currentTag}}"
.to_string(),
issue_url_format: "{{host}}/{{owner}}/{{repository}}/issues/{{id}}".to_string(),
user_url_format: "{{host}}/{{user}}".to_string(),
release_commit_message_format: "chore(release): {{currentTag}}".to_string(),
issue_prefixes: vec!["#".into()],
host: None,
owner: None,
repository: None,
template: None,
scope_regex: "[[:alnum:]]+(?:[-_/][[:alnum:]]+)*".to_string(),
}
)
}
}