use std::path::PathBuf;
use crate::error::{Result, TuicrError};
use crate::model::{DiffFile, DiffHunk, DiffLine, FileStatus, LineOrigin};
use crate::syntax::SyntaxHighlighter;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffFormat {
Hg,
GitStyle,
}
pub fn parse_unified_diff(
diff_text: &str,
format: DiffFormat,
highlighter: &SyntaxHighlighter,
) -> Result<Vec<DiffFile>> {
let mut files: Vec<DiffFile> = Vec::new();
let mut lines = diff_text.lines().peekable();
let header_prefix = match format {
DiffFormat::Hg => "diff ",
DiffFormat::GitStyle => "diff --git ",
};
while let Some(line) = lines.next() {
if line.starts_with(header_prefix) {
let (old_path, new_path, status) = parse_file_header(&mut lines, format);
if lines.peek().is_some_and(|l| l.contains("Binary")) {
lines.next(); files.push(DiffFile {
old_path,
new_path,
status,
hunks: Vec::new(),
is_binary: true,
});
continue;
}
let file_path = new_path.as_ref().or(old_path.as_ref());
let mut hunks = Vec::new();
while lines.peek().is_some() {
if let Some(peek_line) = lines.peek() {
if peek_line.starts_with("diff ") {
break;
} else if peek_line.starts_with("@@") {
if let Some(hunk) = parse_hunk(&mut lines, file_path, highlighter) {
hunks.push(hunk);
}
} else {
lines.next(); }
}
}
files.push(DiffFile {
old_path,
new_path,
status,
hunks,
is_binary: false,
});
}
}
if files.is_empty() {
return Err(TuicrError::NoChanges);
}
Ok(files)
}
fn parse_file_header<'a, I>(
lines: &mut std::iter::Peekable<I>,
format: DiffFormat,
) -> (Option<PathBuf>, Option<PathBuf>, FileStatus)
where
I: Iterator<Item = &'a str>,
{
let mut old_path: Option<PathBuf> = None;
let mut new_path: Option<PathBuf> = None;
let mut status = FileStatus::Modified;
while let Some(line) = lines.peek() {
if line.starts_with("---") {
let path_str = line.trim_start_matches("--- ").trim_start_matches("a/");
if path_str != "/dev/null" {
let path = if format == DiffFormat::Hg {
path_str.split('\t').next().unwrap_or(path_str)
} else {
path_str
};
old_path = Some(PathBuf::from(path));
}
lines.next();
} else if line.starts_with("+++") {
let path_str = line.trim_start_matches("+++ ").trim_start_matches("b/");
if path_str != "/dev/null" {
let path = if format == DiffFormat::Hg {
path_str.split('\t').next().unwrap_or(path_str)
} else {
path_str
};
new_path = Some(PathBuf::from(path));
}
lines.next();
break; } else if line.starts_with("new file") {
status = FileStatus::Added;
lines.next();
} else if line.starts_with("deleted file") {
status = FileStatus::Deleted;
lines.next();
} else if let Some(path) = line.strip_prefix("rename from ") {
status = FileStatus::Renamed;
old_path = Some(PathBuf::from(path));
lines.next();
} else if let Some(path) = line.strip_prefix("rename to ") {
new_path = Some(PathBuf::from(path));
lines.next();
} else if let Some(path) = line.strip_prefix("copy from ") {
status = FileStatus::Copied;
old_path = Some(PathBuf::from(path));
lines.next();
} else if let Some(path) = line.strip_prefix("copy to ") {
new_path = Some(PathBuf::from(path));
lines.next();
} else if line.starts_with("@@") || line.starts_with("diff ") {
break;
} else if line.starts_with("Binary file") {
if let Some((old, new)) = parse_binary_file_line(line) {
if old_path.is_none() {
old_path = old;
}
if new_path.is_none() {
new_path = new;
}
}
break;
} else {
lines.next(); }
}
if status == FileStatus::Modified {
if old_path.is_none() && new_path.is_some() {
status = FileStatus::Added;
} else if old_path.is_some() && new_path.is_none() {
status = FileStatus::Deleted;
}
}
(old_path, new_path, status)
}
fn parse_hunk<'a, I>(
lines: &mut std::iter::Peekable<I>,
file_path: Option<&PathBuf>,
highlighter: &SyntaxHighlighter,
) -> Option<DiffHunk>
where
I: Iterator<Item = &'a str>,
{
let header_line = lines.next()?;
let (old_start, old_count, new_start, new_count) = parse_hunk_header(header_line)?;
let mut line_contents: Vec<String> = Vec::new();
let mut line_origins: Vec<LineOrigin> = Vec::new();
let mut line_numbers: Vec<(Option<u32>, Option<u32>)> = Vec::new();
let mut old_lineno = old_start;
let mut new_lineno = new_start;
while let Some(line) = lines.peek() {
if line.starts_with("@@") || line.starts_with("diff ") {
break;
}
let line = lines.next().unwrap();
if line.starts_with('\\') {
continue;
}
let (origin, content, old_ln, new_ln) = if let Some(stripped) = line.strip_prefix('+') {
if line.starts_with("+++") {
continue;
}
let ln = new_lineno;
new_lineno += 1;
(LineOrigin::Addition, stripped, None, Some(ln))
} else if let Some(stripped) = line.strip_prefix('-') {
if line.starts_with("---") {
continue;
}
let ln = old_lineno;
old_lineno += 1;
(LineOrigin::Deletion, stripped, Some(ln), None)
} else if let Some(stripped) = line.strip_prefix(' ') {
let old_ln = old_lineno;
let new_ln = new_lineno;
old_lineno += 1;
new_lineno += 1;
(LineOrigin::Context, stripped, Some(old_ln), Some(new_ln))
} else if line.is_empty() {
let old_ln = old_lineno;
let new_ln = new_lineno;
old_lineno += 1;
new_lineno += 1;
(LineOrigin::Context, "", Some(old_ln), Some(new_ln))
} else {
continue;
};
line_contents.push(content.to_string());
line_origins.push(origin);
line_numbers.push((old_ln, new_ln));
}
let highlight_sequences =
SyntaxHighlighter::split_diff_lines_for_highlighting(&line_contents, &line_origins);
let (old_highlighted_lines, new_highlighted_lines) = if let Some(path) = file_path {
(
highlighter.highlight_file_lines(path, &highlight_sequences.old_lines),
highlighter.highlight_file_lines(path, &highlight_sequences.new_lines),
)
} else {
(None, None)
};
let mut diff_lines: Vec<DiffLine> = Vec::with_capacity(line_contents.len());
for (idx, content) in line_contents.into_iter().enumerate() {
let origin = line_origins[idx];
let (old_lineno, new_lineno) = line_numbers[idx];
let highlighted_spans = highlighter.highlighted_line_for_diff_with_background(
old_highlighted_lines.as_deref(),
new_highlighted_lines.as_deref(),
highlight_sequences.old_line_indices[idx],
highlight_sequences.new_line_indices[idx],
origin,
);
diff_lines.push(DiffLine {
origin,
content,
old_lineno,
new_lineno,
highlighted_spans,
});
}
Some(DiffHunk {
header: header_line.to_string(),
lines: diff_lines,
old_start,
old_count,
new_start,
new_count,
})
}
fn parse_hunk_header(line: &str) -> Option<(u32, u32, u32, u32)> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 || parts[0] != "@@" {
return None;
}
let old_part = parts[1].trim_start_matches('-');
let new_part = parts[2].trim_start_matches('+');
let (old_start, old_count) = parse_range(old_part);
let (new_start, new_count) = parse_range(new_part);
Some((old_start, old_count, new_start, new_count))
}
fn parse_range(s: &str) -> (u32, u32) {
if let Some((start, count)) = s.split_once(',') {
(start.parse().unwrap_or(1), count.parse().unwrap_or(1))
} else {
(s.parse().unwrap_or(1), 1)
}
}
fn parse_binary_file_line(line: &str) -> Option<(Option<PathBuf>, Option<PathBuf>)> {
if let Some(content) = line.strip_prefix("Binary files ") {
let content = content.strip_suffix(" differ")?;
let (old_part, new_part) = content.split_once(" and ")?;
let old_path = if old_part == "/dev/null" {
None
} else {
Some(PathBuf::from(
old_part.strip_prefix("a/").unwrap_or(old_part),
))
};
let new_path = if new_part == "/dev/null" {
None
} else {
Some(PathBuf::from(
new_part.strip_prefix("b/").unwrap_or(new_part),
))
};
return Some((old_path, new_path));
}
if let Some(content) = line.strip_prefix("Binary file ") {
let path = content.strip_suffix(" has changed")?;
let path = PathBuf::from(path);
return Some((Some(path.clone()), Some(path)));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_return_no_changes_for_empty_diff() {
assert!(matches!(
parse_unified_diff("", DiffFormat::Hg, &SyntaxHighlighter::default()),
Err(TuicrError::NoChanges)
));
assert!(matches!(
parse_unified_diff("", DiffFormat::GitStyle, &SyntaxHighlighter::default()),
Err(TuicrError::NoChanges)
));
}
#[test]
fn should_parse_hunk_header() {
let result = parse_hunk_header("@@ -1,3 +1,4 @@");
assert_eq!(result, Some((1, 3, 1, 4)));
let result = parse_hunk_header("@@ -10,5 +20,8 @@ context");
assert_eq!(result, Some((10, 5, 20, 8)));
}
#[test]
fn should_parse_hunk_header_without_count() {
let (old_start, old_count, new_start, new_count) =
parse_hunk_header("@@ -5 +10 @@").unwrap();
assert_eq!(old_start, 5);
assert_eq!(old_count, 1);
assert_eq!(new_start, 10);
assert_eq!(new_count, 1);
}
#[test]
fn should_reject_invalid_hunk_header() {
assert!(parse_hunk_header("not a hunk header").is_none());
assert!(parse_hunk_header("@@ invalid").is_none());
}
#[test]
fn should_parse_range_with_comma() {
assert_eq!(parse_range("10,5"), (10, 5));
assert_eq!(parse_range("1,100"), (1, 100));
}
#[test]
fn should_parse_range_without_comma() {
assert_eq!(parse_range("42"), (42, 1));
assert_eq!(parse_range("1"), (1, 1));
}
#[test]
fn should_handle_invalid_range() {
assert_eq!(parse_range("abc"), (1, 1));
assert_eq!(parse_range("abc,def"), (1, 1));
}
#[test]
fn hg_should_parse_simple_diff() {
let diff = r#"diff -r abc123 test.rs
--- a/test.rs Thu Jan 01 00:00:00 1970 +0000
+++ b/test.rs Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +1,4 @@
fn main() {
+ println!("hello");
println!("world");
}
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].status, FileStatus::Modified);
assert_eq!(result[0].hunks.len(), 1);
assert_eq!(result[0].hunks[0].lines.len(), 4);
}
#[test]
fn hg_should_parse_new_file() {
let diff = r#"diff -r 000000000000 new_file.rs
--- /dev/null
+++ b/new_file.rs
@@ -0,0 +1,2 @@
+fn new() {
+}
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].status, FileStatus::Added);
assert!(result[0].old_path.is_none());
assert_eq!(
result[0].new_path.as_ref().unwrap().to_str().unwrap(),
"new_file.rs"
);
}
#[test]
fn hg_should_parse_deleted_file() {
let diff = r#"diff -r abc123 old_file.rs
--- a/old_file.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-fn old() {
-}
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].status, FileStatus::Deleted);
assert_eq!(
result[0].old_path.as_ref().unwrap().to_str().unwrap(),
"old_file.rs"
);
assert!(result[0].new_path.is_none());
}
#[test]
fn hg_should_parse_multiple_files() {
let diff = r#"diff -r abc123 file1.rs
--- a/file1.rs
+++ b/file1.rs
@@ -1,1 +1,2 @@
line1
+line2
diff -r abc123 file2.rs
--- a/file2.rs
+++ b/file2.rs
@@ -1,2 +1,1 @@
keep
-remove
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(
result[0].new_path.as_ref().unwrap().to_str().unwrap(),
"file1.rs"
);
assert_eq!(
result[1].new_path.as_ref().unwrap().to_str().unwrap(),
"file2.rs"
);
}
#[test]
fn hg_should_parse_multiple_hunks() {
let diff = r#"diff -r abc123 multi.rs
--- a/multi.rs
+++ b/multi.rs
@@ -1,3 +1,4 @@
fn first() {
+ // added
}
@@ -10,3 +11,4 @@
fn second() {
+ // also added
}
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].hunks.len(), 2);
assert_eq!(result[0].hunks[0].old_start, 1);
assert_eq!(result[0].hunks[1].old_start, 10);
}
#[test]
fn hg_should_parse_renamed_file() {
let diff = r#"diff -r abc123 new_name.rs
rename from old_name.rs
rename to new_name.rs
--- a/old_name.rs
+++ b/new_name.rs
@@ -1,1 +1,1 @@
-old content
+new content
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].status, FileStatus::Renamed);
assert_eq!(
result[0].old_path.as_ref().unwrap().to_str().unwrap(),
"old_name.rs"
);
assert_eq!(
result[0].new_path.as_ref().unwrap().to_str().unwrap(),
"new_name.rs"
);
}
#[test]
fn hg_should_parse_binary_file() {
let diff = r#"diff -r abc123 image.png
Binary file image.png has changed
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].is_binary);
assert!(result[0].hunks.is_empty());
}
#[test]
fn hg_should_parse_renamed_file_without_content_changes() {
let diff = r#"diff -r abc123 new_name.rs
rename from old_name.rs
rename to new_name.rs
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].status, FileStatus::Renamed);
assert_eq!(result[0].old_path, Some(PathBuf::from("old_name.rs")));
assert_eq!(result[0].new_path, Some(PathBuf::from("new_name.rs")));
assert!(result[0].hunks.is_empty());
}
#[test]
fn hg_should_parse_copied_file_without_content_changes() {
let diff = r#"diff -r abc123 dest.rs
copy from source.rs
copy to dest.rs
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].status, FileStatus::Copied);
assert_eq!(result[0].old_path, Some(PathBuf::from("source.rs")));
assert_eq!(result[0].new_path, Some(PathBuf::from("dest.rs")));
assert!(result[0].hunks.is_empty());
}
#[test]
fn hg_should_parse_copied_file_with_content_changes() {
let diff = r#"diff -r abc123 dest.rs
copy from source.rs
copy to dest.rs
--- a/source.rs Thu Jan 01 00:00:00 1970 +0000
+++ b/dest.rs Thu Jan 01 00:00:00 1970 +0000
@@ -1 +1,2 @@
original
+added line
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].status, FileStatus::Copied);
assert_eq!(result[0].old_path, Some(PathBuf::from("source.rs")));
assert_eq!(result[0].new_path, Some(PathBuf::from("dest.rs")));
assert_eq!(result[0].hunks.len(), 1);
}
#[test]
fn hg_should_handle_no_newline_marker() {
let diff = r#"diff -r abc123 no_newline.rs
--- a/no_newline.rs
+++ b/no_newline.rs
@@ -1,1 +1,1 @@
-old
\ No newline at end of file
+new
\ No newline at end of file
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].hunks[0].lines.len(), 2);
}
#[test]
fn hg_should_parse_line_numbers_correctly() {
let diff = r#"diff -r abc123 nums.rs
--- a/nums.rs
+++ b/nums.rs
@@ -5,4 +5,5 @@
context at 5
-deleted at 6
+added at 6
+added at 7
context at 7->8
"#;
let result =
parse_unified_diff(diff, DiffFormat::Hg, &SyntaxHighlighter::default()).unwrap();
let lines = &result[0].hunks[0].lines;
assert_eq!(lines[0].origin, LineOrigin::Context);
assert_eq!(lines[0].old_lineno, Some(5));
assert_eq!(lines[0].new_lineno, Some(5));
assert_eq!(lines[1].origin, LineOrigin::Deletion);
assert_eq!(lines[1].old_lineno, Some(6));
assert_eq!(lines[1].new_lineno, None);
assert_eq!(lines[2].origin, LineOrigin::Addition);
assert_eq!(lines[2].old_lineno, None);
assert_eq!(lines[2].new_lineno, Some(6));
assert_eq!(lines[3].origin, LineOrigin::Addition);
assert_eq!(lines[3].old_lineno, None);
assert_eq!(lines[3].new_lineno, Some(7));
assert_eq!(lines[4].origin, LineOrigin::Context);
assert_eq!(lines[4].old_lineno, Some(7));
assert_eq!(lines[4].new_lineno, Some(8));
}
#[test]
fn jj_should_parse_simple_diff() {
let diff = r#"diff --git a/file.txt b/file.txt
--- a/file.txt
+++ b/file.txt
@@ -1,3 +1,4 @@
line1
+added
line2
line3
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].new_path, Some(PathBuf::from("file.txt")));
assert_eq!(files[0].status, FileStatus::Modified);
assert_eq!(files[0].hunks.len(), 1);
assert_eq!(files[0].hunks[0].lines.len(), 4);
}
#[test]
fn jj_should_keep_highlighting_for_interleaved_typescript_hunk() {
let diff = r#"diff --git a/file.ts b/file.ts
--- a/file.ts
+++ b/file.ts
@@ -1,3 +1,4 @@
const msg = getMsg(
- "old argument"
+ "new argument",
+ { extra: true }
);
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
let lines = &files[0].hunks[0].lines;
assert_eq!(lines.len(), 5);
for (idx, line) in lines.iter().enumerate() {
assert!(
line.highlighted_spans.is_some(),
"line {idx} should retain highlighting"
);
}
}
#[test]
fn jj_should_parse_new_file() {
let diff = r#"diff --git a/new.txt b/new.txt
new file mode 100644
--- /dev/null
+++ b/new.txt
@@ -0,0 +1,2 @@
+line1
+line2
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].status, FileStatus::Added);
}
#[test]
fn jj_should_parse_deleted_file() {
let diff = r#"diff --git a/old.txt b/old.txt
deleted file mode 100644
--- a/old.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-line1
-line2
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].status, FileStatus::Deleted);
}
#[test]
fn jj_should_parse_renamed_file_without_content_changes() {
let diff = r#"diff --git a/old.txt b/new.txt
rename from old.txt
rename to new.txt
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].status, FileStatus::Renamed);
assert_eq!(files[0].old_path, Some(PathBuf::from("old.txt")));
assert_eq!(files[0].new_path, Some(PathBuf::from("new.txt")));
assert!(files[0].hunks.is_empty());
}
#[test]
fn jj_should_parse_renamed_file_with_content_changes() {
let diff = r#"diff --git a/old.txt b/new.txt
rename from old.txt
rename to new.txt
--- a/old.txt
+++ b/new.txt
@@ -1 +1 @@
-old content
+new content
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].status, FileStatus::Renamed);
assert_eq!(files[0].old_path, Some(PathBuf::from("old.txt")));
assert_eq!(files[0].new_path, Some(PathBuf::from("new.txt")));
assert_eq!(files[0].hunks.len(), 1);
}
#[test]
fn jj_should_parse_copied_file_without_content_changes() {
let diff = r#"diff --git a/source.txt b/dest.txt
copy from source.txt
copy to dest.txt
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].status, FileStatus::Copied);
assert_eq!(files[0].old_path, Some(PathBuf::from("source.txt")));
assert_eq!(files[0].new_path, Some(PathBuf::from("dest.txt")));
assert!(files[0].hunks.is_empty());
}
#[test]
fn jj_should_parse_copied_file_with_content_changes() {
let diff = r#"diff --git a/source.txt b/dest.txt
copy from source.txt
copy to dest.txt
--- a/source.txt
+++ b/dest.txt
@@ -1 +1,2 @@
original
+added line
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].status, FileStatus::Copied);
assert_eq!(files[0].old_path, Some(PathBuf::from("source.txt")));
assert_eq!(files[0].new_path, Some(PathBuf::from("dest.txt")));
assert_eq!(files[0].hunks.len(), 1);
}
#[test]
fn jj_should_parse_binary_file_added() {
let diff = r#"diff --git a/image.png b/image.png
new file mode 100644
index 0000000000..abc1234567
Binary files /dev/null and b/image.png differ
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert!(files[0].is_binary);
assert_eq!(files[0].status, FileStatus::Added);
assert!(files[0].old_path.is_none());
assert_eq!(files[0].new_path, Some(PathBuf::from("image.png")));
}
#[test]
fn jj_should_parse_binary_file_deleted() {
let diff = r#"diff --git a/image.png b/image.png
deleted file mode 100644
index abc1234567..0000000000
Binary files a/image.png and /dev/null differ
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert!(files[0].is_binary);
assert_eq!(files[0].status, FileStatus::Deleted);
assert_eq!(files[0].old_path, Some(PathBuf::from("image.png")));
assert!(files[0].new_path.is_none());
}
#[test]
fn jj_should_parse_binary_file_modified() {
let diff = r#"diff --git a/image.png b/image.png
index abc1234567..def7890123 100644
Binary files a/image.png and b/image.png differ
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 1);
assert!(files[0].is_binary);
assert_eq!(files[0].status, FileStatus::Modified);
assert_eq!(files[0].old_path, Some(PathBuf::from("image.png")));
assert_eq!(files[0].new_path, Some(PathBuf::from("image.png")));
}
#[test]
fn jj_should_parse_multiple_files() {
let diff = r#"diff --git a/a.txt b/a.txt
--- a/a.txt
+++ b/a.txt
@@ -1 +1 @@
-old
+new
diff --git a/b.txt b/b.txt
--- a/b.txt
+++ b/b.txt
@@ -1 +1 @@
-foo
+bar
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
assert_eq!(files.len(), 2);
assert_eq!(files[0].new_path, Some(PathBuf::from("a.txt")));
assert_eq!(files[1].new_path, Some(PathBuf::from("b.txt")));
}
#[test]
fn jj_should_calculate_line_numbers() {
let diff = r#"diff --git a/file.txt b/file.txt
--- a/file.txt
+++ b/file.txt
@@ -5,4 +5,5 @@
context
-deleted
+added1
+added2
more
"#;
let files =
parse_unified_diff(diff, DiffFormat::GitStyle, &SyntaxHighlighter::default()).unwrap();
let hunk = &files[0].hunks[0];
assert_eq!(hunk.lines[0].old_lineno, Some(5));
assert_eq!(hunk.lines[0].new_lineno, Some(5));
assert_eq!(hunk.lines[1].old_lineno, Some(6));
assert_eq!(hunk.lines[1].new_lineno, None);
assert_eq!(hunk.lines[2].old_lineno, None);
assert_eq!(hunk.lines[2].new_lineno, Some(6));
assert_eq!(hunk.lines[3].old_lineno, None);
assert_eq!(hunk.lines[3].new_lineno, Some(7));
assert_eq!(hunk.lines[4].old_lineno, Some(7));
assert_eq!(hunk.lines[4].new_lineno, Some(8));
}
}