Initial commit: git-issue-commit CLI tool
Some checks failed
CI / test (push) Has been cancelled
CI / release (push) Has been cancelled

This commit is contained in:
2026-01-29 19:59:22 +00:00
parent 4ca47ade2b
commit 07fc22553f

View 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");
}
}