use std::collections::HashMap;
use tracing::warn;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineType {
Added,
Removed,
Context,
Header,
Meta,
}
#[derive(Debug, Clone)]
pub struct DiffLineInfo {
pub line_content: String,
pub line_type: LineType,
pub new_line_number: Option<u32>,
pub diff_position: Option<u32>,
}
fn parse_hunk_header(line: &str) -> Option<u32> {
let plus_pos = line.find('+')?;
let after_plus = &line[plus_pos + 1..];
let end_pos = after_plus.find([',', ' ']).unwrap_or(after_plus.len());
let num_str = &after_plus[..end_pos];
num_str.parse().ok()
}
pub fn get_line_info(patch: &str, line_index: usize) -> Option<DiffLineInfo> {
let lines: Vec<&str> = patch.lines().collect();
if line_index >= lines.len() {
return None;
}
let mut new_line_number: Option<u32> = None;
let mut position_counter: Option<u32> = None;
for (i, line) in lines.iter().enumerate() {
let (line_type, content) = classify_line(line);
match line_type {
LineType::Meta => {
}
LineType::Header => {
new_line_number = parse_hunk_header(line);
position_counter = Some(position_counter.map_or(0, |p| p + 1));
}
LineType::Added | LineType::Context => {
position_counter = position_counter.map(|p| p + 1);
}
LineType::Removed => {
position_counter = position_counter.map(|p| p + 1);
}
}
if i == line_index {
let current_new_line = match line_type {
LineType::Removed | LineType::Header | LineType::Meta => None,
_ => new_line_number,
};
let current_position = match line_type {
LineType::Meta => None,
LineType::Header if position_counter == Some(0) => None,
_ => position_counter,
};
return Some(DiffLineInfo {
line_content: content.to_string(),
line_type,
new_line_number: current_new_line,
diff_position: current_position,
});
}
match line_type {
LineType::Added | LineType::Context => {
if let Some(n) = new_line_number {
new_line_number = Some(n + 1);
}
}
_ => {}
}
}
None
}
pub fn classify_line(line: &str) -> (LineType, &str) {
if line.starts_with("@@") {
(LineType::Header, line)
} else if line.starts_with("+++")
|| line.starts_with("---")
|| line.starts_with("diff ")
|| line.starts_with("index ")
{
(LineType::Meta, line)
} else if let Some(content) = line.strip_prefix('+') {
(LineType::Added, content)
} else if let Some(content) = line.strip_prefix('-') {
(LineType::Removed, content)
} else if let Some(content) = line.strip_prefix(' ') {
(LineType::Context, content)
} else {
(LineType::Context, line)
}
}
#[allow(dead_code)]
pub fn can_suggest_at_line(patch: &str, line_index: usize) -> bool {
get_line_info(patch, line_index)
.map(|info| matches!(info.line_type, LineType::Added | LineType::Context))
.unwrap_or(false)
}
pub fn validate_multiline_range(patch: &str, start: usize, end: usize) -> bool {
let lines: Vec<&str> = patch.lines().collect();
for idx in start..=end {
let Some(line) = lines.get(idx) else {
return false;
};
let (line_type, _) = classify_line(line);
match line_type {
LineType::Added | LineType::Context => {}
_ => return false,
}
}
true
}
pub fn line_number_to_position(patch: &str, target_line: u32) -> Option<u32> {
let mut new_line_number: Option<u32> = None;
let mut position_counter: Option<u32> = None;
for line in patch.lines() {
let (line_type, _) = classify_line(line);
match line_type {
LineType::Meta => continue,
LineType::Header => {
new_line_number = parse_hunk_header(line);
position_counter = Some(position_counter.map_or(0, |p| p + 1));
}
LineType::Added | LineType::Context => {
position_counter = position_counter.map(|p| p + 1);
if new_line_number == Some(target_line) {
return position_counter;
}
new_line_number = new_line_number.map(|n| n + 1);
}
LineType::Removed => {
position_counter = position_counter.map(|p| p + 1);
}
}
}
None
}
pub fn parse_unified_diff(unified_diff: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
let lines: Vec<&str> = unified_diff.lines().collect();
if lines.is_empty() {
return result;
}
let mut current_filename: Option<String> = None;
let mut current_patch_start: Option<usize> = None;
let mut pending_minus_filename: Option<String> = None;
for (i, line) in lines.iter().enumerate() {
if line.starts_with("diff --git ") {
if let (Some(filename), Some(start)) = (¤t_filename, current_patch_start) {
let patch = lines[start..i].join("\n");
if !patch.is_empty() {
result.insert(filename.clone(), patch);
}
}
current_filename = extract_filename(line);
current_patch_start = Some(i);
pending_minus_filename = None;
} else if current_filename.is_none() && current_patch_start.is_some() {
if let Some(rest) = line.strip_prefix("+++ ") {
if rest != "/dev/null" {
current_filename = strip_diff_prefix(rest);
} else if let Some(ref pending) = pending_minus_filename {
current_filename = Some(pending.clone());
pending_minus_filename = None;
}
} else if let Some(rest) = line.strip_prefix("--- ") {
if rest != "/dev/null" {
pending_minus_filename = strip_diff_prefix(rest);
}
}
}
}
if let (Some(filename), Some(start)) = (current_filename, current_patch_start) {
let patch = lines[start..].join("\n");
if !patch.is_empty() {
result.insert(filename, patch);
}
}
result
}
fn strip_diff_prefix(path: &str) -> Option<String> {
if path.len() >= 2 && path.as_bytes()[1] == b'/' {
Some(path[2..].to_string())
} else {
Some(path.to_string())
}
}
fn extract_filename(git_diff_line: &str) -> Option<String> {
let content = git_diff_line.strip_prefix("diff --git ")?;
if content.len() < 2 || content.as_bytes()[1] != b'/' {
warn!("Failed to parse git diff line: {}", git_diff_line);
return None;
}
let first_prefix = content.as_bytes()[0];
let first_path = &content[2..];
let total_len = first_path.len();
if total_len >= 3 && (total_len - 3) % 2 == 0 {
let path_len = (total_len - 3) / 2;
if path_len > 0 {
let bytes = first_path.as_bytes();
let sep = path_len;
if bytes[sep] == b' ' && bytes[sep + 2] == b'/' {
let path1 = &first_path[..path_len];
let path2 = &first_path[sep + 3..];
if path1 == path2 {
return Some(path2.to_string());
}
}
}
}
let second_prefix = match first_prefix {
b'a' => b'b',
b'c' | b'i' | b'o' => b'w',
_ => {
warn!(
"Failed to parse git diff line (unknown prefix): {}",
git_diff_line
);
return None;
}
};
let bytes = first_path.as_bytes();
let mut matches: Vec<usize> = Vec::new();
for i in 0..bytes.len().saturating_sub(2) {
if bytes[i] == b' ' && bytes[i + 1] == second_prefix && bytes[i + 2] == b'/' {
matches.push(i);
}
}
if matches.len() == 1 {
let path2 = &first_path[matches[0] + 3..];
if !path2.is_empty() {
return Some(path2.to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_snapshot;
use std::collections::BTreeMap;
fn format_parsed_diff(result: &HashMap<String, String>) -> String {
let sorted: BTreeMap<&str, &str> = result
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let mut output = String::new();
for (i, (filename, patch)) in sorted.iter().enumerate() {
if i > 0 {
output.push_str("\n---\n");
}
output.push_str(&format!("[{}]\n{}", filename, patch));
}
output
}
const SAMPLE_PATCH: &str = r#"@@ -1,4 +1,5 @@
line 1
-old line 2
+new line 2
+added line
line 3"#;
const UNIFIED_DIFF_SINGLE: &str = r#"diff --git a/src/main.rs b/src/main.rs
index 1234567..abcdefg 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
fn main() {
+ println!("Hello");
}
"#;
const UNIFIED_DIFF_MULTIPLE: &str = r#"diff --git a/src/lib.rs b/src/lib.rs
index 1111111..2222222 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,2 +1,3 @@
pub mod app;
+pub mod config;
diff --git a/src/app.rs b/src/app.rs
index 3333333..4444444 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -10,6 +10,7 @@
struct App {
name: String,
+ version: String,
}
"#;
const UNIFIED_DIFF_NEW_FILE: &str = r#"diff --git a/src/new_file.rs b/src/new_file.rs
new file mode 100644
index 0000000..1234567
--- /dev/null
+++ b/src/new_file.rs
@@ -0,0 +1,3 @@
+fn new_function() {
+ todo!()
+}
"#;
const UNIFIED_DIFF_DELETED: &str = r#"diff --git a/src/old_file.rs b/src/old_file.rs
deleted file mode 100644
index 1234567..0000000
--- a/src/old_file.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn old_function() {
- todo!()
-}
"#;
const UNIFIED_DIFF_RENAMED: &str = r#"diff --git a/src/old_name.rs b/src/new_name.rs
similarity index 95%
rename from src/old_name.rs
rename to src/new_name.rs
index 1234567..abcdefg 100644
--- a/src/old_name.rs
+++ b/src/new_name.rs
@@ -1,3 +1,3 @@
-fn old_name() {
+fn new_name() {
}
"#;
const UNIFIED_DIFF_BINARY: &str = r#"diff --git a/image.png b/image.png
new file mode 100644
index 0000000..1234567
Binary files /dev/null and b/image.png differ
"#;
#[test]
fn test_parse_hunk_header() {
assert_eq!(parse_hunk_header("@@ -1,4 +1,5 @@"), Some(1));
assert_eq!(parse_hunk_header("@@ -10,3 +15,7 @@"), Some(15));
assert_eq!(parse_hunk_header("@@ -1 +1 @@"), Some(1));
}
#[test]
fn test_get_line_info_header() {
let info = get_line_info(SAMPLE_PATCH, 0).unwrap();
assert_eq!(info.line_type, LineType::Header);
assert!(info.new_line_number.is_none());
}
#[test]
fn test_get_line_info_context() {
let info = get_line_info(SAMPLE_PATCH, 1).unwrap();
assert_eq!(info.line_type, LineType::Context);
assert_eq!(info.line_content, "line 1");
assert_eq!(info.new_line_number, Some(1));
}
#[test]
fn test_get_line_info_removed() {
let info = get_line_info(SAMPLE_PATCH, 2).unwrap();
assert_eq!(info.line_type, LineType::Removed);
assert_eq!(info.line_content, "old line 2");
assert!(info.new_line_number.is_none());
}
#[test]
fn test_get_line_info_added() {
let info = get_line_info(SAMPLE_PATCH, 3).unwrap();
assert_eq!(info.line_type, LineType::Added);
assert_eq!(info.line_content, "new line 2");
assert_eq!(info.new_line_number, Some(2));
}
#[test]
fn test_can_suggest_at_line() {
assert!(!can_suggest_at_line(SAMPLE_PATCH, 0));
assert!(can_suggest_at_line(SAMPLE_PATCH, 1));
assert!(!can_suggest_at_line(SAMPLE_PATCH, 2));
assert!(can_suggest_at_line(SAMPLE_PATCH, 3));
}
#[test]
fn test_classify_line_no_prefix() {
let (line_type, content) = classify_line("no prefix");
assert_eq!(line_type, LineType::Context);
assert_eq!(content, "no prefix");
}
#[test]
fn test_classify_line_empty() {
let (line_type, content) = classify_line("");
assert_eq!(line_type, LineType::Context);
assert_eq!(content, "");
}
#[test]
fn test_parse_hunk_header_no_comma_no_space() {
let patch = "@@ -1 +42\ntest";
let info = get_line_info(patch, 1).unwrap();
assert_eq!(info.line_type, LineType::Context);
assert_eq!(info.new_line_number, Some(42));
}
#[test]
fn test_out_of_bounds() {
assert!(get_line_info(SAMPLE_PATCH, 100).is_none());
}
#[test]
fn test_extract_filename() {
assert_eq!(
extract_filename("diff --git a/src/foo.rs b/src/foo.rs"),
Some("src/foo.rs".to_string())
);
assert_eq!(
extract_filename("diff --git a/main.rs b/main.rs"),
Some("main.rs".to_string())
);
assert_eq!(
extract_filename("diff --git a/deep/nested/path/file.rs b/deep/nested/path/file.rs"),
Some("deep/nested/path/file.rs".to_string())
);
}
#[test]
fn test_extract_filename_renamed() {
assert_eq!(
extract_filename("diff --git a/src/old_name.rs b/src/new_name.rs"),
Some("src/new_name.rs".to_string())
);
}
#[test]
fn test_extract_filename_mnemonic_prefix() {
assert_eq!(
extract_filename("diff --git c/src/foo.rs w/src/foo.rs"),
Some("src/foo.rs".to_string())
);
assert_eq!(
extract_filename("diff --git i/src/bar.rs w/src/bar.rs"),
Some("src/bar.rs".to_string())
);
assert_eq!(
extract_filename("diff --git c/src/old.rs w/src/new.rs"),
Some("src/new.rs".to_string())
);
}
#[test]
fn test_extract_filename_invalid() {
assert_eq!(extract_filename("not a diff line"), None);
assert_eq!(extract_filename("diff something else"), None);
}
#[test]
fn test_extract_filename_no_separator() {
assert_eq!(extract_filename("diff --git a/file nob"), None);
}
#[test]
fn test_extract_filename_spaces_with_subdir() {
assert_eq!(
extract_filename("diff --git a/my Folder/src/file.rs b/my Folder/src/file.rs"),
Some("my Folder/src/file.rs".to_string())
);
assert_eq!(
extract_filename("diff --git a/a b/c d/file.rs b/a b/c d/file.rs"),
Some("a b/c d/file.rs".to_string())
);
assert_eq!(
extract_filename(
"diff --git a/docs/my project/sub b/notes.md b/docs/my project/sub b/notes.md"
),
Some("docs/my project/sub b/notes.md".to_string())
);
}
#[test]
fn test_extract_filename_ambiguous_falls_back_to_none() {
assert_eq!(
extract_filename("diff --git a/x b/old.rs b/x b/new.rs"),
None
);
}
#[test]
fn test_parse_unified_diff_plusplus_fallback() {
let diff = "\
diff --git a/x b/old.rs b/x b/new.rs
index 1234567..abcdefg 100644
--- a/x b/old.rs
+++ b/x b/new.rs
@@ -1,3 +1,3 @@
line1
-old
+new";
let result = parse_unified_diff(diff);
assert!(
result.contains_key("x b/new.rs"),
"expected key 'x b/new.rs', got: {:?}",
result.keys().collect::<Vec<_>>()
);
}
#[test]
fn test_strip_diff_prefix() {
assert_eq!(
strip_diff_prefix("b/src/file.rs"),
Some("src/file.rs".to_string())
);
assert_eq!(strip_diff_prefix("w/file.rs"), Some("file.rs".to_string()));
assert_eq!(strip_diff_prefix("file.rs"), Some("file.rs".to_string()));
}
#[test]
fn test_parse_single_file() {
let result = parse_unified_diff(UNIFIED_DIFF_SINGLE);
assert_snapshot!(format_parsed_diff(&result), @r#"
[src/main.rs]
diff --git a/src/main.rs b/src/main.rs
index 1234567..abcdefg 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
fn main() {
+ println!("Hello");
}
"#);
}
#[test]
fn test_parse_multiple_files() {
let result = parse_unified_diff(UNIFIED_DIFF_MULTIPLE);
assert_snapshot!(format_parsed_diff(&result), @r#"
[src/app.rs]
diff --git a/src/app.rs b/src/app.rs
index 3333333..4444444 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -10,6 +10,7 @@
struct App {
name: String,
+ version: String,
}
---
[src/lib.rs]
diff --git a/src/lib.rs b/src/lib.rs
index 1111111..2222222 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,2 +1,3 @@
pub mod app;
+pub mod config;
"#);
}
#[test]
fn test_parse_new_file() {
let result = parse_unified_diff(UNIFIED_DIFF_NEW_FILE);
assert_snapshot!(format_parsed_diff(&result), @r#"
[src/new_file.rs]
diff --git a/src/new_file.rs b/src/new_file.rs
new file mode 100644
index 0000000..1234567
--- /dev/null
+++ b/src/new_file.rs
@@ -0,0 +1,3 @@
+fn new_function() {
+ todo!()
+}
"#);
}
#[test]
fn test_parse_deleted_file() {
let result = parse_unified_diff(UNIFIED_DIFF_DELETED);
assert_snapshot!(format_parsed_diff(&result), @r#"
[src/old_file.rs]
diff --git a/src/old_file.rs b/src/old_file.rs
deleted file mode 100644
index 1234567..0000000
--- a/src/old_file.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn old_function() {
- todo!()
-}
"#);
}
#[test]
fn test_parse_renamed_file() {
let result = parse_unified_diff(UNIFIED_DIFF_RENAMED);
assert_snapshot!(format_parsed_diff(&result), @r#"
[src/new_name.rs]
diff --git a/src/old_name.rs b/src/new_name.rs
similarity index 95%
rename from src/old_name.rs
rename to src/new_name.rs
index 1234567..abcdefg 100644
--- a/src/old_name.rs
+++ b/src/new_name.rs
@@ -1,3 +1,3 @@
-fn old_name() {
+fn new_name() {
}
"#);
}
#[test]
fn test_parse_binary_file() {
let result = parse_unified_diff(UNIFIED_DIFF_BINARY);
assert_snapshot!(format_parsed_diff(&result), @r#"
[image.png]
diff --git a/image.png b/image.png
new file mode 100644
index 0000000..1234567
Binary files /dev/null and b/image.png differ
"#);
}
#[test]
fn test_parse_empty_diff() {
let result = parse_unified_diff("");
assert!(result.is_empty());
}
#[test]
fn test_filename_matches_github_api_format() {
let result = parse_unified_diff(UNIFIED_DIFF_SINGLE);
let filename = result.keys().next().unwrap();
assert!(!filename.starts_with("a/"));
assert!(!filename.starts_with("b/"));
assert_eq!(filename, "src/main.rs");
}
#[test]
fn test_diff_position_single_hunk() {
let info = get_line_info(SAMPLE_PATCH, 0).unwrap();
assert_eq!(info.diff_position, None);
let info = get_line_info(SAMPLE_PATCH, 1).unwrap();
assert_eq!(info.diff_position, Some(1));
let info = get_line_info(SAMPLE_PATCH, 2).unwrap();
assert_eq!(info.diff_position, Some(2));
let info = get_line_info(SAMPLE_PATCH, 3).unwrap();
assert_eq!(info.diff_position, Some(3));
let info = get_line_info(SAMPLE_PATCH, 4).unwrap();
assert_eq!(info.diff_position, Some(4));
let info = get_line_info(SAMPLE_PATCH, 5).unwrap();
assert_eq!(info.diff_position, Some(5));
}
#[test]
fn test_diff_position_with_meta_lines() {
let patch = "diff --git a/foo.rs b/foo.rs\nindex 123..456 100644\n--- a/foo.rs\n+++ b/foo.rs\n@@ -1,2 +1,3 @@\n fn main() {\n+ println!(\"hello\");\n }";
let info = get_line_info(patch, 0).unwrap();
assert_eq!(info.line_type, LineType::Meta);
assert_eq!(info.diff_position, None);
let info = get_line_info(patch, 3).unwrap();
assert_eq!(info.line_type, LineType::Meta);
assert_eq!(info.diff_position, None);
let info = get_line_info(patch, 4).unwrap();
assert_eq!(info.line_type, LineType::Header);
assert_eq!(info.diff_position, None);
let info = get_line_info(patch, 5).unwrap();
assert_eq!(info.line_type, LineType::Context);
assert_eq!(info.diff_position, Some(1));
let info = get_line_info(patch, 6).unwrap();
assert_eq!(info.line_type, LineType::Added);
assert_eq!(info.diff_position, Some(2));
}
#[test]
fn test_diff_position_no_meta_lines() {
let patch = "@@ -1,2 +1,3 @@\n fn main() {\n+ println!(\"hello\");\n }";
let info = get_line_info(patch, 0).unwrap();
assert_eq!(info.diff_position, None);
let info = get_line_info(patch, 1).unwrap();
assert_eq!(info.diff_position, Some(1));
}
#[test]
fn test_diff_position_multi_hunk() {
let patch = "@@ -1,3 +1,3 @@\n-old1\n+new1\n ctx\n@@ -10,3 +10,3 @@\n-old2\n+new2\n ctx2";
let info = get_line_info(patch, 0).unwrap();
assert_eq!(info.diff_position, None);
let info = get_line_info(patch, 4).unwrap();
assert_eq!(info.line_type, LineType::Header);
assert_eq!(info.diff_position, Some(4));
let info = get_line_info(patch, 6).unwrap();
assert_eq!(info.diff_position, Some(6));
let info = get_line_info(patch, 7).unwrap();
assert_eq!(info.diff_position, Some(7));
}
#[test]
fn test_line_number_to_position_basic() {
assert_eq!(line_number_to_position(SAMPLE_PATCH, 1), Some(1));
assert_eq!(line_number_to_position(SAMPLE_PATCH, 2), Some(3));
assert_eq!(line_number_to_position(SAMPLE_PATCH, 3), Some(4));
assert_eq!(line_number_to_position(SAMPLE_PATCH, 4), Some(5));
}
#[test]
fn test_line_number_to_position_multi_hunk() {
let patch = "@@ -1,3 +1,3 @@\n-old1\n+new1\n ctx\n@@ -10,2 +10,2 @@\n-old2\n+new2";
assert_eq!(line_number_to_position(patch, 1), Some(2));
assert_eq!(line_number_to_position(patch, 2), Some(3));
assert_eq!(line_number_to_position(patch, 10), Some(6));
}
#[test]
fn test_line_number_to_position_with_meta_lines() {
let patch = "diff --git a/foo.rs b/foo.rs\nindex 123..456 100644\n--- a/foo.rs\n+++ b/foo.rs\n@@ -1,2 +1,3 @@\n fn main() {\n+ println!(\"hello\");\n }";
assert_eq!(line_number_to_position(patch, 1), Some(1));
assert_eq!(line_number_to_position(patch, 2), Some(2));
assert_eq!(line_number_to_position(patch, 3), Some(3));
}
#[test]
fn test_line_number_to_position_nonexistent_line() {
assert_eq!(line_number_to_position(SAMPLE_PATCH, 999), None);
assert_eq!(line_number_to_position(SAMPLE_PATCH, 0), None);
}
#[test]
fn test_validate_multiline_range_valid_single_hunk() {
let patch = "@@ -1,3 +1,4 @@\n context line\n+added line\n another context\n-removed line";
assert!(validate_multiline_range(patch, 1, 2));
assert!(validate_multiline_range(patch, 1, 1));
}
#[test]
fn test_validate_multiline_range_includes_removed_line() {
let patch = "@@ -1,3 +1,4 @@\n context line\n+added line\n another context\n-removed line";
assert!(!validate_multiline_range(patch, 1, 4));
}
#[test]
fn test_validate_multiline_range_crosses_hunk_boundary() {
let patch = "@@ -1,2 +1,2 @@\n line1\n+new line2\n@@ -10,2 +10,2 @@\n line10\n+new line11";
assert!(!validate_multiline_range(patch, 1, 4));
assert!(validate_multiline_range(patch, 1, 2));
assert!(validate_multiline_range(patch, 4, 5));
}
#[test]
fn test_validate_multiline_range_starts_at_header() {
let patch = "@@ -1,2 +1,2 @@\n line1\n+added";
assert!(!validate_multiline_range(patch, 0, 1));
}
#[test]
fn test_validate_multiline_range_out_of_bounds() {
let patch = "@@ -1,2 +1,2 @@\n line1";
assert!(!validate_multiline_range(patch, 1, 10));
}
#[test]
fn test_validate_multiline_range_removed_lines_in_middle() {
let patch = "@@ -1,5 +1,4 @@\n context1\n+added1\n-removed_mid\n context2\n+added2";
assert!(!validate_multiline_range(patch, 1, 4));
assert!(validate_multiline_range(patch, 1, 2));
assert!(validate_multiline_range(patch, 4, 5));
}
#[test]
fn test_validate_multiline_range_all_removed() {
let patch = "@@ -1,3 +0,0 @@\n-removed1\n-removed2\n-removed3";
assert!(!validate_multiline_range(patch, 1, 3));
}
#[test]
fn test_multiline_range_new_side_lines_contiguous() {
let patch = "@@ -1,4 +1,5 @@\n context1\n+added1\n+added2\n context2\n+added3";
assert!(validate_multiline_range(patch, 1, 4));
let start_info = get_line_info(patch, 1).unwrap();
let end_info = get_line_info(patch, 4).unwrap();
assert_eq!(start_info.new_line_number, Some(1));
assert_eq!(end_info.new_line_number, Some(4));
for idx in 1..=4 {
let info = get_line_info(patch, idx).unwrap();
assert!(info.new_line_number.is_some());
}
}
#[test]
fn test_single_line_vs_multiline_dispatch() {
let patch = "@@ -1,3 +1,4 @@\n context1\n+added1\n+added2\n context2";
let info = get_line_info(patch, 2).unwrap();
assert_eq!(info.new_line_number, Some(2));
let start_line = if info.new_line_number == info.new_line_number {
None
} else {
info.new_line_number
};
assert_eq!(start_line, None);
let start_info = get_line_info(patch, 1).unwrap();
let end_info = get_line_info(patch, 3).unwrap();
let start_ln = start_info.new_line_number.unwrap();
let end_ln = end_info.new_line_number.unwrap();
let start_line = if start_ln < end_ln {
Some(start_ln)
} else {
None
};
assert_eq!(start_line, Some(1));
assert_eq!(end_ln, 3);
}
#[test]
fn test_validate_multiline_range_meta_lines() {
let patch = "diff --git a/f.rs b/f.rs\nindex abc..def 100644\n--- a/f.rs\n+++ b/f.rs\n@@ -1,2 +1,3 @@\n context1\n+added1\n+added2";
assert!(!validate_multiline_range(patch, 0, 5));
assert!(validate_multiline_range(patch, 5, 7));
}
}