autocommit-rs 0.1.2

Tool that generates conventional commits from staged changes and executes the commit in one go.
use crate::types::CommitType;

pub fn clean_diff(diff: &str) -> String {
    diff.lines()
        .filter(|line| {
            !line.starts_with("diff --git")
                && !line.starts_with("index ")
                && !line.starts_with("--- ")
                && !line.starts_with("+++ ")
        })
        .collect::<Vec<_>>()
        .join("\n")
        .trim()
        .to_string()
}

pub fn generate_prompt(
    diff: &str,
    commit_type: Option<CommitType>,
    files: &[String],
    branch_name: &str,
) -> String {
    let type_constraint = match commit_type {
        Some(t) => format!("Use type: {}.", t.as_str()),
        None => "Determine the best conventional commit type.".to_string(),
    };

    let files_info = if !files.is_empty() {
        format!("\n\nChanged files: {}", files.join(", "))
    } else {
        String::new()
    };

    let mut ticket_instruction = String::new();
    if !branch_name.is_empty()
        && let Some(ticket) = extract_ticket_id(branch_name)
    {
        ticket_instruction = format!(
            "\n\nIMPORTANT: The branch name contains ticket ID {}. You MUST append [{}] to the end of the commit summary line.",
            ticket, ticket
        );
    }

    format!("{type_constraint}{files_info}{ticket_instruction}\n\nGit diff:\n{diff}")
}

fn extract_ticket_id(branch_name: &str) -> Option<String> {
    let chars: Vec<char> = branch_name.chars().collect();
    for i in 0..chars.len() {
        if chars[i].is_ascii_uppercase() {
            let mut j = i;
            while j < chars.len() && chars[j].is_ascii_uppercase() {
                j += 1;
            }
            if j < chars.len() && chars[j] == '-' {
                let start = j + 1;
                if start < chars.len() && chars[start].is_ascii_digit() {
                    let mut k = start;
                    while k < chars.len() && chars[k].is_ascii_digit() {
                        k += 1;
                    }
                    let ticket: String = chars[i..k].iter().collect();
                    return Some(ticket);
                }
            }
        }
    }
    None
}

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

    #[test]
    fn test_clean_diff_removes_metadata_lines() {
        let diff = "diff --git a/file.ts b/file.ts\nindex abc..def 100644\n--- a/file.ts\n+++ b/file.ts\n@@ -1,3 +1,4 @@\n hello\n+world";
        let cleaned = clean_diff(diff);
        assert!(!cleaned.contains("diff --git"));
        assert!(!cleaned.contains("index "));
        assert!(!cleaned.contains("--- "));
        assert!(!cleaned.contains("+++ "));
        assert!(cleaned.contains("+world"));
    }

    #[test]
    fn test_generate_prompt_with_type() {
        let prompt = generate_prompt("test diff", Some(CommitType::Feat), &[], "");
        assert!(prompt.contains("Use type: feat."));
        assert!(prompt.contains("Git diff:\ntest diff"));
    }

    #[test]
    fn test_generate_prompt_with_files() {
        let prompt = generate_prompt("diff", None, &["src/main.rs".to_string()], "");
        assert!(prompt.contains("Changed files: src/main.rs"));
    }

    #[test]
    fn test_generate_prompt_with_ticket() {
        let prompt = generate_prompt("diff", None, &[], "feature/PROJ-123-add-auth");
        assert!(prompt.contains("[PROJ-123]"));
    }
}