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:
parent
4a3edcaf5d
commit
b5b1d3cfc2
37
.github/fixtures/test-tag-message/cliff.toml
vendored
Normal file
37
.github/fixtures/test-tag-message/cliff.toml
vendored
Normal 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
11
.github/fixtures/test-tag-message/commit.sh
vendored
Executable 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"
|
33
.github/fixtures/test-tag-message/expected.md
vendored
Normal file
33
.github/fixtures/test-tag-message/expected.md
vendored
Normal 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 -->
|
1
.github/workflows/test-fixtures.yml
vendored
1
.github/workflows/test-fixtures.yml
vendored
@ -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
|
||||
|
@ -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 %}\
|
||||
|
@ -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"),
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 \
|
||||
|
@ -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
46
git-cliff-core/src/tag.rs
Normal 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\") }"
|
||||
);
|
||||
}
|
||||
}
|
@ -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"),
|
||||
|
@ -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"),
|
||||
|
@ -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())
|
||||
|
Loading…
Reference in New Issue
Block a user