aigitcommit/git/message.rs
1/*!
2 * Copyright (c) 2025 Hangzhou Guanwaii Technology Co., Ltd.
3 *
4 * This source code is licensed under the MIT License,
5 * which is located in the LICENSE file in the source tree's root directory.
6 *
7 * File: message.rs
8 * Author: mingcheng <mingcheng@apache.org>
9 * File Created: 2025-10-16 15:06:58
10 *
11 * Modified By: mingcheng <mingcheng@apache.org>
12 * Last Modified: 2025-10-17 18:22:55
13 */
14
15use crate::git::repository::Repository;
16use std::{error::Error, fmt::Display};
17use tracing::trace;
18
19/// Represents a structured Git commit message
20///
21/// A commit message consists of:
22/// - `title`: The first line (subject line), typically 50-72 characters
23/// - `content`: The body of the commit message with detailed description
24#[derive(Debug, serde::Serialize)]
25pub struct GitMessage {
26 pub title: String,
27 pub content: String,
28}
29
30impl Display for GitMessage {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 // Format as: title\n\ncontent
33 write!(f, "{}\n\n{}", self.title, self.content)
34 }
35}
36
37impl GitMessage {
38 /// Create a new Git commit message
39 ///
40 /// # Arguments
41 /// * `repository` - The Git repository (used to get author info for signoff)
42 /// * `title` - The commit title/subject line (will be trimmed)
43 /// * `content` - The commit body/description (will be trimmed)
44 /// * `signoff` - Whether to append a "Signed-off-by" line
45 ///
46 /// # Returns
47 /// * `Ok(GitMessage)` - A valid commit message
48 /// * `Err` - If title or content is empty after trimming
49 ///
50 pub fn new(
51 repository: &Repository,
52 title: &str,
53 content: &str,
54 signoff: bool,
55 ) -> Result<Self, Box<dyn Error>> {
56 // Trim inputs first to check actual content
57 let title_trimmed = title.trim();
58 let content_trimmed = content.trim();
59
60 // Validate both title and content are non-empty
61 if title_trimmed.is_empty() {
62 return Err("commit title cannot be empty".into());
63 }
64 if content_trimmed.is_empty() {
65 return Err("commit content cannot be empty".into());
66 }
67
68 let mut final_content = content_trimmed.to_string();
69
70 // Append signoff line if requested
71 if signoff {
72 trace!("adding Signed-off-by line to commit message");
73 let author = repository.get_author()?;
74
75 // Ensure proper spacing before signoff
76 final_content.push_str(&format!(
77 "\n\nSigned-off-by: {} <{}>",
78 author.name, author.email
79 ));
80 }
81
82 trace!("created commit message with title: {}", title_trimmed);
83 trace!("content length: {} characters", final_content.len());
84
85 Ok(Self {
86 title: title_trimmed.to_string(),
87 content: final_content,
88 })
89 }
90
91 /// Check if the commit message is empty
92 ///
93 /// Returns true only if both title and content are empty strings
94 pub fn is_empty(&self) -> bool {
95 self.title.is_empty() && self.content.is_empty()
96 }
97
98 /// Get the total character count of the commit message
99 pub fn char_count(&self) -> usize {
100 self.title.len() + 2 + self.content.len() // +2 for "\n\n"
101 }
102
103 /// Get the number of lines in the commit message
104 pub fn line_count(&self) -> usize {
105 1 + self.content.lines().count() // +1 for title, +blank line is implicit
106 }
107}