diff --git a/src/parser/issue.rs b/src/parser/issue.rs new file mode 100644 index 0000000..30a9ad6 --- /dev/null +++ b/src/parser/issue.rs @@ -0,0 +1,132 @@ +use crate::parser::conventional::ConventionalMessage; + +pub fn parse_issue_content(title: &str, body: &str) -> ConventionalMessage { + let description = extract_description(title, body); + let change_type = detect_change_type(&description, body); + + ConventionalMessage { + r#type: change_type, + scope: None, + description: clean_description(&description), + body: None, + breaking: is_breaking_change(body), + breaking_description: extract_breaking_description(body), + footer: None, + } +} + +fn extract_description(title: &str, body: &str) -> String { + if !title.is_empty() { + title.to_string() + } else if !body.is_empty() { + let first_line = body.lines() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .find(|s| !s.starts_with('#')) + .unwrap_or(""); + first_line.to_string() + } else { + String::new() + } +} + +fn detect_change_type(description: &str, body: &str) -> String { + let text = format!("{} {}", description, body).to_lowercase(); + + if text.contains("fix") || text.contains("bug") || text.contains("error") || text.contains("crash") { + return "fix".to_string(); + } + if text.contains("feature") || text.contains("add") || text.contains("implement") { + return "feat".to_string(); + } + if text.contains("document") || text.contains("readme") || text.contains("comment") { + return "docs".to_string(); + } + if text.contains("test") || text.contains("coverage") { + return "test".to_string(); + } + if text.contains("refactor") || text.contains("restructur") { + return "refactor".to_string(); + } + if text.contains("performance") || text.contains("optimize") || text.contains("speed") { + return "perf".to_string(); + } + if text.contains("style") || text.contains("format") || text.contains("lint") { + return "style".to_string(); + } + if text.contains("ci") || text.contains("workflow") || text.contains("pipeline") { + return "ci".to_string(); + } + if text.contains("build") || text.contains("dependenc") || text.contains("package") { + return "build".to_string(); + } + if text.contains("revert") || text.contains("undo") { + return "revert".to_string(); + } + + "chore".to_string() +} + +fn clean_description(desc: &str) -> String { + let cleaned = desc + .trim() + .trim_end_matches(|c: char| c == '.' || c == '!') + .to_string(); + + cleaned + .split_whitespace() + .take(20) + .collect::>() + .join(" ") +} + +fn is_breaking_change(body: &str) -> bool { + let body_lower = body.to_lowercase(); + body_lower.contains("breaking change") || + body_lower.contains("breaking changes") || + body_lower.contains("! ") || + body.contains("BREAKING CHANGE") +} + +fn extract_breaking_description(body: &str) -> Option { + let lines: Vec<&str> = body.lines().collect(); + let mut in_breaking_section = false; + let mut breaking_lines = Vec::new(); + + for line in &lines { + let line_lower = line.to_lowercase(); + if line_lower.contains("breaking change") && line_lower.contains("change") { + in_breaking_section = true; + let parts: Vec<&str> = line.splitn(2, "BREAKING CHANGE").collect(); + if parts.len() > 1 && !parts[1].trim().is_empty() && !parts[1].trim().starts_with(':') { + breaking_lines.push(parts[1].trim()); + } else { + let parts_lower: Vec<&str> = line.splitn(2, "BREAKING CHANGE").collect(); + if parts_lower.len() > 1 { + let after_break = parts_lower[1].trim().trim_start_matches(':').trim(); + if !after_break.is_empty() { + breaking_lines.push(after_break); + } + } + } + continue; + } + if in_breaking_section { + if line.trim().is_empty() { + if !breaking_lines.is_empty() { + break; + } + } else if line.starts_with('#') && !line_lower.contains("breaking") { + break; + } else { + breaking_lines.push(line.trim()); + } + } + } + + if breaking_lines.is_empty() { + None + } else { + Some(breaking_lines.join(" ").trim().to_string()) + } +}