Repository: orhun/git-cliff (rust)

Issue:
Allow including tag-message in changelog
### Is there an existing issue or pull request for this?

- [X] I have searched the existing issues and pull requests

### Feature description

To have a "headline" for a release, it would be great allow including tag-messages (of release tags) into the changelog. For example, a changelog including this headline could look like this:

```
# Changelog

All notable changes to this project will be documented in this file.

## [1.0.1] - 2021-07-18

This is the release-tag message

### Miscellaneous Tasks
- Add release script

### Refactor

- Expose string functions
```


### Desired solution

Provide variable (such as `tag_message`) to the context, so it can be used as a variable within the changelog template.

### Alternatives considered

N/A

### Additional context

_No response_

Pull request description:
feat(changelog): add tag message to release context
## Description

added to the release context the tag's message if it exists

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

closes #623 

## 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. -->

## Screenshots / Logs (if applicable)

## 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)
- [X] 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/Cargo.lock b/Cargo.lock
index 6b7d467..e9113e4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1332,9 +1332,9 @@ dependencies = [
 
 [[package]]
 name = "lazy_static"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
 name = "libc"
diff --git a/Cargo.toml b/Cargo.toml
index 4647077..9f47692 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,7 +7,7 @@ regex = "1.10.5"
 glob = "0.3.1"
 log = "0.4.21"
 secrecy = { version = "0.8.0", features = ["serde"] }
-lazy_static = "1.4.0"
+lazy_static = "1.5.0"
 dirs = "5.0.1"
 
 [profile.dev]
diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs
index 870f633..5680d49 100644
--- a/git-cliff-core/src/changelog.rs
+++ b/git-cliff-core/src/changelog.rs
@@ -815,6 +815,7 @@ mod test {
 		};
 		let test_release = Release {
 			version: Some(String::from("v1.0.0")),
+			message: None,
 			commits: vec![
 				Commit::new(
 					String::from("coffee"),
@@ -909,6 +910,7 @@ mod test {
 			},
 			Release {
 				version: None,
+				message: None,
 				commits: vec![
 					Commit::new(
 						String::from("abc123"),
diff --git a/git-cliff-core/src/lib.rs b/git-cliff-core/src/lib.rs
index 6f78c91..812e0e7 100644
--- a/git-cliff-core/src/lib.rs
+++ b/git-cliff-core/src/lib.rs
@@ -33,6 +33,8 @@ pub mod remote;
 /// Git repository.
 #[cfg(feature = "repo")]
 pub mod repo;
+/// Git tag.
+pub mod tag;
 /// Template engine.
 pub mod template;
 
diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs
index 118134c..05720d2 100644
--- a/git-cliff-core/src/release.rs
+++ b/git-cliff-core/src/release.rs
@@ -21,6 +21,8 @@ use serde::{
 pub struct Release<'a> {
 	/// Release version, git tag.
 	pub version:   Option<String>,
+	/// git tag's message.
+	pub message:   Option<String>,
 	/// Commits made for the release.
 	pub commits:   Vec<Commit<'a>>,
 	/// Commit ID of the tag.
@@ -174,6 +176,7 @@ mod test {
 		fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> {
 			Release {
 				version: None,
+				message: None,
 				commits: commits
 					.iter()
 					.map(|v| Commit::from(v.to_string()))
@@ -363,6 +366,7 @@ mod test {
 
 		let mut release = Release {
 			version: None,
+			message: None,
 			commits: vec![
 				Commit::from(String::from(
 					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
@@ -648,6 +652,7 @@ mod test {
 
 		let mut release = Release {
 			version: None,
+			message: None,
 			commits: vec![
 				Commit::from(String::from(
 					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
@@ -991,6 +996,7 @@ mod test {
 
 		let mut release = Release {
 			version: None,
+			message: None,
 			commits: vec![
 				Commit::from(String::from(
 					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
diff --git a/git-cliff-core/src/repo.rs b/git-cliff-core/src/repo.rs
index 8723a64..50645c8 100644
--- a/git-cliff-core/src/repo.rs
+++ b/git-cliff-core/src/repo.rs
@@ -3,6 +3,7 @@ use crate::error::{
 	Error,
 	Result,
 };
+use crate::tag::Tag;
 use git2::{
 	BranchType,
 	Commit,
@@ -13,11 +14,20 @@ use git2::{
 };
 use glob::Pattern;
 use indexmap::IndexMap;
-use regex::Regex;
+use lazy_regex::{
+	lazy_regex,
+	Lazy,
+	Regex,
+};
 use std::io;
 use std::path::PathBuf;
 use url::Url;
 
+/// Regex for replacing the signature part of a tag message.
+static TAG_SIGNATURE_REGEX: Lazy<Regex> = lazy_regex!(
+	r"(?s)-----BEGIN PGP SIGNATURE-----(.*?)-----END PGP SIGNATURE-----"
+);
+
 /// Wrapper for [`Repository`] type from git2.
 ///
 /// [`Repository`]: GitRepository
@@ -95,11 +105,38 @@ impl Repository {
 	/// Returns the current tag.
 	///
 	/// It is the same as running `git describe --tags`
-	pub fn current_tag(&self) -> Option<String> {
+	pub fn current_tag(&self) -> Option<Tag> {
 		self.inner
 			.describe(DescribeOptions::new().describe_tags())
 			.ok()
-			.and_then(|describe| describe.format(None).ok())
+			.and_then(|describe| {
+				describe
+					.format(None)
+					.ok()
+					.map(|name| self.resolve_tag(&name))
+			})
+	}
+
+	/// Returns the tag object of the given name.
+	///
+	/// If given name doesn't exist, it still returns `Tag` with the given name.
+	pub fn resolve_tag(&self, name: &str) -> Tag {
+		match self
+			.inner
+			.resolve_reference_from_short_name(name)
+			.and_then(|r| r.peel_to_tag())
+		{
+			Ok(tag) => Tag {
+				name:    tag.name().unwrap_or_default().to_owned(),
+				message: tag.message().map(|msg| {
+					TAG_SIGNATURE_REGEX.replace(msg, "").trim().to_owned()
+				}),
+			},
+			_ => Tag {
+				name:    name.to_owned(),
+				message: None,
+			},
+		}
 	}
 
 	/// Returns the commit object of the given ID.
@@ -119,8 +156,8 @@ impl Repository {
 		&self,
 		pattern: &Option<Regex>,
 		topo_order: bool,
-	) -> Result<IndexMap<String, String>> {
-		let mut tags: Vec<(Commit, String)> = Vec::new();
+	) -> Result<IndexMap<String, Tag>> {
+		let mut tags: Vec<(Commit, Tag)> = Vec::new();
 		let tag_names = self.inner.tag_names(None)?;
 		for name in tag_names
 			.iter()
@@ -132,14 +169,22 @@ impl Repository {
 		{
 			let obj = self.inner.revparse_single(&name)?;
 			if let Ok(commit) = obj.clone().into_commit() {
-				tags.push((commit, name));
+				tags.push((commit, Tag {
+					name,
+					message: None,
+				}));
 			} else if let Some(tag) = obj.as_tag() {
 				if let Some(commit) = tag
 					.target()
 					.ok()
 					.and_then(|target| target.into_commit().ok())
 				{
-					tags.push((commit, name));
+					tags.push((commit, Tag {
+						name:    tag.name().map(String::from).unwrap_or(name),
+						message: tag.message().map(|msg| {
+							TAG_SIGNATURE_REGEX.replace(msg, "").trim().to_owned()
+						}),
+					}));
 				}
 			}
 		}
@@ -261,7 +306,7 @@ mod test {
 	fn get_latest_tag() -> Result<()> {
 		let repository = get_repository()?;
 		let tags = repository.tags(&None, false)?;
-		assert_eq!(&get_last_tag()?, tags.last().expect("no tags found").1);
+		assert_eq!(get_last_tag()?, tags.last().expect("no tags found").1.name);
 		Ok(())
 	}
 
@@ -270,16 +315,20 @@ mod test {
 		let repository = get_repository()?;
 		let tags = repository.tags(&None, true)?;
 		assert_eq!(
-			tags.get("2b8b4d3535f29231e05c3572e919634b9af907b6").expect(
-				"the commit hash does not exist in the repository (tag v0.1.0)"
-			),
+			tags.get("2b8b4d3535f29231e05c3572e919634b9af907b6")
+				.expect(
+					"the commit hash does not exist in the repository (tag v0.1.0)"
+				)
+				.name,
 			"v0.1.0"
 		);
 		assert_eq!(
-			tags.get("4ddef08debfff48117586296e49d5caa0800d1b5").expect(
-				"the commit hash does not exist in the repository (tag \
-				 v0.1.0-beta.4)"
-			),
+			tags.get("4ddef08debfff48117586296e49d5caa0800d1b5")
+				.expect(
+					"the commit hash does not exist in the repository (tag \
+					 v0.1.0-beta.4)"
+				)
+				.name,
 			"v0.1.0-beta.4"
 		);
 		let tags = repository.tags(
@@ -290,9 +339,11 @@ mod test {
 			true,
 		)?;
 		assert_eq!(
-			tags.get("2b8b4d3535f29231e05c3572e919634b9af907b6").expect(
-				"the commit hash does not exist in the repository (tag v0.1.0)"
-			),
+			tags.get("2b8b4d3535f29231e05c3572e919634b9af907b6")
+				.expect(
+					"the commit hash does not exist in the repository (tag v0.1.0)"
+				)
+				.name,
 			"v0.1.0"
 		);
 		assert!(!tags.contains_key("4ddef08debfff48117586296e49d5caa0800d1b5"));
@@ -313,4 +364,30 @@ mod test {
 		);
 		Ok(())
 	}
+
+	#[test]
+	fn resolves_existing_tag_with_name_and_message() -> Result<()> {
+		let repository = get_repository()?;
+		let tag = repository.resolve_tag("v0.2.3");
+		assert_eq!(tag.name, "v0.2.3");
+		assert_eq!(
+			tag.message,
+			Some(
+				"Release v0.2.3\n\nBug Fixes\n- Fetch the dependencies before \
+				 copying the file to embed (9e29c95)"
+					.to_string()
+			)
+		);
+
+		Ok(())
+	}
+
+	#[test]
+	fn resolves_tag_when_no_tags_exist() -> Result<()> {
+		let repository = get_repository()?;
+		let tag = repository.resolve_tag("nonexistent-tag");
+		assert_eq!(tag.name, "nonexistent-tag");
+		assert_eq!(tag.message, None);
+		Ok(())
+	}
 }
diff --git a/git-cliff-core/src/tag.rs b/git-cliff-core/src/tag.rs
new file mode 100644
index 00000000..7fe8ab9
--- /dev/null
+++ b/git-cliff-core/src/tag.rs
@@ -0,0 +1,47 @@
+/// Common tag object that is parsed from a repository.
+///
+/// Lightweight tags will have `None` as message.
+#[derive(Debug)]
+pub struct Tag {
+	/// The name of the tag
+	pub name:    String,
+	/// The message of the tag (only if it was annotated).
+	pub message: Option<String>,
+}
+
+#[cfg(test)]
+mod test {
+	use super::*;
+
+	#[test]
+	fn create_tag_with_name_and_message() {
+		let tag = Tag {
+			name:    String::from("v1.0"),
+			message: Some(String::from("Initial release")),
+		};
+		assert_eq!(tag.name, "v1.0");
+		assert_eq!(tag.message, Some(String::from("Initial release")));
+	}
+
+	#[test]
+	fn create_tag_with_name_and_no_message() {
+		let tag = Tag {
+			name:    String::from("v1.0"),
+			message: None,
+		};
+		assert_eq!(tag.name, "v1.0");
+		assert_eq!(tag.message, None);
+	}
+
+	#[test]
+	fn debug_print_tag_with_message() {
+		let tag = Tag {
+			name:    String::from("v1.0"),
+			message: Some(String::from("Initial release")),
+		};
+		assert_eq!(
+			format!("{:?}", tag),
+			"Tag { name: \"v1.0\", message: Some(\"Initial release\") }"
+		);
+	}
+}
diff --git a/git-cliff-core/src/template.rs b/git-cliff-core/src/template.rs
index d5dbf93..3fe0e8a 100644
--- a/git-cliff-core/src/template.rs
+++ b/git-cliff-core/src/template.rs
@@ -189,6 +189,7 @@ mod test {
 	fn get_fake_release_data() -> Release<'static> {
 		Release {
 			version: Some(String::from("1.0")),
+			message: None,
 			commits: vec![
 				Commit::new(
 					String::from("123123"),
diff --git a/git-cliff/src/args.rs b/git-cliff/src/args.rs
index 1fcd1c3..4c76985 100644
--- a/git-cliff/src/args.rs
+++ b/git-cliff/src/args.rs
@@ -64,7 +64,7 @@ pub struct Opt {
 		help = "Prints help information",
 		help_heading = "FLAGS"
 	)]
-	pub help:            Option<bool>,
+	pub help:             Option<bool>,
 	#[arg(
 		short = 'V',
 		long,
@@ -73,10 +73,10 @@ pub struct Opt {
 		help = "Prints version information",
 		help_heading = "FLAGS"
 	)]
-	pub version:         Option<bool>,
+	pub version:          Option<bool>,
 	/// Increases the logging verbosity.
 	#[arg(short, long, action = ArgAction::Count, alias = "debug", help_heading = Some("FLAGS"))]
-	pub verbose:         u8,
+	pub verbose:          u8,
 	/// Writes the default configuration file to cliff.toml
 	#[arg(
 	    short,
@@ -85,7 +85,7 @@ pub struct Opt {
 	    num_args = 0..=1,
 	    required = false
 	)]
-	pub init:            Option<Option<String>>,
+	pub init:             Option<Option<String>>,
 	/// Sets the configuration file.
 	#[arg(
 	    short,
@@ -95,7 +95,7 @@ pub struct Opt {
 	    default_value = DEFAULT_CONFIG,
 	    value_parser = Opt::parse_dir
 	)]
-	pub config:          PathBuf,
+	pub config:           PathBuf,
 	/// Sets the working directory.
 	#[arg(
 	    short,
@@ -104,7 +104,7 @@ pub struct Opt {
 	    value_name = "PATH",
 	    value_parser = Opt::parse_dir
 	)]
-	pub workdir:         Option<PathBuf>,
+	pub workdir:          Option<PathBuf>,
 	/// Sets the git repository.
 	#[arg(
 		short,
@@ -114,7 +114,7 @@ pub struct Opt {
 		num_args(1..),
 		value_parser = Opt::parse_dir
 	)]
-	pub repository:      Option<Vec<PathBuf>>,
+	pub repository:       Option<Vec<PathBuf>>,
 	/// Sets the path to include related commits.
 	#[arg(
 		long,
@@ -122,7 +122,7 @@ pub struct Opt {
 		value_name = "PATTERN",
 		num_args(1..)
 	)]
-	pub include_path:    Option<Vec<Pattern>>,
+	pub include_path:     Option<Vec<Pattern>>,
 	/// Sets the path to exclude related commits.
 	#[arg(
 		long,
@@ -130,10 +130,10 @@ pub struct Opt {
 		value_name = "PATTERN",
 		num_args(1..)
 	)]
-	pub exclude_path:    Option<Vec<Pattern>>,
+	pub exclude_path:     Option<Vec<Pattern>>,
 	/// Sets the regex for matching git tags.
 	#[arg(long, env = "GIT_CLIFF_TAG_PATTERN", value_name = "PATTERN")]
-	pub tag_pattern:     Option<Regex>,
+	pub tag_pattern:      Option<Regex>,
 	/// Sets custom commit messages to include in the changelog.
 	#[arg(
 		long,
@@ -141,10 +141,18 @@ pub struct Opt {
 		value_name = "MSG",
 		num_args(1..)
 	)]
-	pub with_commit:     Option<Vec<String>>,
+	pub with_commit:      Option<Vec<String>>,
+	/// Sets custom message for the latest release.
+	#[arg(
+		long,
+		env = "GIT_CLIFF_WITH_TAG_MESSAGE",
+		value_name = "MSG",
+		num_args = 0..=1,
+	)]
+	pub with_tag_message: Option<String>,
 	/// Sets the tags to ignore in the changelog.
 	#[arg(long, env = "GIT_CLIFF_IGNORE_TAGS", value_name = "PATTERN")]
-	pub ignore_tags:     Option<Regex>,
+	pub ignore_tags:      Option<Regex>,
 	/// Sets commits that will be skipped in the changelog.
 	#[arg(
 		long,
@@ -152,7 +160,7 @@ pub struct Opt {
 		value_name = "SHA1",
 		num_args(1..)
 	)]
-	pub skip_commit:     Option<Vec<String>>,
+	pub skip_commit:      Option<Vec<String>>,
 	/// Prepends entries to the given changelog file.
 	#[arg(
 	    short,
@@ -161,7 +169,7 @@ pub struct Opt {
 	    value_name = "PATH",
 	    value_parser = Opt::parse_dir
 	)]
-	pub prepend:         Option<PathBuf>,
+	pub prepend:          Option<PathBuf>,
 	/// Writes output to the given file.
 	#[arg(
 	    short,
@@ -172,7 +180,7 @@ pub struct Opt {
 	    num_args = 0..=1,
 	    default_missing_value = DEFAULT_OUTPUT
 	)]
-	pub output:          Option<PathBuf>,
+	pub output:           Option<PathBuf>,
 	/// Sets the tag for the latest version.
 	#[arg(
 		short,
@@ -181,13 +189,13 @@ pub struct Opt {
 		value_name = "TAG",
 		allow_hyphen_values = true
 	)]
-	pub tag:             Option<String>,
+	pub tag:              Option<String>,
 	/// Bumps the version for unreleased changes.
 	#[arg(long, help_heading = Some("FLAGS"))]
-	pub bump:            bool,
+	pub bump:             bool,
 	/// Prints bumped version for unreleased changes.
 	#[arg(long, help_heading = Some("FLAGS"))]
-	pub bumped_version:  bool,
+	pub bumped_version:   bool,
 	/// Sets the template for the changelog body.
 	#[arg(
 		short,
@@ -196,38 +204,38 @@ pub struct Opt {
 		value_name = "TEMPLATE",
 		allow_hyphen_values = true
 	)]
-	pub body:            Option<String>,
+	pub body:             Option<String>,
 	/// Processes the commits starting from the latest tag.
 	#[arg(short, long, help_heading = Some("FLAGS"))]
-	pub latest:          bool,
+	pub latest:           bool,
 	/// Processes the commits that belong to the current tag.
 	#[arg(long, help_heading = Some("FLAGS"))]
-	pub current:         bool,
+	pub current:          bool,
 	/// Processes the commits that do not belong to a tag.
 	#[arg(short, long, help_heading = Some("FLAGS"))]
-	pub unreleased:      bool,
+	pub unreleased:       bool,
 	/// Sorts the tags topologically.
 	#[arg(long, help_heading = Some("FLAGS"))]
-	pub topo_order:      bool,
+	pub topo_order:       bool,
 	/// Disables the external command execution.
 	#[arg(long, help_heading = Some("FLAGS"))]
-	pub no_exec:         bool,
+	pub no_exec:          bool,
 	/// Prints changelog context as JSON.
 	#[arg(short = 'x', long, help_heading = Some("FLAGS"))]
-	pub context:         bool,
+	pub context:          bool,
 	/// Strips the given parts from the changelog.
 	#[arg(short, long, value_name = "PART", value_enum)]
-	pub strip:           Option<Strip>,
+	pub strip:            Option<Strip>,
 	/// Sets sorting of the commits inside sections.
 	#[arg(
 		long,
 		value_enum,
 		default_value_t = Sort::Oldest
 	)]
-	pub sort:            Sort,
+	pub sort:             Sort,
 	/// Sets the commit range to process.
 	#[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
-	pub range:           Option<String>,
+	pub range:            Option<String>,
 	/// Sets the GitHub API token.
 	#[arg(
 		long,
@@ -236,7 +244,7 @@ pub struct Opt {
 		hide_env_values = true,
 		hide = !cfg!(feature = "github"),
 	)]
-	pub github_token:    Option<SecretString>,
+	pub github_token:     Option<SecretString>,
 	/// Sets the GitHub repository.
 	#[arg(
 		long,
@@ -245,7 +253,7 @@ pub struct Opt {
 		value_name = "OWNER/REPO",
 		hide = !cfg!(feature = "github"),
 	)]
-	pub github_repo:     Option<RemoteValue>,
+	pub github_repo:      Option<RemoteValue>,
 	/// Sets the GitLab API token.
 	#[arg(
 		long,
@@ -254,7 +262,7 @@ pub struct Opt {
 		hide_env_values = true,
 		hide = !cfg!(feature = "gitlab"),
 	)]
-	pub gitlab_token:    Option<SecretString>,
+	pub gitlab_token:     Option<SecretString>,
 	/// Sets the GitLab repository.
 	#[arg(
 		long,
@@ -263,7 +271,7 @@ pub struct Opt {
 		value_name = "OWNER/REPO",
 		hide = !cfg!(feature = "gitlab"),
 	)]
-	pub gitlab_repo:     Option<RemoteValue>,
+	pub gitlab_repo:      Option<RemoteValue>,
 	/// Sets the Gitea API token.
 	#[arg(
 		long,
@@ -272,7 +280,7 @@ pub struct Opt {
 		hide_env_values = true,
 		hide = !cfg!(feature = "gitea"),
 	)]
-	pub gitea_token:     Option<SecretString>,
+	pub gitea_token:      Option<SecretString>,
 	/// Sets the GitLab repository.
 	#[arg(
 		long,
@@ -281,7 +289,7 @@ pub struct Opt {
 		value_name = "OWNER/REPO",
 		hide = !cfg!(feature = "gitea"),
 	)]
-	pub gitea_repo:      Option<RemoteValue>,
+	pub gitea_repo:       Option<RemoteValue>,
 	/// Sets the Bitbucket API token.
 	#[arg(
 		long,
@@ -290,7 +298,7 @@ pub struct Opt {
 		hide_env_values = true,
 		hide = !cfg!(feature = "bitbucket"),
 	)]
-	pub bitbucket_token: Option<SecretString>,
+	pub bitbucket_token:  Option<SecretString>,
 	/// Sets the Bitbucket repository.
 	#[arg(
 		long,
@@ -299,7 +307,7 @@ pub struct Opt {
 		value_name = "OWNER/REPO",
 		hide = !cfg!(feature = "bitbucket"),
 	)]
-	pub bitbucket_repo:  Option<RemoteValue>,
+	pub bitbucket_repo:   Option<RemoteValue>,
 }
 
 /// Custom type for the remote value.
diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs
index a58c5d5..6fc8e8c 100644
--- a/git-cliff/src/lib.rs
+++ b/git-cliff/src/lib.rs
@@ -90,7 +90,9 @@ fn process_repository<'a>(
 	let ignore_regex = config.git.ignore_tags.as_ref();
 	tags = tags
 		.into_iter()
-		.filter(|(_, name)| {
+		.filter(|(_, tag)| {
+			let name = &tag.name;
+
 			// Keep skip tags to drop commits in the later stage.
 			let skip = skip_regex.map(|r| r.is_match(name)).unwrap_or_default();
 
@@ -184,7 +186,7 @@ fn process_repository<'a>(
 					repository.current_tag().as_ref().and_then(|tag| {
 						tags.iter()
 							.enumerate()
-							.find(|(_, (_, v))| v == &tag)
+							.find(|(_, (_, v))| v.name == tag.name)
 							.map(|(i, _)| i)
 					}) {
 					match current_tag_index.checked_sub(1) {
@@ -226,10 +228,10 @@ 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, commit_id)
 				}
 				None => {
-					tags.insert(commit_id, tag.to_string());
+					tags.insert(commit_id, repository.resolve_tag(tag));
 				}
 			}
 		}
@@ -249,9 +251,13 @@ fn process_repository<'a>(
 			releases[release_index].commits.push(commit);
 		}
 		if let Some(tag) = tags.get(&commit_id) {
-			releases[release_index].version = Some(tag.to_string());
+			let tag_name = &tag.name;
+
+			releases[release_index].version = Some(tag_name.clone());
+			releases[release_index].message = tag.message.clone();
 			releases[release_index].commit_id = Some(commit_id);
-			releases[release_index].timestamp = if args.tag.as_deref() == Some(tag) {
+			releases[release_index].timestamp = if args.tag == Some(tag_name.clone())
+			{
 				SystemTime::now()
 					.duration_since(UNIX_EPOCH)?
 					.as_secs()
@@ -286,6 +292,13 @@ 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().last() {
+			latest_release.message = Some(message.to_owned());
+		}
+	}
+
 	// Set the previous release if the first release does not have one set.
 	if !releases.is_empty() &&
 		releases
@@ -299,7 +312,7 @@ fn process_repository<'a>(
 			.map(|tag| {
 				tags.iter()
 					.enumerate()
-					.find(|(_, (_, v))| v == &tag)
+					.find(|(_, (_, v))| v.name == tag.name)
 					.and_then(|(i, _)| i.checked_sub(1))
 					.and_then(|i| tags.get_index(i))
 			})
@@ -307,10 +320,10 @@ fn process_repository<'a>(
 			.flatten();
 
 		// Set the previous release if the first tag is found.
-		if let Some((commit_id, version)) = first_tag {
+		if let Some((commit_id, tag)) = first_tag {
 			let previous_release = Release {
 				commit_id: Some(commit_id.to_string()),
-				version: Some(version.to_string()),
+				version: Some(tag.name.clone()),
 				timestamp: repository
 					.find_commit(commit_id.to_string())
 					.map(|v| v.time().seconds())
diff --git a/website/docs/templating/context.md b/website/docs/templating/context.md
index 2afebdf..9d61db8 100644
--- a/website/docs/templating/context.md
+++ b/website/docs/templating/context.md
@@ -25,6 +25,7 @@ following context is generated to use for templating:
 ```json
 {
   "version": "v0.1.0-rc.21",
+  "message": "The annotated tag message for the release"
   "commits": [
     {
       "id": "e795460c9bb7275294d1fa53a9d73258fb51eb10",
@@ -129,6 +130,7 @@ If [`conventional_commits`](/docs/configuration/git#conventional_commits) is set
 ```json
 {
   "version": "v0.1.0-rc.21",
+  "message": "The annotated tag message for the release"
   "commits": [
     {
       "id": "e795460c9bb7275294d1fa53a9d73258fb51eb10",
diff --git a/website/docs/usage/adding-tag-messages.md b/website/docs/usage/adding-tag-messages.md
new file mode 100644
index 00000000..7e4f569
--- /dev/null
+++ b/website/docs/usage/adding-tag-messages.md
@@ -0,0 +1,42 @@
+---
+sidebar_position: 9
+---
+
+# Adding version (tag) message
+
+Sometimes, you might want to include a special message or note related to a version of your project.
+This can be used to highlight significant milestones, provide additional context, or share information not captured by individual commit messages.
+
+There are currently 2 ways of doing this, in both ways, the message is available in the context of the template under the name `message`:
+
+```
+{% if message %}
+    {{ message }}
+{% endif %}\
+```
+
+## Using annotated tags
+
+The recommended way of adding a version message is to add the message to the tag:
+
+```bash
+git tag v1.0.0 -m "first release, yay!"
+```
+
+So in the release's context, `message` will be "first release, yay!" (even if it is signed).
+
+## Using `--with-tag-message`
+
+If for some reason you don't want to have the message in the tag (or don't have a tag yet) but want to include it in the generated changelog, you can use the `--with-tag-message` flag:
+
+```bash
+git cliff --bump --unreleased --with-tag-message "some text"
+```
+
+In this case, you can only add a message to the latest release.
+
+:::note
+
+Please note that if you use `--with-tag-message` on a version it will ignore the original tag's message and use the one from the argument.
+
+:::
diff --git a/website/docs/usage/args.md b/website/docs/usage/args.md
index 5219301..efd9b3e 100644
--- a/website/docs/usage/args.md
+++ b/website/docs/usage/args.md
@@ -20,33 +20,38 @@ git-cliff [FLAGS] [OPTIONS] [--] [RANGE]
     --current         Processes the commits that belong to the current tag
 -u, --unreleased      Processes the commits that do not belong to a tag
     --topo-order      Sorts the tags topologically
--x, --context         Prints changelog context as JSON
     --no-exec         Disables the external command execution
+-x, --context         Prints changelog context as JSON
 ```
 
 ## Options
 
 ```
--i, --init [<CONFIG>]            Writes the default configuration file to cliff.toml
--c, --config <PATH>              Sets the configuration file [env: GIT_CLIFF_CONFIG=] [default: cliff.toml]
--w, --workdir <PATH>             Sets the working directory [env: GIT_CLIFF_WORKDIR=]
--r, --repository <PATH>...       Sets the git repository [env: GIT_CLIFF_REPOSITORY=]
-    --include-path <PATTERN>...  Sets the path to include related commits [env: GIT_CLIFF_INCLUDE_PATH=]
-    --exclude-path <PATTERN>...  Sets the path to exclude related commits [env: GIT_CLIFF_EXCLUDE_PATH=]
-    --tag-pattern <PATTERN>      Sets the regex for matching git tags [env: GIT_CLIFF_TAG_PATTERN=]
-    --with-commit <MSG>...       Sets custom commit messages to include in the changelog [env: GIT_CLIFF_WITH_COMMIT=]
-    --ignore-tags <PATTERN>      Sets the tags to ignore in the changelog [env: GIT_CLIFF_IGNORE_TAGS=]
-    --skip-commit <SHA1>...      Sets commits that will be skipped in the changelog [env: GIT_CLIFF_SKIP_COMMIT=]
--p, --prepend <PATH>             Prepends entries to the given changelog file [env: GIT_CLIFF_PREPEND=]
--o, --output [<PATH>]            Writes output to the given file [env: GIT_CLIFF_OUTPUT=]
--t, --tag <TAG>                  Sets the tag for the latest version [env: GIT_CLIFF_TAG=]
--b, --body <TEMPLATE>            Sets the template for the changelog body [env: GIT_CLIFF_TEMPLATE=]
--s, --strip <PART>               Strips the given parts from the changelog [possible values: header, footer, all]
-    --sort <SORT>                Sets sorting of the commits inside sections [default: oldest] [possible values: oldest, newest]
-    --github-token <TOKEN>       Sets the GitHub API token [env: GITHUB_TOKEN]
-    --github-repo <OWNER/REPO>   Sets the GitHub repository [env: GITHUB_REPO=]
-    --gitlab-token <TOKEN>       Sets the GitLab API token [env: GITLAB_TOKEN]
-    --gitlab-repo <OWNER/REPO>   Sets the GitLab repository [env: GITLAB_REPO=]
+-i, --init [<CONFIG>]              Writes the default configuration file to cliff.toml
+-c, --config <PATH>                Sets the configuration file [env: GIT_CLIFF_CONFIG=] [default: cliff.toml]
+-w, --workdir <PATH>               Sets the working directory [env: GIT_CLIFF_WORKDIR=]
+-r, --repository <PATH>...         Sets the git repository [env: GIT_CLIFF_REPOSITORY=]
+    --include-path <PATTERN>...    Sets the path to include related commits [env: GIT_CLIFF_INCLUDE_PATH=]
+    --exclude-path <PATTERN>...    Sets the path to exclude related commits [env: GIT_CLIFF_EXCLUDE_PATH=]
+    --tag-pattern <PATTERN>        Sets the regex for matching git tags [env: GIT_CLIFF_TAG_PATTERN=]
+    --with-commit <MSG>...         Sets custom commit messages to include in the changelog [env: GIT_CLIFF_WITH_COMMIT=]
+    --with-tag-message [<MSG>]     Sets custom message for the latest release [env: GIT_CLIFF_WITH_TAG_MESSAGE=]
+    --ignore-tags <PATTERN>        Sets the tags to ignore in the changelog [env: GIT_CLIFF_IGNORE_TAGS=]
+    --skip-commit <SHA1>...        Sets commits that will be skipped in the changelog [env: GIT_CLIFF_SKIP_COMMIT=]
+-p, --prepend <PATH>               Prepends entries to the given changelog file [env: GIT_CLIFF_PREPEND=]
+-o, --output [<PATH>]              Writes output to the given file [env: GIT_CLIFF_OUTPUT=]
+-t, --tag <TAG>                    Sets the tag for the latest version [env: GIT_CLIFF_TAG=]
+-b, --body <TEMPLATE>              Sets the template for the changelog body [env: GIT_CLIFF_TEMPLATE=]
+-s, --strip <PART>                 Strips the given parts from the changelog [possible values: header, footer, all]
+    --sort <SORT>                  Sets sorting of the commits inside sections [default: oldest] [possible values: oldest, newest]
+    --github-token <TOKEN>         Sets the GitHub API token [env: GITHUB_TOKEN]
+    --github-repo <OWNER/REPO>     Sets the GitHub repository [env: GITHUB_REPO=]
+    --gitlab-token <TOKEN>         Sets the GitLab API token [env: GITLAB_TOKEN]
+    --gitlab-repo <OWNER/REPO>     Sets the GitLab repository [env: GITLAB_REPO=]
+    --gitea-token <TOKEN>          Sets the Gitea API token [env: GITEA_TOKEN]
+    --gitea-repo <OWNER/REPO>      Sets the GitLab repository [env: GITEA_REPO=]
+    --bitbucket-token <TOKEN>      Sets the Bitbucket API token [env: BITBUCKET_TOKEN]
+    --bitbucket-repo <OWNER/REPO>  Sets the Bitbucket repository [env: BITBUCKET_REPO=]
 ```
 
 ## Args
diff --git a/website/docs/usage/print-context.md b/website/docs/usage/print-context.md
index 8335550..fc314b9 100644
--- a/website/docs/usage/print-context.md
+++ b/website/docs/usage/print-context.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 9
+sidebar_position: 10
 ---
 
 # Print context


Test patch:
diff --git a/.github/fixtures/test-bump-unreleased-with-tag-message-arg/cliff.toml b/.github/fixtures/test-bump-unreleased-with-tag-message-arg/cliff.toml
new file mode 100644
index 00000000..9b43af3
--- /dev/null
+++ b/.github/fixtures/test-bump-unreleased-with-tag-message-arg/cliff.toml
@@ -0,0 +1,37 @@
+[changelog]
+# template for the changelog footer
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.\n
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}]
+    {% if message %}
+        {{ message }}
+    {% endif %}\
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\n
+"""
+# template for the changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+# remove the leading and trailing whitespace from the templates
+trim = true
+
+[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-bump-unreleased-with-tag-message-arg/commit.sh b/.github/fixtures/test-bump-unreleased-with-tag-message-arg/commit.sh
new file mode 100755
index 00000000..7c6fe32
--- /dev/null
+++ b/.github/fixtures/test-bump-unreleased-with-tag-message-arg/commit.sh
@@ -0,0 +1,11 @@
+#!/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 tag v0.1.0
+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.2.0
+GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit --allow-empty -m "test: add tests"
diff --git a/.github/fixtures/test-bump-unreleased-with-tag-message-arg/expected.md b/.github/fixtures/test-bump-unreleased-with-tag-message-arg/expected.md
new file mode 100644
index 00000000..bcb74bd
--- /dev/null
+++ b/.github/fixtures/test-bump-unreleased-with-tag-message-arg/expected.md
@@ -0,0 +1,13 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [0.2.1]
+
+Some text
+
+### Test
+
+- Add tests
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-tag-message/cliff.toml b/.github/fixtures/test-tag-message/cliff.toml
new file mode 100644
index 00000000..1f2be1c
--- /dev/null
+++ b/.github/fixtures/test-tag-message/cliff.toml
@@ -0,0 +1,37 @@
+[changelog]
+# template for the changelog footer
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.\n
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+    {% if message %}
+        {{ message }}
+    {% endif %}\
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\n
+"""
+# template for the changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+# remove the leading and trailing whitespace from the templates
+trim = true
+
+[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-tag-message/commit.sh b/.github/fixtures/test-tag-message/commit.sh
new file mode 100755
index 00000000..f2ec2b9
--- /dev/null
+++ b/.github/fixtures/test-tag-message/commit.sh
@@ -0,0 +1,11 @@
+#!/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 tag v0.1.0 -m "Some text"
+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.2.0
+GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit --allow-empty -m "test: add tests"
diff --git a/.github/fixtures/test-tag-message/expected.md b/.github/fixtures/test-tag-message/expected.md
new file mode 100644
index 00000000..afcaca4
--- /dev/null
+++ b/.github/fixtures/test-tag-message/expected.md
@@ -0,0 +1,33 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+### Test
+
+- Add tests
+
+## [0.2.0] - 2022-04-06
+
+### Bug Fixes
+
+- Fix feature 2
+
+### Features
+
+- Add feature 2
+
+## [0.1.0] - 2022-04-06
+
+Some text
+
+### Bug Fixes
+
+- Fix feature 1
+
+### Features
+
+- Add feature 1
+
+<!-- generated by git-cliff -->
diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml
index a338046..77e30ca 100644
--- a/.github/workflows/test-fixtures.yml
+++ b/.github/workflows/test-fixtures.yml
@@ -81,6 +81,9 @@ jobs:
             command: --bump --tag=2.1.1
           - fixtures-name: test-cli-arg-ignore-tags
             command: --ignore-tags ".*beta"
+          - fixtures-name: test-tag-message
+          - fixtures-name: test-bump-unreleased-with-tag-message-arg
+            command: --bump --unreleased --with-tag-message "Some text"
 
     steps:
       - name: Checkout
diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs
index 95fd991..3e960fa 100644
--- a/git-cliff-core/tests/integration_test.rs
+++ b/git-cliff-core/tests/integration_test.rs
@@ -149,6 +149,7 @@ fn generate_changelog() -> Result<()> {
 	let releases = vec![
 		Release {
 			version:   Some(String::from("v2.0.0")),
+			message: None,
 			commits:   vec![
 
 				Commit::new(
@@ -212,6 +213,7 @@ fn generate_changelog() -> Result<()> {
 		},
 		Release {
 			version:   Some(String::from("v1.0.0")),
+			message: None,
 			commits:   vec![
 				Commit::new(
 					String::from("0bc123"),