Repository: orhun/git-cliff (rust)

Issue:
Change scope with commit_parsers 
**Is your feature request related to a problem? Please describe.**
In a monorepo, I that have a folder with the name "www", however, since I am using git-cliff to generate a public-facing, I would wish for this to be displayed as "Application" when generated.

I am using template replace functions to replace "www" with "Application" but would like the ability to change this in the `commit_parsers`.

**Describe the solution you'd like**
`commit_parsers` should allow changing of the scope on-demand. For example:

```toml
commit_parsers = [
  { message = "(www)", scope = "Application" }
]
```

**Describe alternatives you've considered**
N/A

**Additional context**
Thanks for an awesome project!


Pull request description:
feat(config): support changing commit scope with `commit_parsers`
## Description
<!--- Describe your changes in detail -->
I followed the instructions :)
Some new test cases are missing but I'll add as soon as possible
## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
For overriding commit scope.
<!--- If it fixes an open issue, please link to the issue here. -->
resolves #50 

## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
Current tests are passing. New tests are missing.
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

## Screenshots / Output (if appropriate):

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] 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.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.


Patch:
diff --git a/README.md b/README.md
index 3efd5a2..cba03a7 100644
--- a/README.md
+++ b/README.md
@@ -553,7 +553,8 @@ Examples:
   - Skip processing the commit if the commit message (description) starts with "revert".
 - `{ message = "^doc", group = "Documentation", default_scope = "other"},`
   - If the commit starts with "doc", group the commit as "Documentation" and set the default scope to "other". (e.g. `docs: xyz` will be processed as `docs(other): xyz`)
-
+- `{ message = "(www)", scope = "Application"}`
+  - If the commit contains "(www)", override the scope with "Application". Scoping order is: scope specification, conventional commit's scope and default scope.
 #### filter_commits
 
 If set to `true`, commits that are not matched by [commit parsers](#commit_parsers) are filtered out.
diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs
index f2dcd41..adb192f 100644
--- a/git-cliff-core/src/commit.rs
+++ b/git-cliff-core/src/commit.rs
@@ -22,18 +22,21 @@ use serde::ser::{
 #[serde(rename_all = "camelCase")]
 pub struct Commit<'a> {
 	/// Commit ID.
-	pub id:      String,
+	pub id:            String,
 	/// Commit message including title, description and summary.
-	pub message: String,
+	pub message:       String,
 	/// Conventional commit.
 	#[serde(skip_deserializing)]
-	pub conv:    Option<ConventionalCommit<'a>>,
+	pub conv:          Option<ConventionalCommit<'a>>,
 	/// Commit group based on a commit parser or its conventional type.
-	pub group:   Option<String>,
-	/// Commit scope based on conventional type or a commit parser.
-	pub scope:   Option<String>,
+	pub group:         Option<String>,
+	/// Default commit scope based on (inherited from) conventional type or a
+	/// commit parser.
+	pub default_scope: Option<String>,
+	/// Commit scope for overriding the default one.
+	pub scope:         Option<String>,
 	/// A list of links found in the commit
-	pub links:   Vec<Link>,
+	pub links:         Vec<Link>,
 }
 
 /// Object representing a link
@@ -63,6 +66,7 @@ impl Commit<'_> {
 			message,
 			conv: None,
 			group: None,
+			default_scope: None,
 			scope: None,
 			links: vec![],
 		}
@@ -159,7 +163,8 @@ impl Commit<'_> {
 				if regex.is_match(&text) {
 					if parser.skip != Some(true) {
 						self.group = parser.group.as_ref().cloned();
-						self.scope = parser.default_scope.as_ref().cloned();
+						self.scope = parser.scope.as_ref().cloned();
+						self.default_scope = parser.default_scope.as_ref().cloned();
 						return Ok(self);
 					} else {
 						return Err(AppError::GroupError(String::from(
@@ -236,13 +241,20 @@ impl Serialize for Commit<'_> {
 				commit.serialize_field("breaking", &conv.breaking())?;
 				commit.serialize_field(
 					"scope",
-					&conv.scope().map(|v| v.as_str()).or(self.scope.as_deref()),
+					&self
+						.scope
+						.as_deref()
+						.or_else(|| conv.scope().map(|v| v.as_str()))
+						.or(self.default_scope.as_deref()),
 				)?;
 			}
 			None => {
 				commit.serialize_field("message", &self.message)?;
 				commit.serialize_field("group", &self.group)?;
-				commit.serialize_field("scope", &self.scope)?;
+				commit.serialize_field(
+					"scope",
+					&self.scope.as_deref().or(self.default_scope.as_deref()),
+				)?;
 			}
 		}
 		commit.serialize_field("links", &self.links)?;
@@ -282,13 +294,14 @@ mod test {
 					body:          None,
 					group:         Some(String::from("test_group")),
 					default_scope: Some(String::from("test_scope")),
+					scope:         None,
 					skip:          None,
 				}],
 				false,
 			)
 			.unwrap();
 		assert_eq!(Some(String::from("test_group")), commit.group);
-		assert_eq!(Some(String::from("test_scope")), commit.scope);
+		assert_eq!(Some(String::from("test_scope")), commit.default_scope);
 	}
 
 	#[test]
diff --git a/git-cliff-core/src/config.rs b/git-cliff-core/src/config.rs
index 489d7ed..0510910 100644
--- a/git-cliff-core/src/config.rs
+++ b/git-cliff-core/src/config.rs
@@ -75,8 +75,10 @@ pub struct CommitParser {
 	pub body:          Option<Regex>,
 	/// Group of the commit.
 	pub group:         Option<String>,
-	/// Scope of the commit.
+	/// Default scope of the commit.
 	pub default_scope: Option<String>,
+	/// Commit scope for overriding the default scope.
+	pub scope:         Option<String>,
 	/// Whether to skip this commit group.
 	pub skip:          Option<bool>,
 }
diff --git a/git-cliff/src/changelog.rs b/git-cliff/src/changelog.rs
index dc871df..55e7f67 100644
--- a/git-cliff/src/changelog.rs
+++ b/git-cliff/src/changelog.rs
@@ -204,6 +204,7 @@ mod test {
 						body:          None,
 						group:         Some(String::from("New features")),
 						default_scope: Some(String::from("other")),
+						scope:         None,
 						skip:          None,
 					},
 					CommitParser {
@@ -211,6 +212,7 @@ mod test {
 						body:          None,
 						group:         Some(String::from("Bug Fixes")),
 						default_scope: None,
+						scope:         None,
 						skip:          None,
 					},
 					CommitParser {
@@ -218,13 +220,23 @@ mod test {
 						body:          None,
 						group:         None,
 						default_scope: None,
+						scope:         None,
 						skip:          Some(true),
 					},
+					CommitParser {
+						message:       Regex::new("doc:").ok(),
+						body:          None,
+						group:         Some(String::from("Documentation")),
+						default_scope: None,
+						scope:         Some(String::from("documentation")),
+						skip:          None,
+					},
 					CommitParser {
 						message:       Regex::new(".*").ok(),
 						body:          None,
 						group:         Some(String::from("Other")),
 						default_scope: Some(String::from("other")),
+						scope:         None,
 						skip:          None,
 					},
 				]),
@@ -260,6 +272,10 @@ mod test {
 					String::from("0w3rty"),
 					String::from("fix(ui): fix more stuff"),
 				),
+				Commit::new(
+					String::from("qw3rty"),
+					String::from("doc: update docs"),
+				),
 				Commit::new(
 					String::from("0jkl12"),
 					String::from("chore(app): do nothing"),
@@ -339,6 +355,10 @@ mod test {
 			#### ui
 			- fix more stuff
 
+			### Documentation
+			#### documentation
+			- update docs
+
 			### New features
 			#### app
 			- add cool features


Test patch:
diff --git a/.github/fixtures/test-custom-scope/cliff.toml b/.github/fixtures/test-custom-scope/cliff.toml
new file mode 100644
index 00000000..5b7ed0d
--- /dev/null
+++ b/.github/fixtures/test-custom-scope/cliff.toml
@@ -0,0 +1,37 @@
+[changelog]
+# changelog header
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.\n
+"""
+# template for the changelog body
+# https://tera.netlify.app/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for group, commits in commits | group_by(attribute="scope") %}
+        #### {{ group | upper_first }}
+        {% for commit in commits %}
+            - {{ commit.message | upper_first }}\
+        {% endfor %}
+    {% endfor %}\
+{% endfor %}\n
+"""
+# remove the leading and trailing whitespaces from the template
+trim = true
+# changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+
+[git]
+# regex for parsing and grouping commits
+commit_parsers = [
+    { message = "^feat", group = "Features", default_scope = "app"},
+    { message = "^fix", group = "Bug Fixes", scope = "cli"},
+]
diff --git a/.github/fixtures/test-custom-scope/commit.sh b/.github/fixtures/test-custom-scope/commit.sh
new file mode 100755
index 00000000..28b7b56
--- /dev/null
+++ b/.github/fixtures/test-custom-scope/commit.sh
@@ -0,0 +1,9 @@
+#!/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 -m "feat: add feature 1"
+GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit --allow-empty -m "fix: fix feature 1"
+GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit --allow-empty -m "feat(gui): add feature 2"
+GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit --allow-empty -m "fix(gui): fix feature 2"
+git tag v0.1.0
diff --git a/.github/fixtures/test-custom-scope/expected.md b/.github/fixtures/test-custom-scope/expected.md
new file mode 100644
index 00000000..9513fbc
--- /dev/null
+++ b/.github/fixtures/test-custom-scope/expected.md
@@ -0,0 +1,24 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [0.1.0] - 2022-04-06
+
+### Bug Fixes
+
+#### Cli
+
+- Fix feature 1
+- Fix feature 2
+
+### Features
+
+#### App
+
+- Add feature 1
+
+#### Gui
+
+- Add feature 2
+
+<!-- generated by git-cliff -->
diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs
index 493c2fe..a20c7f3 100644
--- a/git-cliff-core/tests/integration_test.rs
+++ b/git-cliff-core/tests/integration_test.rs
@@ -51,6 +51,7 @@ fn generate_changelog() -> Result<()> {
 				body:          None,
 				group:         Some(String::from("shiny features")),
 				default_scope: None,
+				scope:         None,
 				skip:          None,
 			},
 			CommitParser {
@@ -58,6 +59,15 @@ fn generate_changelog() -> Result<()> {
 				body:          None,
 				group:         Some(String::from("fix bugs")),
 				default_scope: None,
+				scope:         None,
+				skip:          None,
+			},
+			CommitParser {
+				message:       Regex::new("^test").ok(),
+				body:          None,
+				group:         None,
+				default_scope: None,
+				scope:         Some(String::from("tests")),
 				skip:          None,
 			},
 		]),
@@ -111,6 +121,10 @@ fn generate_changelog() -> Result<()> {
 					String::from("hjkl12"),
 					String::from("chore: do boring stuff"),
 				),
+				Commit::new(
+					String::from("hjkl13"),
+					String::from("test(x): test some stuff"),
+				),
 				Commit::new(
 					String::from("1234"),
 					String::from("fix: support preprocessing (fixes #99)"),
@@ -174,6 +188,9 @@ fn generate_changelog() -> Result<()> {
 - *(big-feature)* this is a breaking change
   - **BREAKING**: this is a breaking change
 
+### test
+- *(tests)* test some stuff
+
 ## Release v1.0.0
 
 ### chore