use std::collections::HashMap; use crate::prompt::ReviewFinding; /// 一个补丁文件里可被评论的行号集合(左右两侧分开记录)。 #[derive(Debug, Default, Clone)] pub struct FileLines { /// 新增侧(+ 与上下文行)的行号。 pub right: Vec, /// 删除侧(- 行)的行号。 pub left: Vec, } /// 解析 unified diff,汇总每个文件的可评论行号。 /// /// 与前端 `splitDiffFiles`/`parseHunks` 的语义保持一致: /// 头部行(`+++`/`---`)不计入,`\ No newline at end of file` 不推进行号。 pub fn index_diff_lines(diff: &str) -> HashMap { let mut out: HashMap = HashMap::new(); let mut current: Option = None; let mut old_no: i32 = 0; let mut new_no: i32 = 0; for line in diff.split('\n') { if let Some(rest) = line.strip_prefix("diff --git ") { current = parse_diff_git_target(rest); if let Some(path) = current.clone() { out.entry(path).or_default(); } old_no = 0; new_no = 0; continue; } let Some(path) = current.clone() else { continue; }; if let Some(rest) = line.strip_prefix("+++ ") { let target = rest.trim(); if target != "/dev/null" { let path = strip_ab(target); out.entry(path.clone()).or_default(); current = Some(path); } continue; } if line.starts_with("--- ") { continue; } if line.starts_with("@@") { if let Some((old_start, new_start)) = parse_hunk_header(line) { old_no = old_start; new_no = new_start; } continue; } if line.starts_with("\\ No newline") { continue; } if line.starts_with('+') { out.entry(path.clone()).or_default().right.push(new_no); new_no += 1; } else if line.starts_with('-') { out.entry(path.clone()).or_default().left.push(old_no); old_no += 1; } else if line.starts_with(' ') || line.is_empty() { out.entry(path.clone()).or_default().right.push(new_no); old_no += 1; new_no += 1; } } out } fn parse_diff_git_target(rest: &str) -> Option { let trimmed = rest.trim(); let (_, b) = trimmed.split_once(" b/")?; Some(unquote(b.trim())) } fn strip_ab(value: &str) -> String { let unquoted = unquote(value); unquoted .strip_prefix("a/") .or_else(|| unquoted.strip_prefix("b/")) .unwrap_or(&unquoted) .to_string() } fn unquote(value: &str) -> String { let trimmed = value.trim(); if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') { return trimmed[1..trimmed.len() - 1].to_string(); } trimmed.to_string() } fn parse_hunk_header(line: &str) -> Option<(i32, i32)> { let body = line.strip_prefix("@@")?; let body = body.split("@@").next()?; let mut old_start = 0; let mut new_start = 0; for token in body.split_whitespace() { if let Some(rest) = token.strip_prefix('-') { old_start = rest.split(',').next()?.parse().ok()?; } else if let Some(rest) = token.strip_prefix('+') { new_start = rest.split(',').next()?.parse().ok()?; } } Some((old_start, new_start)) } /// 过滤掉落点不在本次变更行上的 findings,返回(有效 findings, 丢弃条数)。 pub fn validate_findings( diff: &str, findings: Vec, max_findings: usize, ) -> (Vec, usize) { let index = index_diff_lines(diff); let mut accepted = Vec::new(); let mut dropped = 0usize; for mut finding in findings { let path = finding.path.trim().trim_start_matches("./").to_string(); let side = match finding.side.as_deref() { Some("left") => "left", _ => "right", }; let Some(lines) = index.get(&path) else { dropped += 1; continue; }; let hit = if side == "left" { lines.left.contains(&finding.line) } else { lines.right.contains(&finding.line) }; if !hit { dropped += 1; continue; } finding.path = path; finding.side = Some(side.to_string()); if finding.severity.is_none() { finding.severity = Some("minor".to_string()); } accepted.push(finding); if accepted.len() >= max_findings { break; } } (accepted, dropped) }