|
|
|
@@ -0,0 +1,260 @@
|
|
|
|
|
use anyhow::Context;
|
|
|
|
|
use chrono::{DateTime, NaiveDate, Utc};
|
|
|
|
|
use git_cliff_core::{
|
|
|
|
|
changelog::Changelog,
|
|
|
|
|
commit::Commit,
|
|
|
|
|
config::{ChangelogConfig, CommitParser, Config, GitConfig},
|
|
|
|
|
release::Release,
|
|
|
|
|
};
|
|
|
|
|
use regex::Regex;
|
|
|
|
|
|
|
|
|
|
pub struct ChangeLogBuilder {
|
|
|
|
|
commits: Vec<String>,
|
|
|
|
|
version: String,
|
|
|
|
|
config: Option<Config>,
|
|
|
|
|
release_date: Option<NaiveDate>,
|
|
|
|
|
release_link: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChangeLogBuilder {
|
|
|
|
|
pub fn new<C>(commits: C, version: impl Into<String>) -> Self
|
|
|
|
|
where
|
|
|
|
|
C: IntoIterator,
|
|
|
|
|
C::Item: Into<String>,
|
|
|
|
|
{
|
|
|
|
|
Self {
|
|
|
|
|
commits: commits.into_iter().map(|s| s.into()).collect(),
|
|
|
|
|
version: version.into(),
|
|
|
|
|
config: None,
|
|
|
|
|
release_date: None,
|
|
|
|
|
release_link: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_release_date(self, release_date: NaiveDate) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
release_date: Some(release_date),
|
|
|
|
|
..self
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_release_link(self, release_link: impl Into<String>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
release_link: Some(release_link.into()),
|
|
|
|
|
..self
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_config(self, config: Config) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
config: Some(config),
|
|
|
|
|
..self
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn build<'a>(self) -> ChangeLog<'a> {
|
|
|
|
|
let git_config = self
|
|
|
|
|
.config
|
|
|
|
|
.clone()
|
|
|
|
|
.map(|c| c.git)
|
|
|
|
|
.unwrap_or_else(default_git_config);
|
|
|
|
|
let timestamp = self.release_timestamp();
|
|
|
|
|
let commits = self
|
|
|
|
|
.commits
|
|
|
|
|
.clone()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|c| Commit::new("id".into(), c))
|
|
|
|
|
.filter_map(|c| c.process(&git_config).ok())
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
ChangeLog {
|
|
|
|
|
release: Release {
|
|
|
|
|
version: Some(self.version),
|
|
|
|
|
commits,
|
|
|
|
|
commit_id: None,
|
|
|
|
|
timestamp,
|
|
|
|
|
previous: None,
|
|
|
|
|
},
|
|
|
|
|
config: self.config,
|
|
|
|
|
release_link: self.release_link,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn release_timestamp(&self) -> i64 {
|
|
|
|
|
self.release_date
|
|
|
|
|
.and_then(|date| date.and_hms_opt(0, 0, 0))
|
|
|
|
|
.map(|d| DateTime::<Utc>::from_utc(d, Utc))
|
|
|
|
|
.unwrap_or_else(Utc::now)
|
|
|
|
|
.timestamp()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct ChangeLog<'a> {
|
|
|
|
|
release: Release<'a>,
|
|
|
|
|
config: Option<Config>,
|
|
|
|
|
release_link: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChangeLog<'_> {
|
|
|
|
|
pub fn generate(&self) -> anyhow::Result<String> {
|
|
|
|
|
let config = self.config.clone().unwrap_or_else(|| self.default_config());
|
|
|
|
|
let changelog = Changelog::new(vec![self.release.clone()], &config)?;
|
|
|
|
|
let mut buffer = Vec::new();
|
|
|
|
|
changelog
|
|
|
|
|
.generate(&mut buffer)
|
|
|
|
|
.context("failed to generate changelog")?;
|
|
|
|
|
String::from_utf8(buffer)
|
|
|
|
|
.context("cannot convert bytes to string (contains non utf-8 char indices)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_config<'a>(&self) -> Config {
|
|
|
|
|
let config = Config {
|
|
|
|
|
changelog: default_changelog_config(
|
|
|
|
|
None,
|
|
|
|
|
self.release_link.as_ref().map(|rl| rl.as_str()),
|
|
|
|
|
),
|
|
|
|
|
git: default_git_config(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
config
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_git_config() -> GitConfig {
|
|
|
|
|
GitConfig {
|
|
|
|
|
conventional_commits: Some(true),
|
|
|
|
|
filter_unconventional: Some(false),
|
|
|
|
|
filter_commits: Some(true),
|
|
|
|
|
commit_parsers: Some(default_commit_parsers()),
|
|
|
|
|
..Default::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_commit_parsers() -> Vec<CommitParser> {
|
|
|
|
|
fn create_commit_parser(message: &str, group: &str) -> CommitParser {
|
|
|
|
|
CommitParser {
|
|
|
|
|
message: Regex::new(&format!("^{message}")).ok(),
|
|
|
|
|
body: None,
|
|
|
|
|
group: Some(group.into()),
|
|
|
|
|
default_scope: None,
|
|
|
|
|
scope: None,
|
|
|
|
|
skip: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vec![
|
|
|
|
|
create_commit_parser("feat", "added"),
|
|
|
|
|
create_commit_parser("changed", "changed"),
|
|
|
|
|
create_commit_parser("deprecated", "deprecated"),
|
|
|
|
|
create_commit_parser("removed", "removed"),
|
|
|
|
|
create_commit_parser("fix", "fixed"),
|
|
|
|
|
create_commit_parser("security", "security"),
|
|
|
|
|
CommitParser {
|
|
|
|
|
message: Regex::new(".*").ok(),
|
|
|
|
|
group: Some(String::from("other")),
|
|
|
|
|
body: None,
|
|
|
|
|
default_scope: None,
|
|
|
|
|
skip: None,
|
|
|
|
|
scope: None,
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CHANGELOG_HEADER: &'static str = r#"# Changelog
|
|
|
|
|
All notable changes to this project will be documented in this file.
|
|
|
|
|
|
|
|
|
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
|
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
|
|
|
|
|
|
|
## [Unreleased]
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
fn default_changelog_config(header: Option<String>, release_link: Option<&str>) -> ChangelogConfig {
|
|
|
|
|
ChangelogConfig {
|
|
|
|
|
header: Some(header.unwrap_or(String::from(CHANGELOG_HEADER))),
|
|
|
|
|
body: Some(default_changelog_body_config(release_link)),
|
|
|
|
|
footer: None,
|
|
|
|
|
trim: Some(true),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_changelog_body_config(release_link: Option<&str>) -> String {
|
|
|
|
|
const pre: &'static str = r#"
|
|
|
|
|
## [{{ version | trim_start_matches(pat="v") }}]"#;
|
|
|
|
|
const post: &'static str = r#" - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
|
|
|
{% for group, commits in commits | group_by(attribute="group") %}
|
|
|
|
|
### {{ group | upper_first }}
|
|
|
|
|
{% for commit in commits %}
|
|
|
|
|
{%- if commit.scope -%}
|
|
|
|
|
- *({{commit.scope}})* {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}{%- if commit.links %} ({% for link in commit.links %}[{{link.text}}]({{link.href}}) {% endfor -%}){% endif %}
|
|
|
|
|
{% else -%}
|
|
|
|
|
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}
|
|
|
|
|
{% endif -%}
|
|
|
|
|
{% endfor -%}
|
|
|
|
|
{% endfor %}"#;
|
|
|
|
|
|
|
|
|
|
match release_link {
|
|
|
|
|
Some(link) => format!("{}{}{}", pre, link, post),
|
|
|
|
|
None => format!("{}{}", pre, post),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn bare_release() {
|
|
|
|
|
let commits: Vec<&str> = Vec::new();
|
|
|
|
|
let changelog = ChangeLogBuilder::new(commits, "0.0.0")
|
|
|
|
|
.with_release_date(NaiveDate::from_ymd_opt(1995, 5, 15).unwrap())
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
let expected = r######"# Changelog
|
|
|
|
|
All notable changes to this project will be documented in this file.
|
|
|
|
|
|
|
|
|
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
|
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
|
|
|
|
|
|
|
## [Unreleased]
|
|
|
|
|
"######;
|
|
|
|
|
|
|
|
|
|
pretty_assertions::assert_eq!(expected, &changelog.generate().unwrap())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn generates_changelog() {
|
|
|
|
|
let commits: Vec<&str> = vec![
|
|
|
|
|
"feat: some feature",
|
|
|
|
|
"some random commit",
|
|
|
|
|
"fix: some fix",
|
|
|
|
|
"chore(scope): some chore",
|
|
|
|
|
];
|
|
|
|
|
let changelog = ChangeLogBuilder::new(commits, "1.0.0")
|
|
|
|
|
.with_release_date(NaiveDate::from_ymd_opt(1995, 5, 15).unwrap())
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
let expected = r######"# Changelog
|
|
|
|
|
All notable changes to this project will be documented in this file.
|
|
|
|
|
|
|
|
|
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
|
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
|
|
|
|
|
|
|
## [Unreleased]
|
|
|
|
|
|
|
|
|
|
## [1.0.0] - 1995-05-15
|
|
|
|
|
|
|
|
|
|
### Added
|
|
|
|
|
- some feature
|
|
|
|
|
|
|
|
|
|
### Fixed
|
|
|
|
|
- some fix
|
|
|
|
|
|
|
|
|
|
### Other
|
|
|
|
|
- some random commit
|
|
|
|
|
- *(scope)* some chore
|
|
|
|
|
"######;
|
|
|
|
|
|
|
|
|
|
pretty_assertions::assert_eq!(expected, &changelog.generate().unwrap())
|
|
|
|
|
}
|
|
|
|
|
}
|