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 0000000000..9b43af3e3e --- /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 = """ + +""" +# 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 0000000000..7c6fe32718 --- /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 0000000000..bcb74bdacd --- /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 + + diff --git a/.github/fixtures/test-tag-message/cliff.toml b/.github/fixtures/test-tag-message/cliff.toml new file mode 100644 index 0000000000..1f2be1c0eb --- /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 = """ + +""" +# 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 0000000000..f2ec2b9ca0 --- /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 0000000000..afcaca4dd9 --- /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 + + diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml index 0afbefd0d9..3a495253e4 100644 --- a/.github/workflows/test-fixtures.yml +++ b/.github/workflows/test-fixtures.yml @@ -79,6 +79,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/src/changelog.rs b/git-cliff-core/src/changelog.rs index 870f633c63..5680d49f76 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 6f78c91ea4..812e0e777d 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 4d3f9308f1..3449f2eac4 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, + /// git tag's message. + pub message: Option, /// Commits made for the release. pub commits: Vec>, /// Commit ID of the tag. @@ -159,6 +161,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())) @@ -340,6 +343,7 @@ mod test { let mut release = Release { version: None, + message: None, commits: vec![ Commit::from(String::from( "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \ @@ -625,6 +629,7 @@ mod test { let mut release = Release { version: None, + message: None, commits: vec![ Commit::from(String::from( "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \ @@ -968,6 +973,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 8723a640e7..50645c883d 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 = 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 { + pub fn current_tag(&self) -> Option { 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, topo_order: bool, - ) -> Result> { - let mut tags: Vec<(Commit, String)> = Vec::new(); + ) -> Result> { + 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 0000000000..7fe8ab9cb3 --- /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, +} + +#[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 d5dbf93012..3fe0e8a3a9 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-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs index 95fd991ab6..3e960fac69 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"), diff --git a/git-cliff/src/args.rs b/git-cliff/src/args.rs index 1fcd1c3b3c..4c7698567d 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, + pub help: Option, #[arg( short = 'V', long, @@ -73,10 +73,10 @@ pub struct Opt { help = "Prints version information", help_heading = "FLAGS" )] - pub version: Option, + pub version: Option, /// 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>, + pub init: Option>, /// 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, + pub workdir: Option, /// Sets the git repository. #[arg( short, @@ -114,7 +114,7 @@ pub struct Opt { num_args(1..), value_parser = Opt::parse_dir )] - pub repository: Option>, + pub repository: Option>, /// 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>, + pub include_path: Option>, /// 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>, + pub exclude_path: Option>, /// Sets the regex for matching git tags. #[arg(long, env = "GIT_CLIFF_TAG_PATTERN", value_name = "PATTERN")] - pub tag_pattern: Option, + pub tag_pattern: Option, /// 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>, + pub with_commit: Option>, + /// 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, /// Sets the tags to ignore in the changelog. #[arg(long, env = "GIT_CLIFF_IGNORE_TAGS", value_name = "PATTERN")] - pub ignore_tags: Option, + pub ignore_tags: Option, /// 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>, + pub skip_commit: Option>, /// 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, + pub prepend: Option, /// 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, + pub output: Option, /// 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, + pub tag: Option, /// 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, + pub body: Option, /// 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, + pub strip: Option, /// 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, + pub range: Option, /// 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, + pub github_token: Option, /// 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, + pub github_repo: Option, /// 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, + pub gitlab_token: Option, /// 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, + pub gitlab_repo: Option, /// 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, + pub gitea_token: Option, /// 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, + pub gitea_repo: Option, /// 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, + pub bitbucket_token: Option, /// 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, + pub bitbucket_repo: Option, } /// Custom type for the remote value. diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs index a58c5d5695..6fc8e8c458 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 2afebdf4f5..9d61db8015 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 0000000000..7e4f5692d2 --- /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 5219301042..efd9b3ee4b 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 [] Writes the default configuration file to cliff.toml --c, --config Sets the configuration file [env: GIT_CLIFF_CONFIG=] [default: cliff.toml] --w, --workdir Sets the working directory [env: GIT_CLIFF_WORKDIR=] --r, --repository ... Sets the git repository [env: GIT_CLIFF_REPOSITORY=] - --include-path ... Sets the path to include related commits [env: GIT_CLIFF_INCLUDE_PATH=] - --exclude-path ... Sets the path to exclude related commits [env: GIT_CLIFF_EXCLUDE_PATH=] - --tag-pattern Sets the regex for matching git tags [env: GIT_CLIFF_TAG_PATTERN=] - --with-commit ... Sets custom commit messages to include in the changelog [env: GIT_CLIFF_WITH_COMMIT=] - --ignore-tags Sets the tags to ignore in the changelog [env: GIT_CLIFF_IGNORE_TAGS=] - --skip-commit ... Sets commits that will be skipped in the changelog [env: GIT_CLIFF_SKIP_COMMIT=] --p, --prepend Prepends entries to the given changelog file [env: GIT_CLIFF_PREPEND=] --o, --output [] Writes output to the given file [env: GIT_CLIFF_OUTPUT=] --t, --tag Sets the tag for the latest version [env: GIT_CLIFF_TAG=] --b, --body