Repository: orhun/git-cliff (language: python)

Issue:
git-cliff-core: allow adding arbitrary values to commits
### Is there an existing issue or pull request for this?

- [X] I have searched the existing issues and pull requests

### Feature description

First of all thank you for the amazing crate and the hard work you put into it 🙏

I would like to attach a `remote` context to the commit, so that people can write `commit.remote.username` in release-plz changelog (which is similar to `commit.github.username` git-cliff equivalent.

Even though I can't have `commit.remote` I tried enabling the `"github"` feature and my Cargo.lock exploded because of many dependencies to retrieve remote info I'm not using (because I'm retrieving remote info by myself). So I would like to have a "lighter" way to add arbitrary fields to the `commit` object.

So a very similar mechanism to https://github.com/orhun/git-cliff/pull/613 but limited to the `commit` object.

### Desired solution


The `Commit` struct could have a Map of <String, Json> (this is psudo-code). In my example, "String" is `remote` and Json is `{ author }`.

Library users should be able to add any fields they like to it.

### Alternatives considered

The alternative is rely entirely on git-cliff for commits remote info retrieval.

My issue is that I generate multiple changelogs that share some commits, so we would hit GitHub API multiple times for the same commits for no reason.
We could solve this by implementing https://github.com/orhun/git-cliff/issues/468 but I'm not sure if it's easier than this issue.

Also, with this approach, the following problems remain:
- I would like to have `commit.remote` instead of `commit.github`, `commit.gitea`, etc. (unless you change my mind that `commit.github` is a better idea 👀)
- I prefer to avoid adding all those dependencies because release-plz already takes a while to compile 😵

### Additional context

PR where I experimented: https://github.com/MarcoIeni/release-plz/pull/1630

Also, I'm curious, why you have `commit.github`, `commit.gitea`, etc instead of a unified solution (e.g. `commit.remote`)?


Discussion / hints:
I just thought about the simplest solution to my issue: the Commit struct adds a `remote` field as Option. This field is not behind a feature flag (so I don't need to pull other dependencies).



Of course it's not a generic solution 👍
Hello Marco 👋🏼 

> The Commit struct could have a Map of <String, Json> (this is psudo-code). In my example, "String" is remote and Json is { author }.

Would that work? Is serde serializing keys as separate fields?

Btw can you take a look at #784 which does something similar (adds a new field called `extra`). Maybe we can tweak PR a bit to satisfy this use case too.

>  Also, I'm curious, why you have commit.github, commit.gitea, etc instead of a unified solution (e.g. commit.remote)?

There is a stale PR #688 which greatly simplifies that module.

> Would that work? Is serde serializing keys as separate fields?

I guess we would need to do what the linked PR is doing:

<img width="2002" alt="image" src="https://github.com/user-attachments/assets/b980cd5b-1f95-4277-94d3-73500e4f13f5">

----


> (adds a new field called extra). Maybe we can tweak PR a bit to satisfy this use case too.

Yeah, I would need something like that, but without typing `extra`. So as the `remote` HashMap 👍

Adding another field called `extra_map` which is an HashMap would work for example. 
This feels like a bit of a workaround and is specific to `release-plz`'s use case.

Maybe we should deprecate individual remote-related fields and instead support `commit.remote`, as you've suggested. That way, we wouldn't need to make small tweaks in the library like this.
Sounds good to me as long as the remote field is not behind a feature flag that pulls other dependencies 👍

Should I create a pr that adds this field?
Maybe for now we can just add the field without filling it so that it unlocks release-plz. I.e. it's a field reserved to library users that git cliff can use later after a refactoring 
Yup, please go ahead.

Fix patch:
diff --git a/examples/github-keepachangelog.toml b/examples/github-keepachangelog.toml
index 0c587aad70..1a8f2f2885 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 9853f54aae..9ec3e91916 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 06b77a252c..5b6aec765f 100644
--- a/git-cliff-core/src/changelog.rs
+++ b/git-cliff-core/src/changelog.rs
@@ -58,15 +58,7 @@ impl<'a> Changelog<'a> {
 				Some(header) => Some(Template::new(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)?),
 				None => None,
@@ -612,6 +604,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(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 a6005cf8e0..90346129cb 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> {
@@ -398,6 +404,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 +473,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 0000000000..957ce4a91a
--- /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/lib.rs b/git-cliff-core/src/lib.rs
index 812e0e777d..1851ca9cd1 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 3c9fcabb2d..3e4dd6f7be 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 7597711b39..f45a47cfe7 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 251776c300..f6df6b2e30 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 91e53956c2..8a06a706c9 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 5863013670..4bcf78f06f 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 626e1d26f7..77d14e72b5 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 f8c724d4a3..62e94cf7c0 100644
--- a/git-cliff-core/src/template.rs
+++ b/git-cliff-core/src/template.rs
@@ -140,7 +140,6 @@ impl Template {
 	}
 
 	/// 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()
diff --git a/website/docs/integration/bitbucket.md b/website/docs/integration/bitbucket.md
index 7295bb9ce3..cf283a8161 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 8020bd243f..1d355c7c26 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 d71e992bc6..5afdec67b9 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 375f7b7b4c..1db4f64da2 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 693bcc2b32..59960d08ce 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 b3a6c73abb..9ad05d1aff 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 20c9b4e34f..00c548fc44 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 17adc959d5..0bb109c19b 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 70d83555ef..2490a37abc 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 %}