use derive_builder::Builder;
use git_conventional::Commit as ConventionalCommit;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use crate::{
analyzer::{
config::AnalyzerConfig,
group::{Group, GroupParser},
},
forge::request::ForgeCommit,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize, Builder)]
#[builder(setter(into, strip_option), default)]
pub struct Commit {
pub id: String,
pub short_id: String,
pub group: Group,
pub scope: Option<String>,
pub title: String,
pub body: Option<String>,
pub link: String,
pub breaking: bool,
pub breaking_description: Option<String>,
pub merge_commit: bool,
pub timestamp: i64,
pub author_name: String,
pub author_email: String,
pub raw_title: String,
pub raw_message: String,
}
impl Commit {
pub fn parse_forge_commit(
group_parser: &GroupParser,
forge_commit: &ForgeCommit,
config: &AnalyzerConfig,
) -> Option<Self> {
let author_name = forge_commit.author_name.clone();
let author_email = forge_commit.author_email.clone();
let commit_id = forge_commit.id.clone();
let short_id = forge_commit.short_id.clone();
let merge_commit = forge_commit.merge_commit;
let raw_message = forge_commit.message.clone();
let timestamp = forge_commit.timestamp;
let link = forge_commit.link.clone();
let split_msg = forge_commit.message.split_once("\n");
let (raw_title, raw_body) = match split_msg {
Some((t, b)) => {
let title_cow = trim_to_cow(t);
if b.is_empty() {
(title_cow.into_owned(), None)
} else {
let body_cow = trim_to_cow(b);
(title_cow.into_owned(), Some(body_cow.into_owned()))
}
}
None => (raw_message.clone(), None),
};
let parsed = ConventionalCommit::parse(raw_message.trim_end());
match parsed {
Ok(cc) => {
let mut commit = Self {
id: commit_id,
short_id,
scope: cc.scope().map(|s| s.to_string()),
title: cc.description().trim().to_string(),
body: cc.body().map(|b| b.trim().to_string()),
merge_commit,
breaking: cc.breaking(),
breaking_description: cc
.breaking_description()
.map(|d| d.to_string()),
raw_title,
raw_message,
group: Group::default(),
link,
timestamp,
author_name,
author_email,
};
commit.group = group_parser.parse(&commit);
let should_skip = match commit.group {
Group::Ci => config.skip_ci,
Group::Chore => config.skip_chore,
Group::Doc => config.skip_doc,
Group::Test => config.skip_test,
Group::Style => config.skip_style,
Group::Refactor => config.skip_refactor,
Group::Perf => config.skip_perf,
Group::Revert => config.skip_revert,
Group::Miscellaneous => config.skip_miscellaneous,
_ => false,
};
if should_skip {
log::debug!(
"omitting {} commit: {} : {}",
commit.group,
commit.short_id,
commit.raw_title
);
return None;
}
if commit.merge_commit && config.skip_merge_commits {
log::debug!(
"omitting merge commit: {} : {}",
commit.short_id,
commit.raw_title
);
return None;
}
if let Some(matcher) = config.release_commit_matcher.as_ref()
&& matcher.is_match(&commit.raw_title)
{
log::debug!(
"omitting release commit: {} : {}",
commit.short_id,
commit.raw_title
);
return None;
}
Some(commit)
}
Err(_) => {
if config.skip_miscellaneous {
return None;
}
if merge_commit && config.skip_merge_commits {
log::debug!(
"omitting merge commit: {short_id} : {raw_title}"
);
return None;
}
Some(Self {
id: commit_id,
short_id,
scope: None,
title: raw_title.clone(),
body: raw_body,
merge_commit,
breaking: false,
breaking_description: None,
raw_title,
raw_message,
group: Group::default(),
link,
timestamp,
author_name,
author_email,
})
}
}
}
}
fn trim_to_cow(s: &str) -> Cow<'_, str> {
let trimmed = s.trim();
if trimmed.len() == s.len() {
Cow::Borrowed(s)
} else {
Cow::Owned(trimmed.to_string())
}
}
#[cfg(test)]
mod tests {
use regex::Regex;
use crate::forge::request::ForgeCommitBuilder;
use super::*;
#[test]
fn test_parse_conventional_feat_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("abc123")
.message("feat: add new user authentication")
.author_name("John Doe")
.author_email("john@example.com")
.timestamp(1640995200)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.id, "abc123");
assert_eq!(commit.group, Group::Feat);
assert_eq!(commit.scope, None);
assert_eq!(commit.title, "add new user authentication");
assert_eq!(commit.body, None);
assert_eq!(commit.link, "");
assert!(!commit.breaking);
assert_eq!(commit.breaking_description, None);
assert!(!commit.merge_commit);
assert_eq!(commit.timestamp, 1640995200);
assert_eq!(commit.author_name, "John Doe");
assert_eq!(commit.author_email, "john@example.com");
assert_eq!(commit.raw_message, "feat: add new user authentication");
}
#[test]
fn test_parse_conventional_feat_commit_with_scope() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("def456")
.message("feat(auth): add OAuth2 support")
.author_name("Jane Smith")
.author_email("jane@example.com")
.timestamp(1640995300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.id, "def456");
assert_eq!(commit.group, Group::Feat);
assert_eq!(commit.scope, Some("auth".to_string()));
assert_eq!(commit.title, "add OAuth2 support");
assert_eq!(commit.body, None);
assert!(!commit.breaking);
assert_eq!(commit.author_name, "Jane Smith");
assert_eq!(commit.author_email, "jane@example.com");
}
#[test]
fn test_parse_conventional_fix_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("ghi789")
.message("fix: resolve null pointer exception")
.author_name("Bob Johnson")
.author_email("bob@example.com")
.timestamp(1640995400)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Fix);
assert_eq!(commit.title, "resolve null pointer exception");
assert!(!commit.breaking);
}
#[test]
fn test_parse_breaking_change_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("jkl012")
.message("feat!: redesign user API\n\nBREAKING CHANGE: The user API has been completely redesigned")
.author_name("Alice Brown")
.author_email("alice@example.com")
.timestamp(1640995500)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Breaking);
assert_eq!(commit.title, "redesign user API");
assert_eq!(commit.body, None);
assert!(commit.breaking);
assert_eq!(
commit.breaking_description,
Some("The user API has been completely redesigned".to_string())
);
}
#[test]
fn test_parse_commit_with_body() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let message = "feat: add user registration\n\nThis feature allows new users to register\nwith email verification.";
let forge_commit = ForgeCommitBuilder::default()
.id("mno345")
.message(message)
.author_name("Charlie Wilson")
.author_email("charlie@example.com")
.timestamp(1640995600)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Feat);
assert_eq!(commit.title, "add user registration");
assert_eq!(commit.body, Some("This feature allows new users to register\nwith email verification.".to_string()));
assert!(!commit.breaking);
}
#[test]
fn test_parse_non_conventional_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("pqr678")
.message("Update user authentication logic")
.author_name("David Lee")
.author_email("david@example.com")
.timestamp(1640995700)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Miscellaneous);
assert_eq!(commit.scope, None);
assert_eq!(commit.title, "Update user authentication logic");
assert_eq!(commit.body, None);
assert!(!commit.breaking);
assert_eq!(commit.breaking_description, None);
}
#[test]
fn test_parse_non_conventional_commit_with_body() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let message = "Update database schema\n\nAdded new indexes for better performance\nand updated user table structure.";
let forge_commit = ForgeCommitBuilder::default()
.id("stu901")
.message(message)
.author_name("Eva Martinez")
.author_email("eva@example.com")
.timestamp(1640995800)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Miscellaneous);
assert_eq!(commit.title, "Update database schema");
assert_eq!(commit.body, Some("Added new indexes for better performance\nand updated user table structure.".to_string()));
}
#[test]
fn test_parses_and_omits_merge_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("vwx234")
.message("Merge pull request #123 from feature/auth")
.author_name("GitHub")
.author_email("noreply@github.com")
.timestamp(1640995900)
.merge_commit(true)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(commit.is_none());
}
#[test]
fn test_parses_and_includes_merge_commit() {
let analyzer_config = AnalyzerConfig {
skip_merge_commits: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("vwx234")
.message("Merge pull request #123 from feature/auth")
.author_name("GitHub")
.author_email("noreply@github.com")
.timestamp(1640995900)
.merge_commit(true)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Miscellaneous);
assert!(commit.merge_commit);
assert_eq!(commit.title, "Merge pull request #123 from feature/auth");
assert_eq!(commit.author_name, "GitHub");
assert_eq!(commit.author_email, "noreply@github.com");
}
#[test]
fn test_parses_and_omits_release_commit() {
let analyzer_config = AnalyzerConfig {
skip_chore: false,
release_commit_matcher: Some(
Regex::new(r#"^chore\(main\):\srelease.+"#).unwrap(),
),
tag_prefix: Some("test-package-v".into()),
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("vwx234")
.message("chore(main): release test-package test-package-v1.0.0")
.author_name("GitHub")
.author_email("noreply@github.com")
.timestamp(1640995900)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(commit.is_none());
}
#[test]
fn test_parse_empty_message() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("yz567")
.message("")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000000)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Miscellaneous);
assert_eq!(commit.title, "");
assert_eq!(commit.body, None);
assert!(!commit.breaking);
}
#[test]
fn test_parse_chore_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("abc890")
.message("chore: update dependencies")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000100)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Chore);
assert_eq!(commit.title, "update dependencies");
}
#[test]
fn test_parse_ci_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("def123")
.message("ci: update GitHub Actions workflow")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000200)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Ci);
assert_eq!(commit.title, "update GitHub Actions workflow");
}
#[test]
fn test_parse_docs_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("ghi456")
.message("doc: update README with installation instructions")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Doc);
assert_eq!(
commit.title,
"update README with installation instructions"
);
}
#[test]
fn test_parse_test_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("jkl789")
.message("test: add unit tests for user service")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Test);
assert_eq!(commit.title, "add unit tests for user service");
}
#[test]
fn test_parse_refactor_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("mno012")
.message("refactor: simplify authentication logic")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Refactor);
assert_eq!(commit.title, "simplify authentication logic");
}
#[test]
fn test_parse_perf_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("pqr345")
.message("perf: optimize database queries")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Perf);
assert_eq!(commit.title, "optimize database queries");
}
#[test]
fn test_parse_style_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("stu678")
.message("style: format code with prettier")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Style);
assert_eq!(commit.title, "format code with prettier");
}
#[test]
fn test_parse_revert_commit() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("vwx901")
.message("revert: undo breaking changes")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Revert);
assert_eq!(commit.title, "undo breaking changes");
}
#[test]
fn test_parse_commit_with_trailing_whitespace() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("xyz234")
.message("feat: add new feature \n\n ")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Feat);
assert_eq!(commit.title, "add new feature");
assert_eq!(commit.body, None);
}
#[test]
fn test_parse_commit_with_empty_body() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("abc567")
.message("fix: resolve issue\n\n")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Fix);
assert_eq!(commit.title, "resolve issue");
assert_eq!(commit.body, None);
}
#[test]
fn test_parse_breaking_change_with_conventional_format() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let message = "feat(api)!: remove deprecated endpoints\n\nBREAKING CHANGE: The old v1 API endpoints have been removed".to_string();
let forge_commit = ForgeCommitBuilder::default()
.id("def890")
.message(message)
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Breaking);
assert_eq!(commit.scope, Some("api".to_string()));
assert_eq!(commit.title, "remove deprecated endpoints");
assert!(commit.breaking);
assert_eq!(
commit.breaking_description,
Some("The old v1 API endpoints have been removed".to_string())
);
}
#[test]
fn test_metadata_preservation() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("unique123")
.short_id("uni")
.link("https://custom-forge.com/commit/unique123")
.author_name("Custom Author")
.author_email("custom@forge.com")
.merge_commit(false)
.message("feat: custom forge commit")
.timestamp(9999999999_i64)
.files(vec![])
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.id, "unique123");
assert_eq!(commit.link, "https://custom-forge.com/commit/unique123");
assert_eq!(commit.author_name, "Custom Author");
assert_eq!(commit.author_email, "custom@forge.com");
assert!(!commit.merge_commit);
assert_eq!(commit.timestamp, 9999999999);
assert_eq!(commit.raw_message, "feat: custom forge commit");
}
#[test]
fn test_non_conventional_single_line() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("single123")
.message("Just a simple commit message")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Miscellaneous);
assert_eq!(commit.title, "Just a simple commit message");
assert_eq!(commit.body, None);
assert_eq!(commit.scope, None);
assert!(!commit.breaking);
assert_eq!(commit.breaking_description, None);
}
#[test]
fn test_breaking_takes_precedence_over_type() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("breaking123")
.message("feat!: feature change")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(commit.group, Group::Breaking);
assert!(commit.breaking);
assert_eq!(commit.title, "feature change");
}
#[test]
fn test_skip_ci_filters_ci_commits() {
let analyzer_config = AnalyzerConfig {
skip_ci: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("ci123")
.message("ci: update github actions workflow")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(result.is_none());
}
#[test]
fn test_skip_ci_false_includes_ci_commits() {
let analyzer_config = AnalyzerConfig {
skip_ci: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("ci123")
.message("ci: update github actions workflow")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.group, Group::Ci);
assert_eq!(commit.title, "update github actions workflow");
}
#[test]
fn test_skip_chore_filters_chore_commits() {
let analyzer_config = AnalyzerConfig {
skip_chore: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("chore123")
.message("chore: update dependencies")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(result.is_none());
}
#[test]
fn test_skip_chore_false_includes_chore_commits() {
let analyzer_config = AnalyzerConfig {
skip_chore: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("chore123")
.message("chore: update dependencies")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.group, Group::Chore);
assert_eq!(commit.title, "update dependencies");
}
#[test]
fn test_skip_miscellaneous_filters_non_conventional_commits() {
let analyzer_config = AnalyzerConfig {
skip_miscellaneous: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("misc123")
.message("random commit message without type")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(result.is_none());
}
#[test]
fn test_skip_miscellaneous_false_includes_non_conventional_commits() {
let analyzer_config = AnalyzerConfig {
skip_miscellaneous: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("misc123")
.message("random commit message without type")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.group, Group::Miscellaneous);
assert_eq!(commit.title, "random commit message without type");
}
#[test]
fn test_skip_docs_filters_docs_commits() {
let analyzer_config = AnalyzerConfig {
skip_doc: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("docs123")
.message("docs: update README")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(result.is_none());
}
#[test]
fn test_skip_docs_false_includes_docs_commits() {
let analyzer_config = AnalyzerConfig {
skip_doc: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("docs123")
.message("docs: update README")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.group, Group::Doc);
assert_eq!(commit.title, "update README");
}
#[test]
fn test_skip_test_filters_test_commits() {
let analyzer_config = AnalyzerConfig {
skip_test: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("test123")
.message("test: add unit tests for auth")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(result.is_none());
}
#[test]
fn test_skip_test_false_includes_test_commits() {
let analyzer_config = AnalyzerConfig {
skip_test: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("test123")
.message("test: add unit tests for auth")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.group, Group::Test);
assert_eq!(commit.title, "add unit tests for auth");
}
#[test]
fn test_skip_style_filters_style_commits() {
let analyzer_config = AnalyzerConfig {
skip_style: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("style123")
.message("style: format code with prettier")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(result.is_none());
}
#[test]
fn test_skip_style_false_includes_style_commits() {
let analyzer_config = AnalyzerConfig {
skip_style: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("style123")
.message("style: format code with prettier")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.group, Group::Style);
assert_eq!(commit.title, "format code with prettier");
}
#[test]
fn test_skip_refactor_filters_refactor_commits() {
let analyzer_config = AnalyzerConfig {
skip_refactor: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("refactor123")
.message("refactor: simplify authentication logic")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(result.is_none());
}
#[test]
fn test_skip_refactor_false_includes_refactor_commits() {
let analyzer_config = AnalyzerConfig {
skip_refactor: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("refactor123")
.message("refactor: simplify authentication logic")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.group, Group::Refactor);
assert_eq!(commit.title, "simplify authentication logic");
}
#[test]
fn test_skip_perf_filters_perf_commits() {
let analyzer_config = AnalyzerConfig {
skip_perf: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("perf123")
.message("perf: optimize database queries")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(result.is_none());
}
#[test]
fn test_skip_perf_false_includes_perf_commits() {
let analyzer_config = AnalyzerConfig {
skip_perf: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("perf123")
.message("perf: optimize database queries")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.group, Group::Perf);
assert_eq!(commit.title, "optimize database queries");
}
#[test]
fn test_skip_revert_filters_revert_commits() {
let analyzer_config = AnalyzerConfig {
skip_revert: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("revert123")
.message("revert: undo breaking changes")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
assert!(result.is_none());
}
#[test]
fn test_skip_revert_false_includes_revert_commits() {
let analyzer_config = AnalyzerConfig {
skip_revert: false,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let forge_commit = ForgeCommitBuilder::default()
.id("revert123")
.message("revert: undo breaking changes")
.author_name("Test User")
.author_email("test@example.com")
.timestamp(1641000300)
.merge_commit(false)
.build()
.unwrap();
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.group, Group::Revert);
assert_eq!(commit.title, "undo breaking changes");
}
#[test]
fn test_skip_options_do_not_affect_other_types() {
let analyzer_config = AnalyzerConfig {
skip_chore: true,
skip_ci: true,
skip_doc: true,
skip_merge_commits: true,
skip_miscellaneous: true,
skip_perf: true,
skip_refactor: true,
skip_revert: true,
skip_style: true,
skip_test: true,
..AnalyzerConfig::default()
};
let group_parser = GroupParser::default();
let test_cases = vec![
("feat: add feature", Group::Feat),
("fix: fix bug", Group::Fix),
("chore!: breaking chore change", Group::Breaking),
];
for (message, expected_group) in test_cases {
let forge_commit = ForgeCommit {
id: "commit123".into(),
message: message.into(),
author_name: "Test User".into(),
author_email: "test@example.com".into(),
timestamp: 1641000300,
merge_commit: false,
..ForgeCommit::default()
};
let commit = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
)
.unwrap();
assert_eq!(
commit.group, expected_group,
"Wrong group for message: {}",
message
);
}
}
#[test]
fn test_author_information_preserved() {
let analyzer_config = AnalyzerConfig::default();
let group_parser = GroupParser::default();
let forge_commit = ForgeCommit {
id: "author123".into(),
message: "feat: add feature".into(),
author_name: "John Doe".into(),
author_email: "john.doe@example.com".into(),
timestamp: 1641000000,
merge_commit: false,
..ForgeCommit::default()
};
let result = Commit::parse_forge_commit(
&group_parser,
&forge_commit,
&analyzer_config,
);
let commit = result.unwrap();
assert_eq!(commit.author_name, "John Doe");
assert_eq!(commit.author_email, "john.doe@example.com");
}
#[test]
fn test_trim_to_cow_no_whitespace() {
use std::borrow::Cow;
let input = "feat: add feature";
let result = trim_to_cow(input);
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result, "feat: add feature");
}
#[test]
fn test_trim_to_cow_with_whitespace() {
use std::borrow::Cow;
let input = " feat: add feature\n";
let result = trim_to_cow(input);
assert!(matches!(result, Cow::Owned(_)));
assert_eq!(result, "feat: add feature");
}
}