Initial commit: git-issue-commit CLI tool
This commit is contained in:
244
src/generator/commit_message.rs
Normal file
244
src/generator/commit_message.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BreakingChange {
|
||||
pub description: String,
|
||||
pub meta: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for BreakingChange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref meta) = self.meta {
|
||||
write!(f, "{} ({})", self.description, meta)
|
||||
} else {
|
||||
write!(f, "{}", self.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CommitMessage {
|
||||
pub r#type: String,
|
||||
pub scope: Option<String>,
|
||||
pub description: String,
|
||||
pub body: Option<String>,
|
||||
pub breaking_changes: Vec<BreakingChange>,
|
||||
pub footer: Option<String>,
|
||||
}
|
||||
|
||||
impl CommitMessage {
|
||||
pub fn new(
|
||||
r#type: String,
|
||||
scope: Option<String>,
|
||||
description: String,
|
||||
body: Option<String>,
|
||||
) -> Self {
|
||||
CommitMessage {
|
||||
r#type,
|
||||
scope,
|
||||
description,
|
||||
body,
|
||||
breaking_changes: Vec::new(),
|
||||
footer: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
!self.r#type.trim().is_empty() && !self.description.trim().is_empty()
|
||||
}
|
||||
|
||||
pub fn format_header(&self) -> String {
|
||||
let scope_part = self.scope.as_ref().map(|s| format!("({})", s)).unwrap_or_default();
|
||||
format!("{}{}: {}", self.r#type, scope_part, self.description)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CommitMessage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.format_header())?;
|
||||
|
||||
if let Some(ref body) = self.body {
|
||||
if !body.trim().is_empty() {
|
||||
writeln!(f)?;
|
||||
writeln!(f, "{}", body.trim())?;
|
||||
}
|
||||
}
|
||||
|
||||
for breaking in &self.breaking_changes {
|
||||
writeln!(f)?;
|
||||
write!(f, "BREAKING CHANGE: {}", breaking)?;
|
||||
}
|
||||
|
||||
if let Some(ref footer) = self.footer {
|
||||
if !footer.trim().is_empty() {
|
||||
writeln!(f)?;
|
||||
writeln!(f, "{}", footer.trim())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ConventionalCommitBuilder {
|
||||
commit: CommitMessage,
|
||||
}
|
||||
|
||||
impl ConventionalCommitBuilder {
|
||||
pub fn new() -> Self {
|
||||
ConventionalCommitBuilder {
|
||||
commit: CommitMessage::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_type(mut self, r#type: String) -> Self {
|
||||
self.commit.r#type = r#type;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_scope(mut self, scope: Option<String>) -> Self {
|
||||
self.commit.scope = scope;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: String) -> Self {
|
||||
self.commit.description = description;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_body(mut self, body: Option<String>) -> Self {
|
||||
self.commit.body = body;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_breaking_change(mut self, description: String, meta: Option<String>) -> Self {
|
||||
self.commit.breaking_changes.push(BreakingChange {
|
||||
description,
|
||||
meta,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_footer(mut self, footer: String) -> Self {
|
||||
self.commit.footer = Some(footer);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> CommitMessage {
|
||||
self.commit
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_commit_message_format() {
|
||||
let msg = CommitMessage::new(
|
||||
"feat".to_string(),
|
||||
Some("auth".to_string()),
|
||||
"add login functionality".to_string(),
|
||||
Some("Users can now log in using OAuth".to_string()),
|
||||
);
|
||||
|
||||
let formatted = msg.to_string();
|
||||
assert!(formatted.contains("feat(auth): add login functionality"));
|
||||
assert!(formatted.contains("Users can now log in using OAuth"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit_message_with_breaking_change() {
|
||||
let mut msg = CommitMessage::new(
|
||||
"feat".to_string(),
|
||||
Some("api".to_string()),
|
||||
"update API response format".to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
msg.breaking_changes.push(BreakingChange {
|
||||
description: "API response format changed".to_string(),
|
||||
meta: Some("BREAKING".to_string()),
|
||||
});
|
||||
|
||||
let formatted = msg.to_string();
|
||||
assert!(formatted.contains("BREAKING CHANGE:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit_message_validation() {
|
||||
let valid = CommitMessage::new(
|
||||
"feat".to_string(),
|
||||
None,
|
||||
"add feature".to_string(),
|
||||
None,
|
||||
);
|
||||
assert!(valid.is_valid());
|
||||
|
||||
let invalid_type = CommitMessage::new(
|
||||
String::new(),
|
||||
None,
|
||||
"add feature".to_string(),
|
||||
None,
|
||||
);
|
||||
assert!(!invalid_type.is_valid());
|
||||
|
||||
let invalid_desc = CommitMessage::new(
|
||||
"feat".to_string(),
|
||||
None,
|
||||
String::new(),
|
||||
None,
|
||||
);
|
||||
assert!(!invalid_desc.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conventional_commit_builder() {
|
||||
let msg = ConventionalCommitBuilder::new()
|
||||
.with_type("fix".to_string())
|
||||
.with_scope(Some("bug".to_string()))
|
||||
.with_description("fix a bug".to_string())
|
||||
.with_body(Some("this fixes the bug".to_string()))
|
||||
.build();
|
||||
|
||||
assert_eq!(msg.r#type, "fix");
|
||||
assert_eq!(msg.scope, Some("bug".to_string()));
|
||||
assert_eq!(msg.description, "fix a bug");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit_message_header_format() {
|
||||
let msg = CommitMessage::new(
|
||||
"feat".to_string(),
|
||||
Some("core".to_string()),
|
||||
"implement new algorithm".to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(msg.format_header(), "feat(core): implement new algorithm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_breaking_change_display() {
|
||||
let breaking = BreakingChange {
|
||||
description: "API changed".to_string(),
|
||||
meta: Some("v2.0".to_string()),
|
||||
};
|
||||
|
||||
let display = breaking.to_string();
|
||||
assert!(display.contains("API changed"));
|
||||
assert!(display.contains("v2.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_breaking_change_without_meta() {
|
||||
let breaking = BreakingChange {
|
||||
description: "Breaking change without meta".to_string(),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let display = breaking.to_string();
|
||||
assert_eq!(display, "Breaking change without meta");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user