Repository: orhun/git-cliff (rust)

Issue:
Detailed template errors out with template render error
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Description of the bug

Detailed template config errors out with template render error when running on falcosecurity/falco.

### Steps To Reproduce

1. Install git cliff v2.4.0
2. Clone https://github.com/falcosecurity/falco
3. Run: `git cliff -c detailed`

### Expected behavior

Changelog created without error, or the error to explain why the source data is invalid.

### Screenshots / Logs

```
 git cliff -c detailed --verbose
 INFO  git_cliff > Using built-in configuration file: detailed
 DEBUG git_cliff > No GitHub remote is set, using remote: falco/
 DEBUG git_cliff_core::changelog > Processing the commits...
 DEBUG git_cliff_core::changelog > Processing the releases...
 DEBUG git_cliff_core::changelog > Adding remote data...
 DEBUG git_cliff_core::changelog > Generating changelog...
# Changelog

All notable changes to this project will be documented in this file.

 ERROR git_cliff                 > Template render error:
Filter call 'truncate' failed
Filter `truncate` was called on an incorrect value: got `null` but expected a String
```

### Software information

- Operating system: macOS 14.5 (23F79)
- Rust version: n/a
- Project version: v2.4.0, installed via Homebrew


### Additional context

_No response_

Pull request description:
chore(config): check for null commit_id in detailed template
## Description

Simply does what the title says.

## Motivation and Context

Potentially fixes #785

## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

## Types of Changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation (no code change)
- [ ] Refactor (refactoring production code)
- [ ] Other <!--- (provide information) -->

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project.
- [x] I have updated the documentation accordingly.
- [x] I have formatted the code with [rustfmt](https://github.com/rust-lang/rustfmt).
- [x] I checked the lints with [clippy](https://github.com/rust-lang/rust-clippy).
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.

<!--- Thank you for contributing to git-cliff! ⛰️  -->


Patch:
diff --git a/examples/detailed.toml b/examples/detailed.toml
index e39a60e..c4d6a88 100644
--- a/examples/detailed.toml
+++ b/examples/detailed.toml
@@ -16,7 +16,7 @@ body = """
     ## [unreleased]
 {% endif %}\
 {% if previous %}\
-    {% if previous.commit_id %}
+    {% if previous.commit_id and commit_id %}
         [{{ previous.commit_id | truncate(length=7, end="") }}]({{ previous.commit_id }})...\
             [{{ commit_id | truncate(length=7, end="") }}]({{ commit_id }})
     {% endif %}\
diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs
index d15aed1..9af571a 100644
--- a/git-cliff-core/src/changelog.rs
+++ b/git-cliff-core/src/changelog.rs
@@ -62,9 +62,9 @@ impl<'a> Changelog<'a> {
 			config,
 			additional_context: HashMap::new(),
 		};
+		changelog.add_remote_data()?;
 		changelog.process_commits();
 		changelog.process_releases();
-		changelog.add_remote_data()?;
 		Ok(changelog)
 	}
 
diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs
index f609284..0474772 100644
--- a/git-cliff-core/src/commit.rs
+++ b/git-cliff-core/src/commit.rs
@@ -264,6 +264,11 @@ impl Commit<'_> {
 		protect_breaking: bool,
 		filter: bool,
 	) -> Result<Self> {
+		let lookup_context = serde_json::to_value(&self).map_err(|e| {
+			AppError::FieldError(format!(
+				"failed to convert context into value: {e}",
+			))
+		})?;
 		for parser in parsers {
 			let mut regex_checks = Vec::new();
 			if let Some(message_regex) = parser.message.as_ref() {
@@ -287,25 +292,22 @@ impl Commit<'_> {
 			if let (Some(field_name), Some(pattern_regex)) =
 				(parser.field.as_ref(), parser.pattern.as_ref())
 			{
-				regex_checks.push((
-					pattern_regex,
-					match field_name.as_str() {
-						"id" => Some(self.id.clone()),
-						"message" => Some(self.message.clone()),
-						"body" => body,
-						"author.name" => self.author.name.clone(),
-						"author.email" => self.author.email.clone(),
-						"committer.name" => self.committer.name.clone(),
-						"committer.email" => self.committer.email.clone(),
-						_ => None,
+				let value = if field_name == "body" {
+					body.clone()
+				} else {
+					tera::dotted_pointer(&lookup_context, field_name)
+						.map(|v| v.to_string())
+				};
+				match value {
+					Some(value) => {
+						regex_checks.push((pattern_regex, value));
 					}
-					.ok_or_else(|| {
-						AppError::FieldError(format!(
-							"field {} does not have a value",
-							field_name
-						))
-					})?,
-				));
+					None => {
+						return Err(AppError::FieldError(format!(
+							"field {field_name} does not have a value",
+						)));
+					}
+				}
 			}
 			if parser.sha.clone().map(|v| v.to_lowercase()).as_deref() ==
 				Some(&self.id)
@@ -728,4 +730,55 @@ mod test {
 
 		Ok(())
 	}
+
+	#[test]
+	fn field_name_regex() -> Result<()> {
+		let commit = Commit {
+			message: String::from("feat: do something"),
+			author: Signature {
+				name:      Some("John Doe".to_string()),
+				email:     None,
+				timestamp: 0x0,
+			},
+			..Default::default()
+		};
+		let parsed_commit = commit.clone().parse(
+			&[CommitParser {
+				sha:           None,
+				message:       None,
+				body:          None,
+				footer:        None,
+				group:         Some(String::from("Test group")),
+				default_scope: None,
+				scope:         None,
+				skip:          None,
+				field:         Some(String::from("author.name")),
+				pattern:       Regex::new("Something else").ok(),
+			}],
+			false,
+			true,
+		);
+
+		assert!(parsed_commit.is_err());
+
+		let parsed_commit = commit.parse(
+			&[CommitParser {
+				sha:           None,
+				message:       None,
+				body:          None,
+				footer:        None,
+				group:         Some(String::from("Test group")),
+				default_scope: None,
+				scope:         None,
+				skip:          None,
+				field:         Some(String::from("author.name")),
+				pattern:       Regex::new("John Doe").ok(),
+			}],
+			false,
+			false,
+		)?;
+
+		assert_eq!(Some(String::from("Test group")), parsed_commit.group);
+		Ok(())
+	}
 }
diff --git a/git-cliff-core/src/error.rs b/git-cliff-core/src/error.rs
index 5f96b58..c9342fc 100644
--- a/git-cliff-core/src/error.rs
+++ b/git-cliff-core/src/error.rs
@@ -68,7 +68,7 @@ pub enum Error {
 	#[error("Failed to parse integer: `{0}`")]
 	IntParseError(#[from] std::num::TryFromIntError),
 	/// Error that may occur while processing parsers that define field and
-	/// value matchers
+	/// value matchers.
 	#[error("Field error: `{0}`")]
 	FieldError(String),
 	/// Error that may occur while parsing a SemVer version or version
diff --git a/website/docs/configuration/git.md b/website/docs/configuration/git.md
index 51dadd5..3c3722c 100644
--- a/website/docs/configuration/git.md
+++ b/website/docs/configuration/git.md
@@ -165,14 +165,17 @@ Examples:
 - `{ sha = "f6f2472bdf0bbb5f9fcaf2d72c1fa9f98f772bb2", group = "Stuff" }`
   - Set the group of the commit by using its SHA1.
 - `{ field = "author.name", pattern = "John Doe", group = "John's stuff" }`
-  - If the author's name attribute of the commit matches the pattern "John Doe" (as a regex), override the scope with "John' stuff". Supported commit attributes are:
+  - If the author's name attribute of the commit matches the pattern "John Doe" (as a regex), override the scope with "John's stuff".
+  - All values that are part of the commit context can be used. Nested fields can be accessed via the [dot notation](https://keats.github.io/tera/docs/#dot-notation). Some commonly used ones are:
     - `id`
     - `message`
-    - `body`
     - `author.name`
     - `author.email`
     - `committer.email`
     - `committer.name`
+  - `body` is a special field which contains the body of a convetional commit, if applicable.
+  - Be aware that all fields are converted to JSON strings before they are parsed by the given regex, especially when dealing with arrays.
+
 
 ### protect_breaking_commits
 
diff --git a/website/docs/integration/github.md b/website/docs/integration/github.md
index 98e40aa..d71e992 100644
--- a/website/docs/integration/github.md
+++ b/website/docs/integration/github.md
@@ -51,7 +51,7 @@ Although this is enough for a couple of runs of **git-cliff**, it is suggested t
 
 :::tip
 
-Follow [this guide](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) for creating an access token. It can be either a classic or fine-grained token _without_ permissions.
+Follow [this guide](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) for creating an access token. It can be either a classic or fine-grained token **without permissions**. Also, if you are running **git-cliff** in GitHub Actions, using `${{ secrets.GITHUB_TOKEN }}` is also enough.
 
 :::
 
diff --git a/website/docs/tips-and-tricks.md b/website/docs/tips-and-tricks.md
index 5c13592..693bcc2 100644
--- a/website/docs/tips-and-tricks.md
+++ b/website/docs/tips-and-tricks.md
@@ -43,14 +43,6 @@ Then strip the tags in the template with the series of filters:
 {% for group, commits in commits | filter(attribute="merge_commit", value=false) %}
 ```
 
-## Skip commits by PR label
-
-```jinja2
-{% if commit.github.pr_labels is containing("skip-release-notes") %}
-    {% continue %}
-{% endif %}
-```
-
 ## Remove gitmoji
 
 ```toml
@@ -69,3 +61,26 @@ commit_parsers = [
   { body = "$^", skip = true },
 ]
 ```
+
+## Skip commits by GitHub PR label
+
+```jinja2
+{% if commit.github.pr_labels is containing("skip-release-notes") %}
+    {% continue %}
+{% endif %}
+```
+
+## Use GitHub PR labels as groups
+
+```toml
+[git]
+commit_parsers = [
+  { field = "github.pr_labels", pattern = "breaking-change", group = "<!-- 0 --> 🏗️ Breaking changes" },
+  { field = "github.pr_labels", pattern = "type/enhancement", group = "<!-- 1 --> 🚀 Features" },
+  { field = "github.pr_labels", pattern = "type/bug", group = "<!-- 2 --> 🐛 Fixes" },
+  { field = "github.pr_labels", pattern = "type/update", group = "<!-- 3 --> 🧪 Dependencies" },
+  { field = "github.pr_labels", pattern = "type/refactor", group = "<!-- 4 --> 🏭 Refactor" },
+  { field = "github.pr_labels", pattern = "area/documentation", group = "<!-- 5 --> 📝 Documentation" },
+  { field = "github.pr_labels", pattern = ".*", group = "<!-- 6 --> 🌀 Miscellaneous" },
+]
+```


Test patch:
diff --git a/.github/fixtures/test-regex-label-grouping/cliff.toml b/.github/fixtures/test-regex-label-grouping/cliff.toml
new file mode 100644
index 00000000..49ce37f
--- /dev/null
+++ b/.github/fixtures/test-regex-label-grouping/cliff.toml
@@ -0,0 +1,25 @@
+[changelog]
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.\n
+"""
+body = """
+### What's changed
+{% for group, commits in commits | group_by(attribute="group") %}
+  #### {{ group }}
+  {% for commit in commits -%}
+    - {{ commit.message }}
+  {% endfor -%}\n
+{% endfor %}\n
+"""
+footer = """
+<!-- generated by git-cliff -->
+"""
+trim = true
+
+[git]
+commit_parsers = [
+    { field = "author.name", pattern = "testa", group = "<!-- 0 -->TEST A" },
+    { field = "author.name", pattern = "testb", group = "<!-- 1 -->TEST B" },
+    { field = "author.name", pattern = "testc", group = "<!-- 2 -->TEST C" },
+]
diff --git a/.github/fixtures/test-regex-label-grouping/commit.sh b/.github/fixtures/test-regex-label-grouping/commit.sh
new file mode 100755
index 00000000..3a190fd
--- /dev/null
+++ b/.github/fixtures/test-regex-label-grouping/commit.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+set -e
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:08" git commit --allow-empty -m "Initial commit"
+GIT_COMMITTER_DATE="2022-04-06 01:25:09" git commit --allow-empty --author="testa <testa@address.com>" -m "feat: add feature 1"
+GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit --allow-empty --author="testa <testa@address.com>" -m "feat: add feature 2"
+GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit --allow-empty --author="testb <testb@address.com>" -m "feat: add feature 3"
+GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit --allow-empty --author="testb <testb@address.com>" -m "feat: add feature 4"
+GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit --allow-empty --author="testc <testc@address.com>" -m "feat: add feature 5"
+GIT_COMMITTER_DATE="2022-04-06 01:25:14" git commit --allow-empty --author="testc <testc@address.com>" -m "feat: add feature 6"    
diff --git a/.github/fixtures/test-regex-label-grouping/expected.md b/.github/fixtures/test-regex-label-grouping/expected.md
new file mode 100644
index 00000000..9320ce2
--- /dev/null
+++ b/.github/fixtures/test-regex-label-grouping/expected.md
@@ -0,0 +1,19 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+### What's changed
+
+#### <!-- 0 -->TEST A
+- add feature 1
+- add feature 2
+
+#### <!-- 1 -->TEST B
+- add feature 3
+- add feature 4
+
+#### <!-- 2 -->TEST C
+- add feature 5
+- add feature 6
+
+<!-- generated by git-cliff -->
\ No newline at end of file