1
1
mirror of https://github.com/orhun/git-cliff.git synced 2024-08-16 18:30:30 +03:00

feat: add tag message to release context

- add to the release context the tag's message if it exists
- add message to the default config
This commit is contained in:
Meitar 2024-06-18 00:16:17 +03:00
parent 4a3edcaf5d
commit b5b1d3cfc2
13 changed files with 257 additions and 26 deletions

View File

@ -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" },
]

11
.github/fixtures/test-tag-message/commit.sh vendored Executable file
View File

@ -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"

View File

@ -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 -->

View File

@ -79,6 +79,7 @@ jobs:
command: --bump --tag=2.1.1
- fixtures-name: test-cli-arg-ignore-tags
command: --ignore-tags ".*beta"
- fixtures-name: test-tag-message
steps:
- name: Checkout

View File

@ -16,6 +16,9 @@ All notable changes to this project will be documented in this file.\n
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% if message %}
{{ message }}
{% endif %}\
{% else %}\
## [unreleased]
{% endif %}\

View File

@ -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"),

View File

@ -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;

View File

@ -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.
@ -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 \

View File

@ -3,6 +3,7 @@ use crate::error::{
Error,
Result,
};
use crate::tag::Tag;
use git2::{
BranchType,
Commit,
@ -95,11 +96,47 @@ 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 don't exists, still returns Tag object with the given name
pub fn resolve_tag(&self, name: &str) -> Tag {
match self.inner.resolve_reference_from_short_name(name) {
Ok(reference) => match reference.peel_to_tag() {
Ok(tag) => Tag {
name: tag.name().unwrap_or_default().to_owned(),
message: tag.message().map(Self::cleanup_message),
},
_ => Tag {
name: name.to_owned(),
message: None,
},
},
_ => Tag {
name: name.to_owned(),
message: None,
},
}
}
/// used to remove signature from signed tag message
fn cleanup_message(message: &str) -> String {
let re = Regex::new(
r"(?s)-----BEGIN PGP SIGNATURE-----(.*?)-----END PGP SIGNATURE-----",
)
.expect("invalid regex, wtf");
re.replace(message, "").trim().to_string()
}
/// 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,21 @@ impl Repository {
{
let obj = self.inner.revparse_single(&name)?;
if let Ok(commit) = obj.clone().into_commit() {
tags.push((commit, name));
// lightweight commit?
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().expect("tag don't have name").to_owned(),
message: tag.message().map(|msg| msg.to_owned()),
}));
}
}
}
@ -261,7 +305,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 +314,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 +338,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 +363,35 @@ 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(())
}
}

46
git-cliff-core/src/tag.rs Normal file
View File

@ -0,0 +1,46 @@
/// Common tag object that is parsed from a repository.
/// lightweight tag will have None at 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\") }"
);
}
}

View File

@ -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"),

View File

@ -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"),

View File

@ -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()
@ -299,7 +305,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 +313,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())