diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs index ec2fc34052..a339281f2d 100644 --- a/git-cliff-core/src/changelog.rs +++ b/git-cliff-core/src/changelog.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; use std::io::{Read, Write}; use std::time::{SystemTime, UNIX_EPOCH}; +use log::{debug, trace}; + use crate::commit::Commit; -use crate::config::{Config, GitConfig}; +use crate::config::{Config, GitConfig, ProcessingStep}; use crate::error::{Error, Result}; use crate::release::{Release, Releases}; #[cfg(feature = "bitbucket")] @@ -83,26 +85,6 @@ impl<'a> Changelog<'a> { Ok(()) } - /// Processes a single commit and returns/logs the result. - fn process_commit(commit: &Commit<'a>, git_config: &GitConfig) -> Option> { - match commit.process(git_config) { - Ok(commit) => Some(commit), - Err(e) => { - let short_id = commit.id.chars().take(7).collect::(); - let summary = commit.message.lines().next().unwrap_or_default().trim(); - match &e { - Error::ParseError(_) | Error::FieldError(_) => { - log::warn!("{short_id} - {e} ({summary})"); - } - _ => { - log::trace!("{short_id} - {e} ({summary})"); - } - } - None - } - } - } - /// Checks the commits and returns an error if any unconventional commits /// are found. fn check_conventional_commits(commits: &Vec>) -> Result<()> { @@ -131,31 +113,141 @@ impl<'a> Changelog<'a> { Ok(()) } - fn process_commit_list(commits: &mut Vec>, git_config: &GitConfig) -> Result<()> { - *commits = commits + /// Splits the commits by their message lines. + /// Returns a new vector of commits with each line as a separate commit. + fn apply_split_commits(commits: &mut Vec>) -> Vec> { + let mut split_commits = Vec::new(); + for commit in commits { + commit.message.lines().for_each(|line| { + let mut c = commit.clone(); + c.message = line.to_string(); + c.links.clear(); + if !c.message.is_empty() { + split_commits.push(c) + }; + }) + } + split_commits + } + + /// Applies the commit parsers to the commits and returns the parsed + /// commits. + fn apply_commit_parsers( + commits: &mut Vec>, + git_config: &GitConfig, + ) -> Vec> { + commits .iter() - .filter_map(|commit| Self::process_commit(commit, git_config)) - .flat_map(|commit| { - if git_config.split_commits { - commit - .message - .lines() - .filter_map(|line| { - let mut c = commit.clone(); - c.message = line.to_string(); - c.links.clear(); - if c.message.is_empty() { - None - } else { - Self::process_commit(&c, git_config) - } - }) - .collect() - } else { - vec![commit] + .filter_map(|commit| { + match commit.clone().parse( + &git_config.commit_parsers, + git_config.protect_breaking_commits, + git_config.filter_commits, + ) { + Ok(commit) => Some(commit), + Err(e) => { + Self::on_step_err(commit.clone(), e); + None + } + } + }) + .collect::>() + } + + /// Applies the commit preprocessors to the commits and returns the + /// preprocessed commits. + fn apply_commit_preprocessors( + commits: &mut Vec>, + git_config: &GitConfig, + ) -> Vec> { + commits + .iter() + .filter_map(|commit| { + // Apply commit parsers + match commit.clone().preprocess(&git_config.commit_preprocessors) { + Ok(commit) => Some(commit), + Err(e) => { + Self::on_step_err(commit.clone(), e); + None + } + } + }) + .collect::>() + } + + /// Converts the commits into conventional format if the configuration + /// requires it. + fn apply_into_conventional( + commits: &mut Vec>, + git_config: &GitConfig, + ) -> Vec> { + commits + .iter() + .filter_map(|commit| { + let mut commit_into_conventional = Ok(commit.clone()); + if git_config.conventional_commits { + if !git_config.require_conventional && + git_config.filter_unconventional && + !git_config.split_commits + { + commit_into_conventional = commit.clone().into_conventional(); + } else if let Ok(conv_commit) = commit.clone().into_conventional() { + commit_into_conventional = Ok(conv_commit); + }; + }; + match commit_into_conventional { + Ok(commit) => Some(commit), + Err(e) => { + Self::on_step_err(commit.clone(), e); + None + } } }) - .collect::>(); + .collect::>() + } + + /// Applies the link parsers to the commits and returns the parsed commits. + fn apply_link_parsers( + commits: &mut Vec>, + git_config: &GitConfig, + ) -> Vec> { + commits + .iter() + .map(|commit| commit.clone().parse_links(&git_config.link_parsers)) + .collect::>() + } + + /// Processes the commit list based on the processing order defined in the + /// configuration. + fn process_commit_list(commits: &mut Vec>, git_config: &GitConfig) -> Result<()> { + for step in &git_config.processing_order.order { + match step { + ProcessingStep::CommitParsers => { + debug!("Applying commit parsers..."); + *commits = Self::apply_commit_parsers(commits, git_config); + } + ProcessingStep::CommitPreprocessors => { + debug!("Applying commit preprocessors..."); + *commits = Self::apply_commit_preprocessors(commits, git_config); + } + ProcessingStep::IntoConventional => { + debug!("Converting commits to conventional format..."); + *commits = Self::apply_into_conventional(commits, git_config); + } + ProcessingStep::LinkParsers => { + debug!("Applying link parsers..."); + *commits = Self::apply_link_parsers(commits, git_config); + } + ProcessingStep::SplitCommits => { + debug!("Splitting commits..."); + if git_config.split_commits { + *commits = Self::apply_split_commits(commits); + } else { + debug!("Split commits is disabled, skipping..."); + } + } + } + } if git_config.require_conventional { Self::check_conventional_commits(commits)?; @@ -164,6 +256,16 @@ impl<'a> Changelog<'a> { Ok(()) } + /// Logs the error of a failed step from a single commit. + fn on_step_err(commit: Commit<'a>, error: Error) { + trace!( + "{} - {} ({})", + commit.id.chars().take(7).collect::(), + error, + commit.message.lines().next().unwrap_or_default().trim() + ); + } + /// Processes the commits and omits the ones that doesn't match the /// criteria set by configuration file. fn process_commits(&mut self) -> Result<()> { @@ -687,6 +789,7 @@ mod test { output: None, }, git: GitConfig { + processing_order: Default::default(), conventional_commits: true, require_conventional: false, filter_unconventional: false, @@ -1367,7 +1470,7 @@ style: make awesome stuff look better releases[2].commits.push(Commit { id: String::from("123abc"), message: String::from( - "chore(deps): bump some deps + "merge(deps): bump some deps chore(deps): bump some more deps chore(deps): fix broken deps @@ -1401,7 +1504,6 @@ chore(deps): fix broken deps - document zyx #### deps - - bump some deps - bump some more deps - fix broken deps @@ -1414,9 +1516,9 @@ chore(deps): fix broken deps ### Commit Statistics - - 8 commit(s) contributed to the release. + - 7 commit(s) contributed to the release. - 6 day(s) passed between the first and last commit. - - 8 commit(s) parsed as conventional. + - 7 commit(s) parsed as conventional. - 1 linked issue(s) detected in commits. - [#5](https://github.com/5) (referenced 1 time(s)) - -578 day(s) passed between releases. @@ -1463,6 +1565,8 @@ chore(deps): fix broken deps #### other - support unconventional commits - this commit is preprocessed + - use footer + - footer text - make awesome stuff look better #### ui @@ -1470,9 +1574,9 @@ chore(deps): fix broken deps ### Commit Statistics - - 18 commit(s) contributed to the release. - - 12 day(s) passed between the first and last commit. - - 17 commit(s) parsed as conventional. + - 20 commit(s) contributed to the release. + - 13 day(s) passed between the first and last commit. + - 19 commit(s) parsed as conventional. - 1 linked issue(s) detected in commits. - [#3](https://github.com/3) (referenced 1 time(s)) -- total releases: 2 -- diff --git a/git-cliff-core/src/config.rs b/git-cliff-core/src/config.rs index 3831b35d41..fbfa357a7e 100644 --- a/git-cliff-core/src/config.rs +++ b/git-cliff-core/src/config.rs @@ -82,9 +82,54 @@ pub struct ChangelogConfig { pub output: Option, } +/// Processing steps for the commits +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProcessingStep { + /// Split commits on newlines, treating each line as an individual commit. + SplitCommits, + /// An array of regex based parsers to modify commit messages prior to + /// further processing. + CommitPreprocessors, + /// Try to parse commits according to the conventional commits + /// specification. + IntoConventional, + /// An array of regex based parsers for extracting data from the commit + /// message. + CommitParsers, + /// An array of regex based parsers to extract links from the commit message + /// and add them to the commit's context. + LinkParsers, +} + +/// Processing order for the changelog. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessingOrder { + /// The order in which the changelog should be processed. + pub order: Vec, +} + +impl Default for ProcessingOrder { + /// Returns the default processing order. + fn default() -> Self { + Self { + order: vec![ + ProcessingStep::CommitPreprocessors, + ProcessingStep::SplitCommits, + ProcessingStep::IntoConventional, + ProcessingStep::CommitParsers, + ProcessingStep::LinkParsers, + ], + } + } +} + /// Git configuration #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct GitConfig { + /// Defines the processing order of the commits. + #[serde(default)] + pub processing_order: ProcessingOrder, /// Parse commits according to the conventional commits specification. pub conventional_commits: bool, /// Require all commits to be conventional. diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs index 1788efa7b6..f64875a7d5 100644 --- a/git-cliff-core/tests/integration_test.rs +++ b/git-cliff-core/tests/integration_test.rs @@ -39,6 +39,7 @@ fn generate_changelog() -> Result<()> { output: None, }; let git_config = GitConfig { + processing_order: Default::default(), conventional_commits: true, require_conventional: false, filter_unconventional: true,