M3X5J5GHHZXBPTFXDCHWQKQPWUVRUH6RJNGVWIA44B57J4T3O75AC
EWPTHPFKC63MW6PN344NKCJQD6KTOXBJHPKHTQEE3EHCKPRCZN2AC
PKI5SXRHXTLHPUJQANVDKGA2HLTZNMIVPYU2JVOBC45QUGNBJI7QC
VUSECHH6JNFVRXXS5ZP7NBGO3BQWBJZCWRJCYF2SKOGZLKRYLK4QC
4FCEKFETVRRY5ZNAO44GFUCEIXYCM3J2MRTUQ7CUYZ2ZHANWI2MAC
VE5MD6H3NT7PNDZH5EWQT72QV7R3753HICOLYHK3OYW57ZT25YNQC
LGQWR3KUNCFZCI6E3OT7FY4TBL3LGOBWVTIQMVHJQSNHO6QY4AXAC
G4X7YXNZRQNHFE5N3HFXKD7R66RLP6RCIUZYSGU4Y4AYZTRAPMQAC
6CDBBU3Z2KQRMC4KUE4HU7ST2RYXD5Y7GC24JFLYPDDIKNXHTJ4AC
4LFON6GEZVZPITBA7AZCNARXHUSRHWGFYQMR2X6JEVBGHKHECVTAC
let mut builder = ContextBuilder::new(self.config, self.git)?
.version(version)
.is_patch(is_patch)
.previous_tag(to_rev.0)
.current_tag(from_rev.0)
.commit_groups(commit_groups)
.note_groups(
notes
.into_iter()
.map(|(title, notes)| NoteGroup { title, notes })
.collect(),
);
builder = builder.date(version_date);
Ok(builder.build()?)
let note_groups: Vec<NoteGroup> = notes
.into_iter()
.map(|(title, notes)| NoteGroup { title, notes })
.collect();
let Config {
host,
owner,
repository,
..
} = self.config;
let context_base = ContextBase {
version,
date: Some(version_date),
is_patch,
commit_groups,
note_groups,
previous_tag: to_rev.0,
current_tag: from_rev.0,
host: host.to_owned(),
owner: owner.to_owned(),
repository: repository.to_owned(),
};
self.context_builder.build(context_base)
.unwrap_or_default()
.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
}
type HostOwnerRepo = (Option<String>, Option<String>, Option<String>);
/// Get host, owner and repository based on the git remote origin url.
pub(crate) fn host_info(git: &GitHelper) -> Result<HostOwnerRepo, Error> {
if let Some(mut url) = git.url()? {
if !url.contains("://") {
// check if it contains a port
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))
}
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Type {
pub(crate) r#type: String,
#[serde(default)]
pub(crate) section: String,
#[serde(default)]
pub(crate) hidden: bool,
}
/// see: [Conventional Changelog Configuration](https://github.com/conventional-changelog/conventional-changelog-config-spec/blob/master/versions/2.1.0/README.md)
/// Additional config: `host`, `owner`, `repository`.
/// Those values are derived from `git remote origin get-url` if not set.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Config {
/// A string to be used as the main header section of the CHANGELOG.
#[serde(default = "default_header")]
pub(crate) header: String,
/// An array of `type` objects representing the explicitly supported commit message types, and whether they should show up in generated `CHANGELOG`s.
#[serde(default = "default_types")]
pub(crate) types: Vec<Type>,
/// Boolean indicating whether or not the action being run (generating CHANGELOG, recommendedBump, etc.) is being performed for a pre-major release (<1.0.0).\n This config setting will generally be set by tooling and not a user.
#[serde(default)]
pre_major: bool,
/// A URL representing a specific commit at a hash.
#[serde(default = "default_commit_url_format")]
pub(crate) commit_url_format: String,
/// A URL representing the comparison between two git SHAs.
#[serde(default = "default_compare_url_format")]
pub(crate) compare_url_format: String,
/// A URL representing the issue format (allowing a different URL format to be swapped in for Gitlab, Bitbucket, etc).
#[serde(default = "default_issue_url_format")]
pub(crate) issue_url_format: String,
/// A URL representing the a user's profile URL on GitHub, Gitlab, etc. This URL is used for substituting @bcoe with https://github.com/bcoe in commit messages.
#[serde(default = "default_user_url_format")]
pub(crate) user_url_format: String,
/// A string to be used to format the auto-generated release commit message.
#[serde(default = "default_release_commit_message_format")]
pub(crate) release_commit_message_format: String,
/// An array of prefixes used to detect references to issues
#[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>,
}
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,
}
}
}
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,
},
]
}
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()]
}
#[cfg(test)]
mod tests {
use super::*;
use serde_yaml;
#[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,
}
)
}
}
use url::Url;
/// [Conventional Changelog Configuration](https://github.com/conventional-changelog/conventional-changelog-config-spec/blob/master/versions/2.1.0/README.md)
/// Describes the configuration options supported by conventional-config for upstream tooling.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Config {
/// A string to be used as the main header section of the CHANGELOG.
#[serde(default = "default_header")]
pub(crate) header: String,
/// An array of `type` objects representing the explicitly supported commit message types, and whether they should show up in generated `CHANGELOG`s.
#[serde(default = "default_types")]
pub(crate) types: Vec<Type>,
/// Boolean indicating whether or not the action being run (generating CHANGELOG, recommendedBump, etc.) is being performed for a pre-major release (<1.0.0).\n This config setting will generally be set by tooling and not a user.
#[serde(default)]
pre_major: bool,
/// A URL representing a specific commit at a hash.
#[serde(default = "default_commit_url_format")]
commit_url_format: String,
/// A URL representing the comparison between two git SHAs.
#[serde(default = "default_compare_url_format")]
compare_url_format: String,
/// A URL representing the issue format (allowing a different URL format to be swapped in for Gitlab, Bitbucket, etc).
#[serde(default = "default_issue_url_format")]
issue_url_format: String,
/// A URL representing the a user's profile URL on GitHub, Gitlab, etc. This URL is used for substituting @bcoe with https://github.com/bcoe in commit messages.
#[serde(default = "default_user_url_format")]
user_url_format: String,
/// A string to be used to format the auto-generated release commit message.
#[serde(default = "default_release_commit_message_format")]
release_commit_message_format: String,
/// An array of prefixes used to detect references to issues
#[serde(default = "default_issue_prefixes")]
pub(crate) issue_prefixes: Vec<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(),
}
}
}
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,
},
]
}
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()]
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Type {
pub(crate) r#type: String,
#[serde(default)]
pub(crate) section: String,
#[serde(default)]
pub(crate) hidden: bool,
}
let (host, owner, repository) = Self::host_info(git)?;
Ok(Self {
handlebars,
context: ContextBase {
version: Default::default(),
date: Default::default(),
is_patch: Default::default(),
commit_groups: Default::default(),
note_groups: Default::default(),
previous_tag: "",
current_tag: "",
host,
owner,
repository,
},
})
}
fn host_info(
git: &'a GitHelper,
) -> Result<(Option<String>, Option<String>, Option<String>), Error> {
if let Some(mut url) = git.url()? {
if !url.contains("://") {
// check if it contains a port
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 fn version(mut self, version: &'a str) -> Self {
self.context.version = version;
self
}
pub fn date(mut self, date: NaiveDate) -> Self {
self.context.date = Some(date);
self
}
pub fn is_patch(mut self, is_patch: bool) -> Self {
self.context.is_patch = is_patch;
self
}
pub fn commit_groups(mut self, commit_groups: Vec<CommitGroup<'a>>) -> Self {
self.context.commit_groups = commit_groups;
self
}
pub fn note_groups(mut self, note_groups: Vec<NoteGroup>) -> Self {
self.context.note_groups = note_groups;
self
}
pub fn previous_tag(mut self, previous_tag: &'a str) -> Self {
self.context.previous_tag = previous_tag;
self
Ok(Self { handlebars })
pub fn current_tag(mut self, current_tag: &'a str) -> Self {
self.context.current_tag = current_tag;
self
}
pub fn build(self) -> Result<Context<'a>, Error> {
pub fn build(&self, context_base: ContextBase<'a>) -> Result<Context<'a>, Error> {
.render("compare_url_format", &self.context)?;
let commit_url_format = self.handlebars.render("commit_url_format", &self.context)?;
let issue_url_format = self.handlebars.render("issue_url_format", &self.context)?;
.render("compare_url_format", &context_base)?;
let commit_url_format = self.handlebars.render("commit_url_format", &context_base)?;
let issue_url_format = self.handlebars.render("issue_url_format", &context_base)?;
.render("release_commit_message_format", &self.context)?;
let user_url_format = self.handlebars.render("user_url_format", &self.context)?;
let link_compare = self.context.current_tag != "" && self.context.previous_tag != "";
.render("release_commit_message_format", &context_base)?;
let user_url_format = self.handlebars.render("user_url_format", &context_base)?;
let link_compare = context_base.current_tag != "" && context_base.previous_tag != "";
pub(crate) fn write_header(&mut self, header: &str) -> Result<(), Error> {
write!(self.writer, "{}", header)?;
Ok(())
}
pub fn write_template(&mut self, context: &Context<'_>) -> Result<(), Error> {
pub(crate) fn new(writer: W) -> Result<Self, Error> {
#[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()]
}
)
pub(crate) fn write_template(&mut self, context: &Context<'_>) -> Result<(), Error> {
let writer = &mut self.writer;
self.handlebars
.render_to_write("template", context, writer)?;
Ok(())