From 3e7cf027f329d6acda24b29cf22ae9eed4d06a18 Mon Sep 17 00:00:00 2001 From: Pascal Bach Date: Fri, 17 Dec 2021 21:56:38 +0100 Subject: [PATCH 1/4] feat(core): add link parsing Searches for patterns in commite messages and allows to convert them into links via regular expression replacement. This allows to include links to issue trackers and other tools in the resulting changelog. Closes #41 --- README.md | 21 ++++- git-cliff-core/src/commit.rs | 103 +++++++++++++++++++++++ git-cliff-core/src/config.rs | 14 +++ git-cliff-core/tests/integration_test.rs | 94 ++++++++++++--------- git-cliff/src/changelog.rs | 1 + 5 files changed, 192 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 541a39d6a0..d47422ad58 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,7 @@ This minimal example creates artifacts that can be used on another job. - CHANGELOG.md ``` -Please note that the stage is `doc` and has to be changed accordingly to your need. +Please note that the stage is `doc` and has to be changed accordingly to your need. ## Configuration File @@ -406,6 +406,10 @@ skip_tags = "v0.1.0-beta.1" ignore_tags = "" topo_order = false sort_commits = "oldest" +link_parsers = [ + { pattern = "#(\\d+)", href = "https://github.com/orhun/git-cliff/issues/$1"}, + { pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1"}, +] ``` #### conventional_commits @@ -513,6 +517,17 @@ Possible values: This can also be achieved by specifying the `--sort` command line argument. +#### link_parsers + +An array of link parsers for extracting external references, and turning them into URLs, using regex. + +Examples: + +- `{ pattern = "#(\\d+)", href = "https://github.com/orhun/git-cliff/issues/$1"}` + - Extract all gitlab issues and PRs and generate URLs linking to them. The link thext will be the matching pattern. +- `{ pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1"}`, + - Extract mentiones of IETF RFCs and generate URLs linking to them. It alos rewrites the text to `ietf-rfc...`. + ## Templating A template is a text where variables and expressions get replaced with values when it is rendered. @@ -550,7 +565,8 @@ following context is generated to use for templating: "footers": ["[footer]", "[footer]"], "breaking_description": "", "breaking": false, - "conventional": true + "conventional": true, + "links": [{"text": "[text]", "href": "[href]"}] } ], "commit_id": "a440c6eb26404be4877b7e3ad592bfaa5d4eb210 (release commit)", @@ -595,6 +611,7 @@ If [conventional_commits](#conventional_commits) is set to `false`, then some of "scope": "(overrided by commit_parsers)", "message": "(full commit message including description, footers, etc.)", "conventional": false, + "links": [{"text": "[text]", "href": "[href]"}] } ], "commit_id": "a440c6eb26404be4877b7e3ad592bfaa5d4eb210 (release commit)", diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs index 6c559ccbf2..d61a7c70bf 100644 --- a/git-cliff-core/src/commit.rs +++ b/git-cliff-core/src/commit.rs @@ -1,6 +1,7 @@ use crate::config::{ CommitParser, GitConfig, + LinkParser, }; use crate::error::{ Error as AppError, @@ -29,6 +30,20 @@ pub struct Commit<'a> { pub group: Option, /// Commit scope based on conventional type or a commit parser. pub scope: Option, + /// A list of links found in the commit + pub links: Vec, +} + +/// Object representing a link +#[derive( + Debug, Clone, PartialEq, serde_derive::Deserialize, serde_derive::Serialize, +)] +#[serde(rename_all = "camelCase")] +pub struct Link { + /// Text of the link. + pub text: String, + /// URL of the link + pub href: String, } impl<'a> From<&GitCommit<'a>> for Commit<'a> { @@ -49,6 +64,7 @@ impl Commit<'_> { conv: None, group: None, scope: None, + links: vec![], } } @@ -56,6 +72,7 @@ impl Commit<'_> { /// /// * converts commit to a conventional commit /// * sets the group for the commit + /// * extacts links and generates URLs pub fn process(&self, config: &GitConfig) -> Result { let mut commit = self.clone(); if config.conventional_commits { @@ -69,6 +86,9 @@ impl Commit<'_> { commit = commit.parse(parsers, config.filter_commits.unwrap_or(false))?; } + if let Some(parsers) = &config.link_parsers { + commit = commit.parse_links(parsers)?; + } Ok(commit) } @@ -118,6 +138,32 @@ impl Commit<'_> { ))) } } + + /// Parses the commit using [`LinkParser`]s. + /// + /// Sets the [`links`] of the commit. + /// + /// [`links`]: Commit::links + pub fn parse_links(mut self, parsers: &[LinkParser]) -> Result { + for parser in parsers { + let regex = &parser.pattern; + let replace = &parser.href; + for mat in regex.find_iter(&self.message) { + let m = mat.as_str(); + let text = if let Some(text_replace) = &parser.text { + regex.replace(m, text_replace).to_string() + } else { + m.to_string() + }; + let href = regex.replace(m, replace); + self.links.push(Link { + text, + href: href.to_string(), + }); + } + } + Ok(self) + } } impl Serialize for Commit<'_> { @@ -163,6 +209,7 @@ impl Serialize for Commit<'_> { commit.serialize_field("scope", &self.scope)?; } } + commit.serialize_field("links", &self.links)?; commit.serialize_field("conventional", &self.conv.is_some())?; commit.end() } @@ -207,4 +254,60 @@ mod test { assert_eq!(Some(String::from("test_group")), commit.group); assert_eq!(Some(String::from("test_scope")), commit.scope); } + + #[test] + fn parse_link() { + let test_cases = vec![ + ( + Commit::new( + String::from("123123"), + String::from("test(commit): add test\n\nBody with issue #123"), + ), + true, + ), + ( + Commit::new( + String::from("123123"), + String::from( + "test(commit): add test\n\nImlement RFC456\n\nFixes: #456", + ), + ), + true, + ), + ]; + for (commit, is_conventional) in &test_cases { + assert_eq!(is_conventional, &commit.clone().into_conventional().is_ok()) + } + let commit = Commit::new( + String::from("123123"), + String::from("test(commit): add test\n\nImlement RFC456\n\nFixes: #455"), + ); + let commit = commit + .parse_links(&[ + LinkParser { + pattern: Regex::new("RFC(\\d+)").unwrap(), + href: String::from("rfc://$1"), + text: None, + }, + LinkParser { + pattern: Regex::new("#(\\d+)").unwrap(), + href: String::from("https://github.com/$1"), + text: None, + }, + ]) + .unwrap(); + assert_eq!( + vec![ + Link { + text: String::from("RFC456"), + href: String::from("rfc://456"), + }, + Link { + text: String::from("#455"), + href: String::from("https://github.com/455"), + } + ], + commit.links + ); + } } diff --git a/git-cliff-core/src/config.rs b/git-cliff-core/src/config.rs index c6b55ba25f..79866f4435 100644 --- a/git-cliff-core/src/config.rs +++ b/git-cliff-core/src/config.rs @@ -32,6 +32,8 @@ pub struct GitConfig { pub filter_unconventional: Option, /// Git commit parsers. pub commit_parsers: Option>, + /// Link parsers. + pub link_parsers: Option>, /// Whether to filter out commits. pub filter_commits: Option, /// Blob pattern for git tags. @@ -65,6 +67,18 @@ pub struct CommitParser { pub skip: Option, } +/// Parser for extracting links in commits. +#[derive(Debug, Clone, serde_derive::Serialize, serde_derive::Deserialize)] +pub struct LinkParser { + /// Regex for finding links in the commit message. + #[serde(with = "serde_regex")] + pub pattern: Regex, + /// The string used to generate the link URL. + pub href: String, + /// The string used to generate the link text. + pub text: Option, +} + impl Config { /// Parses the config file and returns the values. pub fn parse(file_name: String) -> Result { diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs index 4a1b666947..58b5c9fb93 100644 --- a/git-cliff-core/tests/integration_test.rs +++ b/git-cliff-core/tests/integration_test.rs @@ -3,6 +3,7 @@ use git_cliff_core::config::{ ChangelogConfig, CommitParser, GitConfig, + LinkParser, }; use git_cliff_core::error::Result; use git_cliff_core::release::*; @@ -17,19 +18,19 @@ fn generate_changelog() -> Result<()> { header: Some(String::from("this is a changelog")), body: String::from( r#" - ## Release {{ version }} - {% for group, commits in commits | group_by(attribute="group") %} - ### {{ group }} - {% for commit in commits %} - {%- if commit.scope -%} - - *({{commit.scope}})* {{ commit.message }} - {% else -%} - - {{ commit.message }} - {% endif -%} - {% if commit.breaking -%} - {% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}} - {% endif -%} - {% endfor -%} +## Release {{ version }} +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group }} +{% for commit in commits %} +{%- if commit.scope -%} +- *({{commit.scope}})* {{ commit.message }}{%- if commit.links %} ({% for link in commit.links %}[{{link.text}}]({{link.href}}) {% endfor -%}){% endif %} +{% else -%} +- {{ commit.message }} +{% endif -%} +{% if commit.breaking -%} +{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}} +{% endif -%} +{% endfor -%} {% endfor %}"#, ), footer: Some(String::from("eoc - end of changelog")), @@ -60,6 +61,18 @@ fn generate_changelog() -> Result<()> { ignore_tags: None, topo_order: None, sort_commits: None, + link_parsers: Some(vec![ + LinkParser { + pattern: Regex::new("#(\\d+)").unwrap(), + href: String::from("https://github.com/$1"), + text: None, + }, + LinkParser { + pattern: Regex::new("https://github.com/(.*)").unwrap(), + href: String::from("https://github.com/$1"), + text: Some(String::from("$1")), + }, + ]), }; let releases = vec![ @@ -74,7 +87,9 @@ fn generate_changelog() -> Result<()> { Commit::new(String::from("abc124"), String::from("feat: add zyx")), Commit::new( String::from("abc124"), - String::from("feat(random-scope): add random feature"), + String::from( + "feat(random-scope): add random feature\n\nThis is related to https://github.com/NixOS/nixpkgs/issues/136814\n\nCloses #123", + ), ), Commit::new(String::from("def789"), String::from("invalid commit")), Commit::new( @@ -134,32 +149,33 @@ fn generate_changelog() -> Result<()> { writeln!(out, "{}", changelog_config.footer.unwrap()).unwrap(); assert_eq!( - "this is a changelog + r#"this is a changelog + +## Release v2.0.0 + +### fix bugs +- fix abc + +### shiny features +- add xyz +- add zyx +- *(random-scope)* add random feature ([#123](https://github.com/123) [NixOS/nixpkgs/issues/136814](https://github.com/NixOS/nixpkgs/issues/136814) ) +- *(big-feature)* this is a breaking change + - **BREAKING**: this is a breaking change + +## Release v1.0.0 + +### chore +- do nothing + +### feat +- add cool features - ## Release v2.0.0 - - ### fix bugs - - fix abc - - ### shiny features - - add xyz - - add zyx - - *(random-scope)* add random feature - - *(big-feature)* this is a breaking change - - **BREAKING**: this is a breaking change - - ## Release v1.0.0 - - ### chore - - do nothing - - ### feat - - add cool features - - ### fix - - fix stuff - - fix more stuff - eoc - end of changelog\n", +### fix +- fix stuff +- fix more stuff +eoc - end of changelog +"#, out ); diff --git a/git-cliff/src/changelog.rs b/git-cliff/src/changelog.rs index 70d00e63e5..ab9216b1ed 100644 --- a/git-cliff/src/changelog.rs +++ b/git-cliff/src/changelog.rs @@ -206,6 +206,7 @@ mod test { ignore_tags: None, topo_order: Some(false), sort_commits: Some(String::from("oldest")), + link_parsers: None, }, }; let test_release = Release { From 72c3936b55cb66033eabffe7a8a23245eedd75bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Fri, 31 Dec 2021 20:27:54 +0300 Subject: [PATCH 2/4] fix(ci): update lychee arguments --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index def1a91bcc..53562a198b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,6 @@ jobs: - name: Check the links uses: lycheeverse/lychee-action@v1 with: - args: --exclude "%7Busername%7D|file:///" -v *.md + args: --exclude "%7Busername%7D|file:///|https://datatracker.ietf.org" -v *.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From cf5de0e006b803fcd11eb9f8854b1569520fa2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Fri, 31 Dec 2021 20:45:43 +0300 Subject: [PATCH 3/4] docs(readme): fix typos --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d47422ad58..22e1b896df 100644 --- a/README.md +++ b/README.md @@ -524,7 +524,7 @@ An array of link parsers for extracting external references, and turning them in Examples: - `{ pattern = "#(\\d+)", href = "https://github.com/orhun/git-cliff/issues/$1"}` - - Extract all gitlab issues and PRs and generate URLs linking to them. The link thext will be the matching pattern. + - Extract all GitLab issues and PRs and generate URLs linking to them. The link text will be the matching pattern. - `{ pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1"}`, - Extract mentiones of IETF RFCs and generate URLs linking to them. It alos rewrites the text to `ietf-rfc...`. From 6f748b06e6db082b4f322a13a89ffe6602f408a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Fri, 31 Dec 2021 20:46:54 +0300 Subject: [PATCH 4/4] docs(readme): fix typos --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22e1b896df..7e1cc7943e 100644 --- a/README.md +++ b/README.md @@ -526,7 +526,7 @@ Examples: - `{ pattern = "#(\\d+)", href = "https://github.com/orhun/git-cliff/issues/$1"}` - Extract all GitLab issues and PRs and generate URLs linking to them. The link text will be the matching pattern. - `{ pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1"}`, - - Extract mentiones of IETF RFCs and generate URLs linking to them. It alos rewrites the text to `ietf-rfc...`. + - Extract mentions of IETF RFCs and generate URLs linking to them. It also rewrites the text as `ietf-rfc...`. ## Templating