use crate::git::repository::Repository;
use std::fmt::Write as _;
use std::{error::Error, fmt::Display};
use tracing::trace;
#[derive(Debug, serde::Serialize)]
pub struct GitMessage {
pub title: String,
pub content: String,
}
impl Display for GitMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}\n\n{}", self.title, self.content)
}
}
impl GitMessage {
pub fn new(
repository: &Repository,
title: &str,
content: &str,
signoff: bool,
) -> Result<Self, Box<dyn Error>> {
let title_trimmed = title.trim();
let content_trimmed = content.trim();
if title_trimmed.is_empty() {
return Err("commit title cannot be empty".into());
}
if content_trimmed.is_empty() {
return Err("commit content cannot be empty".into());
}
let mut final_content = content_trimmed.to_string();
if signoff {
trace!("adding Signed-off-by line to commit message");
let author = repository.get_author()?;
write!(
final_content,
"\n\nSigned-off-by: {} <{}>",
author.name, author.email
)?;
}
trace!("created commit message with title: {}", title_trimmed);
trace!("content length: {} characters", final_content.len());
Ok(Self {
title: title_trimmed.to_string(),
content: final_content,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
fn setup() -> Option<Repository> {
let path = env::var("TEST_REPO_PATH").unwrap_or_else(|_| ".".to_string());
Repository::new(&path).ok()
}
#[test]
fn rejects_empty_title() {
let Some(repo) = setup() else { return };
let err = GitMessage::new(&repo, " ", "body", false).unwrap_err();
assert!(err.to_string().contains("title"));
}
#[test]
fn rejects_empty_content() {
let Some(repo) = setup() else { return };
let err = GitMessage::new(&repo, "title", " ", false).unwrap_err();
assert!(err.to_string().contains("content"));
}
#[test]
fn trims_inputs_and_formats_display() {
let Some(repo) = setup() else { return };
let msg = GitMessage::new(&repo, " feat: x ", " body line ", false).unwrap();
assert_eq!(msg.title, "feat: x");
assert_eq!(msg.content, "body line");
assert_eq!(format!("{msg}"), "feat: x\n\nbody line");
}
#[test]
fn appends_signoff_line_when_requested() {
let Some(repo) = setup() else { return };
let msg = GitMessage::new(&repo, "feat: x", "body", true).unwrap();
assert!(
msg.content.contains("Signed-off-by:"),
"signoff line missing: {}",
msg.content
);
assert!(msg.content.contains("\n\nSigned-off-by:"));
}
}