pub(crate) const DELETED_ENTRY: &str = "(file deleted)";
pub(crate) const BINARY_ENTRY: &str = "(binary file)";
pub(crate) const SUBMODULE_ENTRY: &str = "(submodule)";
pub(crate) const EMPTY_ENTRY: &str = "(empty file)";
const HUNK_HEADER: &str = "@@ -";
pub(crate) const NEW_FILE_HEADER: &str = "@@ -0,0 ";
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct DiffHunk {
pub text: String,
pub modified_lines: Vec<usize>,
}
impl DiffHunk {
pub(crate) fn is_text(&self) -> bool {
self.text.starts_with(HUNK_HEADER)
}
pub(crate) fn is_whole_new_file(&self) -> bool {
self.text.starts_with(NEW_FILE_HEADER)
}
pub(crate) fn added_lines(&self) -> Vec<usize> {
let mut lines = self.text.lines();
let Some(mut line_no) = lines.next().and_then(parse_hunk_new_start) else {
return vec![];
};
let mut added = Vec::new();
for line in lines {
match line.as_bytes().first() {
Some(b'+') => {
added.push(line_no);
line_no += 1;
}
Some(b'-' | b'\\') => {}
_ => line_no += 1,
}
}
added
}
}
pub(crate) fn parse_hunks(diff: &str) -> Vec<DiffHunk> {
let mut hunks: Vec<DiffHunk> = Vec::new();
let mut current_text = String::new();
let mut current_modified: Vec<usize> = Vec::new();
let mut current_orig_line: usize = 0;
let mut in_hunk = false;
for line in diff.lines() {
if line.starts_with(HUNK_HEADER) {
if in_hunk {
hunks.push(DiffHunk {
text: std::mem::take(&mut current_text),
modified_lines: std::mem::take(&mut current_modified),
});
}
current_text = format!("{}\n", line);
current_modified = Vec::new();
current_orig_line = parse_hunk_start(line).unwrap_or(1);
in_hunk = true;
} else if !in_hunk {
continue;
} else if line.starts_with('-') {
current_text.push_str(line);
current_text.push('\n');
current_modified.push(current_orig_line);
current_orig_line += 1;
} else if line.starts_with('+') {
current_text.push_str(line);
current_text.push('\n');
} else if line.starts_with('\\') {
current_text.push_str(line);
current_text.push('\n');
} else {
current_text.push_str(line);
current_text.push('\n');
current_orig_line += 1;
}
}
if in_hunk {
hunks.push(DiffHunk {
text: current_text,
modified_lines: current_modified,
});
}
hunks
}
pub(crate) fn parse_hunk_start(line: &str) -> Option<usize> {
let line = line.strip_prefix(HUNK_HEADER)?;
let end = line.find([',', ' '])?;
line[..end].parse().ok()
}
fn parse_hunk_new_start(line: &str) -> Option<usize> {
let line = line.strip_prefix(HUNK_HEADER)?;
let new = &line[line.find(" +")? + 2..];
let end = new.find([',', ' '])?;
new[..end].parse().ok()
}
pub(crate) fn build_hunk_patch(path: &str, hunks: &[impl std::borrow::Borrow<DiffHunk>]) -> String {
let mut hunks = hunks.iter().map(|h| h.borrow()).filter(|h| h.is_text());
let Some(first) = hunks.next() else {
return String::new();
};
let mut patch = format!("--- a/{path}\n+++ b/{path}\n");
patch.push_str(&first.text);
for hunk in hunks {
patch.push_str(&hunk.text);
}
patch
}
#[cfg(test)]
#[path = "diff_test.rs"]
mod tests;