Repository: orhun/git-cliff (rust)

Issue:
`--with-tag-message` has no effect with `--tag`
Note: Title changed "multiple repositories" to "`--tag`"

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Description of the bug

When using `--tag foo --with-tag-message bar` with multiple repositories, the `message` is `null` and the `version` is `foo` for all repositories.

### Steps To Reproduce

```
git cliff --repository repo1 repo2 --tag 0.1.0 --with-tag-message "test release" --context
```

### Expected behavior

`message` is `bar`

### Screenshots / Logs

_No response_

### Software information

- Project version: 2.4.0


### Additional context

_No response_

Pull request description:
fix(changelog): correctly set the tag message for the latest release
## Description

Now we support the following use case:

```sh
$ git cliff --unreleased --tag 1.2.3 --tag-message "Release 1.2.3"
```

## Motivation and Context

closes #836

## How Has This Been Tested?

Locally.

## 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/github-keepachangelog.toml b/examples/github-keepachangelog.toml
index 0c587aa..1a8f2f2 100644
--- a/examples/github-keepachangelog.toml
+++ b/examples/github-keepachangelog.toml
@@ -27,9 +27,9 @@ body = """
     ### {{ group | upper_first }}
     {%- for commit in commits %}
         - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
-            {% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}
-            {% if commit.github.pr_number %} in \
-            [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
+            {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+            {% if commit.remote.pr_number %} in \
+            [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
             {%- endif -%}
     {% endfor %}
 {% endfor %}
diff --git a/examples/github.toml b/examples/github.toml
index 9853f54..9ec3e91 100644
--- a/examples/github.toml
+++ b/examples/github.toml
@@ -14,15 +14,15 @@ body = """
 
 {%- if version %} in {{ version }}{%- endif -%}
 {% for commit in commits %}
-  {% if commit.github.pr_title -%}
-    {%- set commit_message = commit.github.pr_title -%}
+  {% if commit.remote.pr_title -%}
+    {%- set commit_message = commit.remote.pr_title -%}
   {%- else -%}
     {%- set commit_message = commit.message -%}
   {%- endif -%}
   * {{ commit_message | split(pat="\n") | first | trim }}\
-    {% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}
-    {% if commit.github.pr_number %} in \
-      [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+    {% if commit.remote.pr_number %} in \
+      [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
     {%- endif %}
 {%- endfor -%}
 
diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs
index 32317a5..36f1f5e 100644
--- a/git-cliff-core/src/changelog.rs
+++ b/git-cliff-core/src/changelog.rs
@@ -55,20 +55,16 @@ impl<'a> Changelog<'a> {
 		Ok(Self {
 			releases,
 			header_template: match &config.changelog.header {
-				Some(header) => Some(Template::new(header.to_string(), trim)?),
+				Some(header) => {
+					Some(Template::new("header", header.to_string(), trim)?)
+				}
 				None => None,
 			},
-			body_template: Template::new(
-				config
-					.changelog
-					.body
-					.as_deref()
-					.unwrap_or_default()
-					.to_string(),
-				trim,
-			)?,
+			body_template: get_body_template(config, trim)?,
 			footer_template: match &config.changelog.footer {
-				Some(footer) => Some(Template::new(footer.to_string(), trim)?),
+				Some(footer) => {
+					Some(Template::new("footer", footer.to_string(), trim)?)
+				}
 				None => None,
 			},
 			config,
@@ -162,7 +158,7 @@ impl<'a> Changelog<'a> {
 			.rev()
 			.filter(|release| {
 				if release.commits.is_empty() {
-					if let Some(version) = release.version.as_ref().cloned() {
+					if let Some(version) = release.version.clone() {
 						trace!("Release doesn't have any commits: {}", version);
 					}
 					false
@@ -475,6 +471,7 @@ impl<'a> Changelog<'a> {
 			} else {
 				(vec![], vec![])
 			};
+		#[cfg(feature = "remote")]
 		for release in self.releases.iter_mut() {
 			#[cfg(feature = "github")]
 			release.update_github_metadata(
@@ -612,6 +609,29 @@ impl<'a> Changelog<'a> {
 	}
 }
 
+fn get_body_template(config: &Config, trim: bool) -> Result<Template> {
+	let template_str = config
+		.changelog
+		.body
+		.as_deref()
+		.unwrap_or_default()
+		.to_string();
+	let template = Template::new("body", template_str, trim)?;
+	let deprecated_vars = [
+		"commit.github",
+		"commit.gitea",
+		"commit.gitlab",
+		"commit.bitbucket",
+	];
+	if template.contains_variable(&deprecated_vars) {
+		warn!(
+			"Variables {deprecated_vars:?} are deprecated and will be removed in \
+			 the future. Use `commit.remote` instead."
+		);
+	}
+	Ok(template)
+}
+
 #[cfg(test)]
 mod test {
 	use super::*;
diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs
index ebd5328..19b9a0c 100644
--- a/git-cliff-core/src/commit.rs
+++ b/git-cliff-core/src/commit.rs
@@ -125,18 +125,24 @@ pub struct Commit<'a> {
 	pub merge_commit:  bool,
 	/// Arbitrary data to be used with the `--from-context` CLI option.
 	pub extra:         Option<Value>,
+	/// Remote metadata of the commit.
+	pub remote:        Option<crate::contributor::RemoteContributor>,
 	/// GitHub metadata of the commit.
 	#[cfg(feature = "github")]
-	pub github:        crate::remote::RemoteContributor,
+	#[deprecated(note = "Use `remote` field instead")]
+	pub github:        crate::contributor::RemoteContributor,
 	/// GitLab metadata of the commit.
 	#[cfg(feature = "gitlab")]
-	pub gitlab:        crate::remote::RemoteContributor,
+	#[deprecated(note = "Use `remote` field instead")]
+	pub gitlab:        crate::contributor::RemoteContributor,
 	/// Gitea metadata of the commit.
 	#[cfg(feature = "gitea")]
-	pub gitea:         crate::remote::RemoteContributor,
+	#[deprecated(note = "Use `remote` field instead")]
+	pub gitea:         crate::contributor::RemoteContributor,
 	/// Bitbucket metadata of the commit.
 	#[cfg(feature = "bitbucket")]
-	pub bitbucket:     crate::remote::RemoteContributor,
+	#[deprecated(note = "Use `remote` field instead")]
+	pub bitbucket:     crate::contributor::RemoteContributor,
 }
 
 impl<'a> From<String> for Commit<'a> {
@@ -341,11 +347,9 @@ impl Commit<'_> {
 							}
 							value
 						};
-						self.group =
-							parser.group.as_ref().cloned().map(regex_replace);
-						self.scope =
-							parser.scope.as_ref().cloned().map(regex_replace);
-						self.default_scope = parser.default_scope.as_ref().cloned();
+						self.group = parser.group.clone().map(regex_replace);
+						self.scope = parser.scope.clone().map(regex_replace);
+						self.default_scope = parser.default_scope.clone();
 						return Ok(self);
 					}
 				}
@@ -398,6 +402,7 @@ impl Commit<'_> {
 }
 
 impl Serialize for Commit<'_> {
+	#[allow(deprecated)]
 	fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
 	where
 		S: Serializer,
@@ -466,6 +471,9 @@ impl Serialize for Commit<'_> {
 		commit.serialize_field("gitea", &self.gitea)?;
 		#[cfg(feature = "bitbucket")]
 		commit.serialize_field("bitbucket", &self.bitbucket)?;
+		if let Some(remote) = &self.remote {
+			commit.serialize_field("remote", remote)?;
+		}
 		commit.end()
 	}
 }
diff --git a/git-cliff-core/src/contributor.rs b/git-cliff-core/src/contributor.rs
new file mode 100644
index 00000000..957ce4a
--- /dev/null
+++ b/git-cliff-core/src/contributor.rs
@@ -0,0 +1,29 @@
+use serde::{
+	Deserialize,
+	Serialize,
+};
+use std::hash::{
+	Hash,
+	Hasher,
+};
+
+/// Representation of a remote contributor.
+#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
+pub struct RemoteContributor {
+	/// Username.
+	pub username:      Option<String>,
+	/// Title of the pull request.
+	pub pr_title:      Option<String>,
+	/// The pull request that the user created.
+	pub pr_number:     Option<i64>,
+	/// Labels of the pull request.
+	pub pr_labels:     Vec<String>,
+	/// Whether if the user contributed for the first time.
+	pub is_first_time: bool,
+}
+
+impl Hash for RemoteContributor {
+	fn hash<H: Hasher>(&self, state: &mut H) {
+		self.username.hash(state);
+	}
+}
diff --git a/git-cliff-core/src/embed.rs b/git-cliff-core/src/embed.rs
index 0cf2d29..79450df 100644
--- a/git-cliff-core/src/embed.rs
+++ b/git-cliff-core/src/embed.rs
@@ -4,6 +4,7 @@ use crate::error::{
 	Result,
 };
 use rust_embed::RustEmbed;
+use std::path::Path;
 use std::str;
 
 /// Default configuration file embedder/extractor.
@@ -44,7 +45,10 @@ pub struct BuiltinConfig;
 impl BuiltinConfig {
 	/// Extracts the embedded content.
 	pub fn get_config(mut name: String) -> Result<String> {
-		if !name.ends_with(".toml") {
+		if !Path::new(&name)
+			.extension()
+			.map_or(false, |ext| ext.eq_ignore_ascii_case("toml"))
+		{
 			name = format!("{name}.toml");
 		}
 		let contents = match Self::get(&name) {
diff --git a/git-cliff-core/src/lib.rs b/git-cliff-core/src/lib.rs
index 812e0e7..1851ca9 100644
--- a/git-cliff-core/src/lib.rs
+++ b/git-cliff-core/src/lib.rs
@@ -20,6 +20,8 @@ pub mod command;
 pub mod commit;
 /// Config file parser.
 pub mod config;
+/// Remote contributor.
+pub mod contributor;
 /// Embedded file handler.
 pub mod embed;
 /// Error handling.
diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs
index 3c9fcab..3e4dd6f 100644
--- a/git-cliff-core/src/release.rs
+++ b/git-cliff-core/src/release.rs
@@ -1,16 +1,18 @@
 use crate::error::Result;
-#[cfg(feature = "remote")]
-use crate::remote::{
-	RemoteCommit,
-	RemoteContributor,
-	RemotePullRequest,
-	RemoteReleaseMetadata,
-};
 use crate::{
 	commit::Commit,
 	config::Bump,
 	config::BumpType,
 };
+#[cfg(feature = "remote")]
+use crate::{
+	contributor::RemoteContributor,
+	remote::{
+		RemoteCommit,
+		RemotePullRequest,
+		RemoteReleaseMetadata,
+	},
+};
 
 use next_version::{
 	NextVersion,
@@ -179,6 +181,7 @@ impl<'a> Releases<'a> {
 #[cfg(test)]
 mod test {
 	use super::*;
+	use pretty_assertions::assert_eq;
 	#[test]
 	fn bump_version() -> Result<()> {
 		fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> {
@@ -539,6 +542,7 @@ mod test {
 			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
 			.collect(),
 		)?;
+		#[allow(deprecated)]
 		let expected_commits = vec![
 			Commit {
 				id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
@@ -550,6 +554,13 @@ mod test {
 					pr_labels:     vec![String::from("rust")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("1")),
+					pr_number:     Some(42),
+					pr_labels:     vec![String::from("rust")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -562,6 +573,13 @@ mod test {
 					pr_labels:     vec![String::from("rust")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("2")),
+					pr_number:     Some(66),
+					pr_labels:     vec![String::from("rust")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -574,6 +592,13 @@ mod test {
 					pr_labels:     vec![String::from("deps")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("nuhro")),
+					pr_title:      Some(String::from("3")),
+					pr_number:     Some(53),
+					pr_labels:     vec![String::from("deps")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -586,6 +611,13 @@ mod test {
 					pr_labels:     vec![String::from("deps")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("awesome_contributor")),
+					pr_title:      Some(String::from("4")),
+					pr_number:     Some(1000),
+					pr_labels:     vec![String::from("deps")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -598,6 +630,13 @@ mod test {
 					pr_labels:     vec![String::from("github")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("5")),
+					pr_number:     Some(999999),
+					pr_labels:     vec![String::from("github")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -610,6 +649,13 @@ mod test {
 					pr_labels:     vec![],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("someone")),
+					pr_title:      None,
+					pr_number:     None,
+					pr_labels:     vec![],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 		];
@@ -703,6 +749,7 @@ mod test {
 			github: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
+			#[cfg(feature = "gitlab")]
 			gitlab: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
@@ -884,6 +931,7 @@ mod test {
 				labels:            vec![String::from("rust")],
 			})],
 		)?;
+		#[allow(deprecated)]
 		let expected_commits = vec![
 			Commit {
 				id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
@@ -895,6 +943,13 @@ mod test {
 					pr_labels:     vec![String::from("rust")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("1")),
+					pr_number:     Some(1),
+					pr_labels:     vec![String::from("rust")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -907,6 +962,13 @@ mod test {
 					pr_labels:     vec![],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      None,
+					pr_number:     None,
+					pr_labels:     vec![],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -919,6 +981,13 @@ mod test {
 					pr_labels:     vec![],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("nuhro")),
+					pr_title:      None,
+					pr_number:     None,
+					pr_labels:     vec![],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -931,6 +1000,13 @@ mod test {
 					pr_labels:     vec![],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("awesome_contributor")),
+					pr_title:      None,
+					pr_number:     None,
+					pr_labels:     vec![],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -943,6 +1019,13 @@ mod test {
 					pr_labels:     vec![],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      None,
+					pr_number:     None,
+					pr_labels:     vec![],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -955,6 +1038,13 @@ mod test {
 					pr_labels:     vec![],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("someone")),
+					pr_title:      None,
+					pr_number:     None,
+					pr_labels:     vec![],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 		];
@@ -1053,6 +1143,7 @@ mod test {
 			gitlab: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
+			#[cfg(feature = "gitea")]
 			gitea: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
@@ -1173,6 +1264,7 @@ mod test {
 			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
 			.collect(),
 		)?;
+		#[allow(deprecated)]
 		let expected_commits = vec![
 			Commit {
 				id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
@@ -1184,6 +1276,13 @@ mod test {
 					pr_labels:     vec![String::from("rust")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("1")),
+					pr_number:     Some(42),
+					pr_labels:     vec![String::from("rust")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -1196,6 +1295,13 @@ mod test {
 					pr_labels:     vec![String::from("rust")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("2")),
+					pr_number:     Some(66),
+					pr_labels:     vec![String::from("rust")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -1208,6 +1314,13 @@ mod test {
 					pr_labels:     vec![String::from("deps")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("nuhro")),
+					pr_title:      Some(String::from("3")),
+					pr_number:     Some(53),
+					pr_labels:     vec![String::from("deps")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -1220,6 +1333,13 @@ mod test {
 					pr_labels:     vec![String::from("deps")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("awesome_contributor")),
+					pr_title:      Some(String::from("4")),
+					pr_number:     Some(1000),
+					pr_labels:     vec![String::from("deps")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -1232,6 +1352,13 @@ mod test {
 					pr_labels:     vec![String::from("github")],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("5")),
+					pr_number:     Some(999999),
+					pr_labels:     vec![String::from("github")],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 			Commit {
@@ -1244,6 +1371,13 @@ mod test {
 					pr_labels:     vec![],
 					is_first_time: false,
 				},
+				remote: Some(RemoteContributor {
+					username:      Some(String::from("someone")),
+					pr_title:      None,
+					pr_number:     None,
+					pr_labels:     vec![],
+					is_first_time: false,
+				}),
 				..Default::default()
 			},
 		];
diff --git a/git-cliff-core/src/remote/bitbucket.rs b/git-cliff-core/src/remote/bitbucket.rs
index 7597711..f45a47c 100644
--- a/git-cliff-core/src/remote/bitbucket.rs
+++ b/git-cliff-core/src/remote/bitbucket.rs
@@ -22,7 +22,8 @@ pub const START_FETCHING_MSG: &str = "Retrieving data from Bitbucket...";
 pub const FINISHED_FETCHING_MSG: &str = "Done fetching Bitbucket data.";
 
 /// Template variables related to this remote.
-pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["bitbucket", "commit.bitbucket"];
+pub(crate) const TEMPLATE_VARIABLES: &[&str] =
+	&["bitbucket", "commit.bitbucket", "commit.remote"];
 
 /// Maximum number of entries to fetch for bitbucket pull requests.
 pub(crate) const BITBUCKET_MAX_PAGE_PRS: usize = 50;
diff --git a/git-cliff-core/src/remote/gitea.rs b/git-cliff-core/src/remote/gitea.rs
index 251776c..f6df6b2 100644
--- a/git-cliff-core/src/remote/gitea.rs
+++ b/git-cliff-core/src/remote/gitea.rs
@@ -22,7 +22,8 @@ pub const START_FETCHING_MSG: &str = "Retrieving data from Gitea...";
 pub const FINISHED_FETCHING_MSG: &str = "Done fetching Gitea data.";
 
 /// Template variables related to this remote.
-pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["gitea", "commit.gitea"];
+pub(crate) const TEMPLATE_VARIABLES: &[&str] =
+	&["gitea", "commit.gitea", "commit.remote"];
 
 /// Representation of a single commit.
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
diff --git a/git-cliff-core/src/remote/github.rs b/git-cliff-core/src/remote/github.rs
index 91e5395..8a06a70 100644
--- a/git-cliff-core/src/remote/github.rs
+++ b/git-cliff-core/src/remote/github.rs
@@ -22,7 +22,8 @@ pub const START_FETCHING_MSG: &str = "Retrieving data from GitHub...";
 pub const FINISHED_FETCHING_MSG: &str = "Done fetching GitHub data.";
 
 /// Template variables related to this remote.
-pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["github", "commit.github"];
+pub(crate) const TEMPLATE_VARIABLES: &[&str] =
+	&["github", "commit.github", "commit.remote"];
 
 /// Representation of a single commit.
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
diff --git a/git-cliff-core/src/remote/gitlab.rs b/git-cliff-core/src/remote/gitlab.rs
index ba21b33..facf311 100644
--- a/git-cliff-core/src/remote/gitlab.rs
+++ b/git-cliff-core/src/remote/gitlab.rs
@@ -22,7 +22,8 @@ pub const START_FETCHING_MSG: &str = "Retrieving data from GitLab...";
 pub const FINISHED_FETCHING_MSG: &str = "Done fetching GitLab data.";
 
 /// Template variables related to this remote.
-pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["gitlab", "commit.gitlab"];
+pub(crate) const TEMPLATE_VARIABLES: &[&str] =
+	&["gitlab", "commit.gitlab", "commit.remote"];
 
 /// Representation of a single GitLab Project.
 ///
diff --git a/git-cliff-core/src/remote/mod.rs b/git-cliff-core/src/remote/mod.rs
index 626e1d2..77d14e7 100644
--- a/git-cliff-core/src/remote/mod.rs
+++ b/git-cliff-core/src/remote/mod.rs
@@ -15,6 +15,7 @@ pub mod bitbucket;
 pub mod gitea;
 
 use crate::config::Remote;
+use crate::contributor::RemoteContributor;
 use crate::error::{
 	Error,
 	Result,
@@ -48,10 +49,7 @@ use serde::{
 	Serialize,
 };
 use std::fmt::Debug;
-use std::hash::{
-	Hash,
-	Hasher,
-};
+use std::hash::Hash;
 use std::time::Duration;
 
 /// User agent for interacting with the GitHub API.
@@ -114,27 +112,6 @@ pub struct RemoteReleaseMetadata {
 	pub contributors: Vec<RemoteContributor>,
 }
 
-/// Representation of a remote contributor.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
-pub struct RemoteContributor {
-	/// Username.
-	pub username:      Option<String>,
-	/// Title of the pull request.
-	pub pr_title:      Option<String>,
-	/// The pull request that the user created.
-	pub pr_number:     Option<i64>,
-	/// Labels of the pull request.
-	pub pr_labels:     Vec<String>,
-	/// Whether if the user contributed for the first time.
-	pub is_first_time: bool,
-}
-
-impl Hash for RemoteContributor {
-	fn hash<H: Hasher>(&self, state: &mut H) {
-		self.username.hash(state);
-	}
-}
-
 /// Creates a HTTP client for the remote.
 fn create_remote_client(
 	remote: &Remote,
@@ -316,6 +293,7 @@ macro_rules! update_release_metadata {
 			///   username.
 			/// - Pull requests: needed for generating the contributor list for the
 			///   release.
+			#[allow(deprecated)]
 			pub fn $fn(
 				&mut self,
 				mut commits: Vec<Box<dyn RemoteCommit>>,
@@ -350,6 +328,7 @@ macro_rules! update_release_metadata {
 								is_first_time: false,
 							});
 						}
+						commit.remote = Some(commit.$remote.clone());
 						false
 					} else {
 						true
diff --git a/git-cliff-core/src/template.rs b/git-cliff-core/src/template.rs
index a18204b..4cbe496 100644
--- a/git-cliff-core/src/template.rs
+++ b/git-cliff-core/src/template.rs
@@ -22,6 +22,9 @@ use tera::{
 /// Wrapper for [`Tera`].
 #[derive(Debug)]
 pub struct Template {
+	/// Template name.
+	name:          String,
+	/// Internal Tera instance.
 	tera:          Tera,
 	/// Template variables.
 	#[cfg_attr(not(feature = "github"), allow(dead_code))]
@@ -30,16 +33,16 @@ pub struct Template {
 
 impl Template {
 	/// Constructs a new instance.
-	pub fn new(mut template: String, trim: bool) -> Result<Self> {
+	pub fn new(name: &str, mut content: String, trim: bool) -> Result<Self> {
 		if trim {
-			template = template
+			content = content
 				.lines()
 				.map(|v| v.trim())
 				.collect::<Vec<&str>>()
 				.join("\n");
 		}
 		let mut tera = Tera::default();
-		if let Err(e) = tera.add_raw_template("template", &template) {
+		if let Err(e) = tera.add_raw_template(name, &content) {
 			return if let Some(error_source) = e.source() {
 				Err(Error::TemplateParseError(error_source.to_string()))
 			} else {
@@ -48,7 +51,8 @@ impl Template {
 		}
 		tera.register_filter("upper_first", Self::upper_first_filter);
 		Ok(Self {
-			variables: Self::get_template_variables(&tera)?,
+			name: name.to_string(),
+			variables: Self::get_template_variables(name, &tera)?,
 			tera,
 		})
 	}
@@ -129,18 +133,17 @@ impl Template {
 	}
 
 	/// Returns the variable names that are used in the template.
-	fn get_template_variables(tera: &Tera) -> Result<Vec<String>> {
+	fn get_template_variables(name: &str, tera: &Tera) -> Result<Vec<String>> {
 		let mut variables = HashSet::new();
-		let ast = &tera.get_template("template")?.ast;
+		let ast = &tera.get_template(name)?.ast;
 		for node in ast {
 			Self::find_identifiers(node, &mut variables);
 		}
-		trace!("Template variables: {variables:?}");
+		trace!("Template variables for {name}: {variables:?}");
 		Ok(variables.into_iter().collect())
 	}
 
 	/// Returns `true` if the template contains one of the given variables.
-	#[cfg(feature = "remote")]
 	pub(crate) fn contains_variable(&self, variables: &[&str]) -> bool {
 		variables
 			.iter()
@@ -160,7 +163,7 @@ impl Template {
 				context.insert(key.clone(), &value);
 			}
 		}
-		match self.tera.render("template", &context) {
+		match self.tera.render(&self.name, &context) {
 			Ok(mut v) => {
 				for postprocessor in postprocessors {
 					postprocessor.replace(&mut v, vec![])?;
@@ -243,7 +246,7 @@ mod test {
 		### {{ commit.group }}
 		- {{ commit.message | upper_first }}
 		{% endfor %}"#;
-		let mut template = Template::new(template.to_string(), false)?;
+		let mut template = Template::new("test", template.to_string(), false)?;
 		let release = get_fake_release_data();
 		assert_eq!(
 			"\n\t\t## 1.0 - 2023\n\t\t\n\t\t### feat\n\t\t- Add xyz\n\t\t\n\t\t### \
@@ -282,7 +285,7 @@ mod test {
 		let template = r#"
 		##  {{ version }}
 		"#;
-		let template = Template::new(template.to_string(), true)?;
+		let template = Template::new("test", template.to_string(), true)?;
 		let release = get_fake_release_data();
 		assert_eq!(
 			"\n##  1.0\n",
@@ -301,7 +304,7 @@ mod test {
 		let template =
 			"{% set hello_variable = 'hello' %}{{ hello_variable | upper_first }}";
 		let release = get_fake_release_data();
-		let template = Template::new(template.to_string(), true)?;
+		let template = Template::new("test", template.to_string(), true)?;
 		let r = template.render(
 			&release,
 			Option::<HashMap<&str, String>>::None.as_ref(),
diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs
index 95a36d9..2767847 100644
--- a/git-cliff/src/lib.rs
+++ b/git-cliff/src/lib.rs
@@ -239,7 +239,7 @@ fn process_repository<'a>(
 		if let Some(commit_id) = commits.first().map(|c| c.id().to_string()) {
 			match tags.get(&commit_id) {
 				Some(tag) => {
-					warn!("There is already a tag ({:?}) for {}", tag, commit_id);
+					warn!("There is already a tag ({}) for {}", tag.name, commit_id);
 				}
 				None => {
 					tags.insert(commit_id, repository.resolve_tag(tag));
@@ -302,13 +302,6 @@ fn process_repository<'a>(
 			.extend(custom_commits.iter().cloned().map(Commit::from));
 	}
 
-	// Set custom message for the latest release.
-	if let Some(message) = &args.with_tag_message {
-		if let Some(latest_release) = releases.iter_mut().last() {
-			latest_release.message = Some(message.to_owned());
-		}
-	}
-
 	// Set the previous release if the first release does not have one set.
 	if releases[0]
 		.previous
@@ -343,6 +336,17 @@ fn process_repository<'a>(
 		}
 	}
 
+	// Set custom message for the latest release.
+	if let Some(message) = &args.with_tag_message {
+		if let Some(latest_release) = releases
+			.iter_mut()
+			.filter(|release| !release.commits.is_empty())
+			.last()
+		{
+			latest_release.message = Some(message.to_owned());
+		}
+	}
+
 	Ok(releases)
 }
 
diff --git a/website/docs/integration/bitbucket.md b/website/docs/integration/bitbucket.md
index 7295bb9..cf283a8 100644
--- a/website/docs/integration/bitbucket.md
+++ b/website/docs/integration/bitbucket.md
@@ -104,7 +104,7 @@ For each commit, Bitbucket related values are added as a nested object (named `b
 
   "...": "<strip>",
 
-  "bitbucket": {
+  "remote": {
     "username": "orhun",
     "pr_title": "some things have changed",
     "pr_number": 420,
@@ -119,8 +119,8 @@ This can be used in the template as follows:
 ```
 {% for commit in commits %}
   * {{ commit.message | split(pat="\n") | first | trim }}\
-    {% if commit.bitbucket.username %} by @{{ commit.bitbucket.username }}{%- endif %}\
-    {% if commit.bitbucket.pr_number %} in #{{ commit.bitbucket.pr_number }}{%- endif %}
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}\
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
 {%- endfor -%}
 ```
 
diff --git a/website/docs/integration/gitea.md b/website/docs/integration/gitea.md
index 8020bd2..1d355c7 100644
--- a/website/docs/integration/gitea.md
+++ b/website/docs/integration/gitea.md
@@ -104,7 +104,7 @@ For each commit, Gitea related values are added as a nested object (named `gitea
 
   "...": "<strip>",
 
-  "gitea": {
+  "remote": {
     "username": "orhun",
     "pr_title": "some things have changed",
     "pr_number": 420,
@@ -119,8 +119,8 @@ This can be used in the template as follows:
 ```
 {% for commit in commits %}
   * {{ commit.message | split(pat="\n") | first | trim }}\
-    {% if commit.gitea.username %} by @{{ commit.gitea.username }}{%- endif %}\
-    {% if commit.gitea.pr_number %} in #{{ commit.gitea.pr_number }}{%- endif %}
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}\
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
 {%- endfor -%}
 ```
 
diff --git a/website/docs/integration/github.md b/website/docs/integration/github.md
index d71e992..5afdec6 100644
--- a/website/docs/integration/github.md
+++ b/website/docs/integration/github.md
@@ -108,7 +108,7 @@ For each commit, GitHub related values are added as a nested object (named `gith
 
   "...": "<strip>",
 
-  "github": {
+  "remote": {
     "username": "orhun",
     "pr_title": "some things have changed",
     "pr_number": 420,
@@ -123,8 +123,8 @@ This can be used in the template as follows:
 ```
 {% for commit in commits %}
   * {{ commit.message | split(pat="\n") | first | trim }}\
-    {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
-    {% if commit.github.pr_number %} in #{{ commit.github.pr_number }}{%- endif %}
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}\
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
 {%- endfor -%}
 ```
 
diff --git a/website/docs/integration/gitlab.md b/website/docs/integration/gitlab.md
index 375f7b7..1db4f64 100644
--- a/website/docs/integration/gitlab.md
+++ b/website/docs/integration/gitlab.md
@@ -106,7 +106,7 @@ For each commit, GitLab related values are added as a nested object (named `gitl
 
   "...": "<strip>",
 
-  "gitlab": {
+  "remote": {
     "username": "orhun",
     "pr_title": "some things have changed",
     "pr_number": 420,
@@ -121,8 +121,8 @@ This can be used in the template as follows:
 ```
 {% for commit in commits %}
   * {{ commit.message | split(pat="\n") | first | trim }}\
-    {% if commit.gitlab.username %} by @{{ commit.gitlab.username }}{%- endif %}\
-    {% if commit.gitlab.pr_number %} in #{{ commit.gitlab.pr_number }}{%- endif %}
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}\
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
 {%- endfor -%}
 ```
 
diff --git a/website/docs/tips-and-tricks.md b/website/docs/tips-and-tricks.md
index 693bcc2..59960d0 100644
--- a/website/docs/tips-and-tricks.md
+++ b/website/docs/tips-and-tricks.md
@@ -65,7 +65,7 @@ commit_parsers = [
 ## Skip commits by GitHub PR label
 
 ```jinja2
-{% if commit.github.pr_labels is containing("skip-release-notes") %}
+{% if commit.remote.pr_labels is containing("skip-release-notes") %}
     {% continue %}
 {% endif %}
 ```


Test patch:
diff --git a/.github/fixtures/test-bitbucket-integration/cliff.toml b/.github/fixtures/test-bitbucket-integration/cliff.toml
index b3a6c73..9ad05d1 100644
--- a/.github/fixtures/test-bitbucket-integration/cliff.toml
+++ b/.github/fixtures/test-bitbucket-integration/cliff.toml
@@ -10,8 +10,8 @@ body = """
 {%- if version %} in {{ version }}{%- endif -%}
 {% for commit in commits %}
   * {{ commit.message | split(pat="\n") | first | trim }}\
-    {% if commit.bitbucket.username %} by @{{ commit.bitbucket.username }}{%- endif -%}
-    {% if commit.bitbucket.pr_number %} in #{{ commit.bitbucket.pr_number }}{%- endif %}
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
 {%- endfor -%}
 
 {% if bitbucket.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
diff --git a/.github/fixtures/test-gitea-integration/cliff.toml b/.github/fixtures/test-gitea-integration/cliff.toml
index 20c9b4e..00c548f 100644
--- a/.github/fixtures/test-gitea-integration/cliff.toml
+++ b/.github/fixtures/test-gitea-integration/cliff.toml
@@ -10,8 +10,8 @@ body = """
 {%- if version %} in {{ version }}{%- endif -%}
 {% for commit in commits %}
   * {{ commit.message | split(pat="\n") | first | trim }}\
-    {% if commit.gitea.username %} by @{{ commit.gitea.username }}{%- endif -%}
-    {% if commit.gitea.pr_number %} in #{{ commit.gitea.pr_number }}{%- endif %}
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
 {%- endfor -%}
 
 {% if gitea.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
diff --git a/.github/fixtures/test-github-integration/cliff.toml b/.github/fixtures/test-github-integration/cliff.toml
index 17adc95..0bb109c 100644
--- a/.github/fixtures/test-github-integration/cliff.toml
+++ b/.github/fixtures/test-github-integration/cliff.toml
@@ -11,8 +11,8 @@ body = """
 {%- if version %} in {{ version }}{%- endif -%}
 {% for commit in commits %}
   * {{ commit.message | split(pat="\n") | first | trim }}\
-    {% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}
-    {% if commit.github.pr_number %} in #{{ commit.github.pr_number }}{%- endif %}
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
 {%- endfor -%}
 
 {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
diff --git a/.github/fixtures/test-gitlab-integration/cliff.toml b/.github/fixtures/test-gitlab-integration/cliff.toml
index 70d8355..2490a37 100644
--- a/.github/fixtures/test-gitlab-integration/cliff.toml
+++ b/.github/fixtures/test-gitlab-integration/cliff.toml
@@ -11,8 +11,8 @@ body = """
 {%- if version %} in {{ version }}{%- endif -%}
 {% for commit in commits %}
   * {{ commit.message | split(pat="\n") | first | trim }}\
-    {% if commit.gitlab.username %} by @{{ commit.gitlab.username }}{%- endif -%}
-    {% if commit.gitlab.pr_number %} in #{{ commit.gitlab.pr_number }}{%- endif %}
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
 {%- endfor -%}
 
 {% if gitlab.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs
index 6952eac..45add26 100644
--- a/git-cliff-core/tests/integration_test.rs
+++ b/git-cliff-core/tests/integration_test.rs
@@ -261,7 +261,7 @@ fn generate_changelog() -> Result<()> {
 	];
 
 	let out = &mut String::new();
-	let template = Template::new(changelog_config.body.unwrap(), false)?;
+	let template = Template::new("test", changelog_config.body.unwrap(), false)?;
 
 	writeln!(out, "{}", changelog_config.header.unwrap()).unwrap();
 	for release in releases {