use crate::git::parsers::diff::DiffHunk;
use std::collections::HashSet;
const HEADER_PREFIX: &str = "diff --git a/";
pub fn file_header(path: &str) -> String {
format!("{HEADER_PREFIX}{0} b/{0}\n--- a/{0}\n+++ b/{0}\n", path)
}
pub fn build_hunk_patch(path: &str, hunk: &DiffHunk) -> String {
let mut buf = String::new();
buf.push_str(&file_header(path));
for line in &hunk.lines {
buf.push_str(&line.content);
buf.push('\n');
}
buf
}
pub fn build_lines_patch(path: &str, raw: &str, selected: &HashSet<usize>) -> Result<String, String> {
if selected.is_empty() {
return Err("No selected lines".into());
}
let mut records: Vec<(usize, String, Option<i32>, Option<i32>)> = Vec::new();
let mut old_lineno = 0;
let mut new_lineno = 0;
for (idx, line) in raw.lines().enumerate() {
if line.starts_with("@@") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
if let Some(old) = parts.get(1) {
let nums: Vec<&str> = old.trim_start_matches('-').split(',').collect();
old_lineno = nums.get(0).and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
}
if let Some(newn) = parts.get(2) {
let nums: Vec<&str> = newn.trim_start_matches('+').split(',').collect();
new_lineno = nums.get(0).and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
}
}
} else if line.starts_with('+') {
let ln = new_lineno;
new_lineno += 1;
records.push((idx, line.to_string(), None, Some(ln)));
} else if line.starts_with('-') {
let ln = old_lineno;
old_lineno += 1;
records.push((idx, line.to_string(), Some(ln), None));
} else if line.starts_with(' ') {
old_lineno += 1;
new_lineno += 1;
}
}
let mut selected_records: Vec<(usize, String, Option<i32>, Option<i32>)> = records
.into_iter()
.filter(|(idx, _, _, _)| selected.contains(idx))
.collect();
selected_records.sort_by_key(|(idx, _, _, _)| *idx);
if selected_records.is_empty() {
return Err("Selected lines are not diff lines".into());
}
let mut buf = String::new();
buf.push_str(&file_header(path));
let mut groups: Vec<Vec<(usize, String, Option<i32>, Option<i32>)>> = Vec::new();
for rec in selected_records {
let should_start_new_group = groups.is_empty() || {
groups.last()
.and_then(|g| g.last())
.map(|last_rec| rec.0 != last_rec.0 + 1)
.unwrap_or(true)
};
if should_start_new_group {
groups.push(vec![rec]);
} else if let Some(last) = groups.last_mut() {
last.push(rec);
}
}
for g in groups {
let old_start = g.iter().filter_map(|(_, _, o, _)| *o).min().unwrap_or(0);
let new_start = g.iter().filter_map(|(_, _, _, n)| *n).min().unwrap_or(0);
let old_count = g.iter().filter(|(_, _, o, _)| o.is_some()).count();
let new_count = g.iter().filter(|(_, _, _, n)| n.is_some()).count();
buf.push_str(&format!("@@ -{},{} +{},{} @@\n", old_start, old_count.max(1), new_start, new_count.max(1)));
for (_, line, _, _) in g {
buf.push_str(&line);
buf.push('\n');
}
}
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::parsers::diff::{DiffLine, DiffLineKind};
#[test]
fn build_lines_patch_single_group() {
let raw = "\
@@ -1,3 +1,3 @@
line1
-line2
+line2-changed
line3
";
let mut selected = HashSet::new();
selected.insert(3);
let patch = build_lines_patch("file.txt", raw, &selected).expect("patch");
assert!(patch.contains("diff --git a/file.txt b/file.txt"));
assert!(patch.contains("@@"));
assert!(patch.contains("+line2-changed"));
}
#[test]
fn build_lines_patch_multiple_groups() {
let raw = "\
@@ -1,6 +1,6 @@
line1
-line2
+line2-changed
line3
-line4
+line4-changed
line5
";
let mut selected = HashSet::new();
selected.insert(3); selected.insert(6); let patch = build_lines_patch("file.txt", raw, &selected).expect("patch");
assert!(patch.matches("@@").count() >= 2);
assert!(patch.contains("+line2-changed"));
assert!(patch.contains("+line4-changed"));
}
#[test]
fn build_lines_patch_rejects_non_diff_line() {
let raw = "\
@@ -1,3 +1,3 @@
line1
-line2
+line2-changed
line3
";
let mut selected = HashSet::new();
selected.insert(1);
let err = build_lines_patch("file.txt", raw, &selected).unwrap_err();
assert!(err.contains("Selected lines are not diff lines"));
}
#[test]
fn build_hunk_patch_contains_hunk_lines() {
let hunk = DiffHunk {
header: "@@ -1,1 +1,1 @@".to_string(),
old_start: 1,
old_lines: 1,
new_start: 1,
new_lines: 1,
lines: vec![
DiffLine {
kind: DiffLineKind::HunkHeader,
content: "@@ -1,1 +1,1 @@".to_string(),
old_lineno: None,
new_lineno: None,
},
DiffLine {
kind: DiffLineKind::Delete,
content: "-old".to_string(),
old_lineno: Some(1),
new_lineno: None,
},
DiffLine {
kind: DiffLineKind::Add,
content: "+new".to_string(),
old_lineno: None,
new_lineno: Some(1),
},
],
};
let patch = build_hunk_patch("file.txt", &hunk);
assert!(patch.contains("diff --git a/file.txt b/file.txt"));
assert!(patch.contains("@@ -1,1 +1,1 @@"));
assert!(patch.contains("-old"));
assert!(patch.contains("+new"));
}
}