commitfmt 0.0.1

A git commit message formatter
Documentation
#[derive(Debug, Clone, PartialEq)]
pub struct CommitMessage {
    pub subject: String,
    pub body_lines: Vec<String>,
    pub comments: Vec<String>,
}

impl CommitMessage {
    pub fn parse(input: &str) -> Self {
        let all_lines: Vec<&str> = input.lines().collect();

        // Find where comments/diff section starts
        let comment_start = all_lines.iter().position(|line| line.starts_with('#'));

        let (message_lines, comment_lines) = if let Some(idx) = comment_start {
            (all_lines[..idx].to_vec(), all_lines[idx..].to_vec())
        } else {
            (all_lines, vec![])
        };

        let comments: Vec<String> = comment_lines.iter().map(|s| s.to_string()).collect();

        if message_lines.is_empty() {
            return CommitMessage {
                subject: String::new(),
                body_lines: Vec::new(),
                comments,
            };
        }

        let subject = message_lines[0].to_string();
        let mut body_lines = Vec::new();
        let mut found_body_start = false;

        for line in message_lines.iter().skip(1) {
            if !found_body_start {
                if line.trim().is_empty() {
                    continue;
                }
                found_body_start = true;
            }

            body_lines.push(line.to_string());
        }

        CommitMessage {
            subject,
            body_lines,
            comments,
        }
    }

    pub fn to_string(&self) -> String {
        let mut result = self.subject.clone();

        if !self.body_lines.is_empty() {
            result.push_str("\n\n");
            result.push_str(&self.body_lines.join("\n"));
        }

        if !self.comments.is_empty() {
            if !result.is_empty() && !result.ends_with('\n') {
                result.push('\n');
            }
            result.push('\n');
            result.push_str(&self.comments.join("\n"));
        }

        if !result.ends_with('\n') {
            result.push('\n');
        }

        result
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_simple_message() {
        let input = "Fix bug in parser";
        let msg = CommitMessage::parse(input);
        assert_eq!(msg.subject, "Fix bug in parser");
        assert_eq!(msg.body_lines.len(), 0);
        assert_eq!(msg.comments.len(), 0);
    }

    #[test]
    fn test_parse_message_with_body() {
        let input = "Fix bug in parser\n\nThis fixes the issue with parsing.";
        let msg = CommitMessage::parse(input);
        assert_eq!(msg.subject, "Fix bug in parser");
        assert_eq!(msg.body_lines, vec!["This fixes the issue with parsing."]);
    }

    #[test]
    fn test_parse_message_with_comments() {
        let input = "Fix bug\n\nDetails\n# Please enter the commit message";
        let msg = CommitMessage::parse(input);
        assert_eq!(msg.subject, "Fix bug");
        assert_eq!(msg.body_lines, vec!["Details"]);
        assert_eq!(msg.comments, vec!["# Please enter the commit message"]);
    }

    #[test]
    fn test_to_string_simple() {
        let msg = CommitMessage {
            subject: "Fix bug".to_string(),
            body_lines: Vec::new(),
            comments: Vec::new(),
        };
        assert_eq!(msg.to_string(), "Fix bug\n");
    }

    #[test]
    fn test_to_string_with_body() {
        let msg = CommitMessage {
            subject: "Fix bug".to_string(),
            body_lines: vec!["Details here".to_string()],
            comments: Vec::new(),
        };
        assert_eq!(msg.to_string(), "Fix bug\n\nDetails here\n");
    }

    #[test]
    fn test_to_string_with_comments() {
        let msg = CommitMessage {
            subject: "Fix bug".to_string(),
            body_lines: vec!["Details".to_string()],
            comments: vec!["# Comment".to_string()],
        };
        assert_eq!(msg.to_string(), "Fix bug\n\nDetails\n\n# Comment\n");
    }
}