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 portif 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 portif 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(())