191 lines
5.8 KiB
Rust
191 lines
5.8 KiB
Rust
#[derive(Debug, Eq, PartialEq)]
|
|
pub struct Line {
|
|
pub old_number: String,
|
|
pub new_number: String,
|
|
pub text: String,
|
|
pub kind: &'static str,
|
|
}
|
|
|
|
pub struct Parsed {
|
|
pub lines: Vec<Line>,
|
|
pub columns: usize,
|
|
}
|
|
|
|
pub fn parse_file(diff: &str, path: &str) -> Parsed {
|
|
let section = sections(diff)
|
|
.into_iter()
|
|
.find(|section| section_path(section).as_deref() == Some(path))
|
|
.unwrap_or("");
|
|
if section.is_empty() {
|
|
let text = format!("No diff data is available for {path}.");
|
|
return Parsed {
|
|
columns: text.chars().count(),
|
|
lines: vec![Line {
|
|
old_number: String::new(),
|
|
new_number: String::new(),
|
|
text,
|
|
kind: "header",
|
|
}],
|
|
};
|
|
}
|
|
let mut old = 0;
|
|
let mut new = 0;
|
|
let mut columns = 0;
|
|
let lines = section
|
|
.lines()
|
|
.map(|text| {
|
|
columns = columns.max(display_columns(text));
|
|
let (old_number, new_number, kind) = if text.starts_with("@@") {
|
|
if let Some((old_start, new_start)) = hunk_starts(text) {
|
|
old = old_start;
|
|
new = new_start;
|
|
}
|
|
(String::new(), String::new(), "hunk")
|
|
} else if text.starts_with('+') && !text.starts_with("+++") {
|
|
let number = new.to_string();
|
|
new += 1;
|
|
(String::new(), number, "addition")
|
|
} else if text.starts_with('-') && !text.starts_with("---") {
|
|
let number = old.to_string();
|
|
old += 1;
|
|
(number, String::new(), "removal")
|
|
} else if text.starts_with(' ') {
|
|
let numbers = (old.to_string(), new.to_string());
|
|
old += 1;
|
|
new += 1;
|
|
(numbers.0, numbers.1, "context")
|
|
} else {
|
|
(String::new(), String::new(), "header")
|
|
};
|
|
Line {
|
|
old_number,
|
|
new_number,
|
|
text: text.into(),
|
|
kind,
|
|
}
|
|
})
|
|
.collect();
|
|
Parsed { lines, columns }
|
|
}
|
|
|
|
fn sections(diff: &str) -> Vec<&str> {
|
|
let mut starts: Vec<_> = diff
|
|
.match_indices("diff --git ")
|
|
.map(|(index, _)| index)
|
|
.collect();
|
|
starts.push(diff.len());
|
|
starts
|
|
.windows(2)
|
|
.map(move |bounds| &diff[bounds[0]..bounds[1]])
|
|
.collect()
|
|
}
|
|
|
|
fn section_path(section: &str) -> Option<String> {
|
|
section
|
|
.lines()
|
|
.find_map(|line| line.strip_prefix("+++ "))
|
|
.filter(|path| *path != "/dev/null")
|
|
.or_else(|| {
|
|
section
|
|
.lines()
|
|
.find_map(|line| line.strip_prefix("--- "))
|
|
.filter(|path| *path != "/dev/null")
|
|
})
|
|
.map(|path| {
|
|
decode_path(path)
|
|
.trim_start_matches("a/")
|
|
.trim_start_matches("b/")
|
|
.into()
|
|
})
|
|
}
|
|
|
|
fn decode_path(path: &str) -> String {
|
|
let path = path.trim_matches('"');
|
|
let mut bytes = Vec::with_capacity(path.len());
|
|
let mut chars = path.bytes();
|
|
while let Some(byte) = chars.next() {
|
|
if byte != b'\\' {
|
|
bytes.push(byte);
|
|
continue;
|
|
}
|
|
let Some(escaped) = chars.next() else { break };
|
|
match escaped {
|
|
b'n' => bytes.push(b'\n'),
|
|
b'r' => bytes.push(b'\r'),
|
|
b't' => bytes.push(b'\t'),
|
|
b'\\' | b'"' => bytes.push(escaped),
|
|
b'0'..=b'7' => {
|
|
let mut value = escaped - b'0';
|
|
for _ in 0..2 {
|
|
let Some(next) = chars.next() else { break };
|
|
if !(b'0'..=b'7').contains(&next) {
|
|
bytes.push(value);
|
|
bytes.push(next);
|
|
value = 0;
|
|
break;
|
|
}
|
|
value = value * 8 + next - b'0';
|
|
}
|
|
if value != 0 {
|
|
bytes.push(value);
|
|
}
|
|
}
|
|
other => bytes.push(other),
|
|
}
|
|
}
|
|
String::from_utf8_lossy(&bytes).into_owned()
|
|
}
|
|
|
|
fn hunk_starts(line: &str) -> Option<(i32, i32)> {
|
|
let mut parts = line.split_whitespace();
|
|
(parts.next()? == "@@").then_some(())?;
|
|
Some((
|
|
range_start(parts.next()?, '-'),
|
|
range_start(parts.next()?, '+'),
|
|
))
|
|
}
|
|
|
|
fn range_start(value: &str, prefix: char) -> i32 {
|
|
value
|
|
.strip_prefix(prefix)
|
|
.and_then(|value| value.split(',').next())
|
|
.and_then(|value| value.parse().ok())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn display_columns(line: &str) -> usize {
|
|
line.chars().fold(0, |column, character| {
|
|
if character == '\t' {
|
|
(column / 8 + 1) * 8
|
|
} else {
|
|
column + 1
|
|
}
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn selects_and_numbers_a_file_diff() {
|
|
let diff = "diff --git a/one b/one\n--- a/one\n+++ b/one\n@@ -1 +1 @@\n-old\n+new\n\
|
|
diff --git a/two b/two\n--- a/two\n+++ b/two\n@@ -4,2 +8,3 @@\n context\n-removed\n+added\n";
|
|
let parsed = parse_file(diff, "two");
|
|
assert_eq!(parsed.lines[4].old_number, "4");
|
|
assert_eq!(parsed.lines[4].new_number, "8");
|
|
assert_eq!(parsed.lines[5].kind, "removal");
|
|
assert_eq!(parsed.lines[6].kind, "addition");
|
|
assert!(parsed.lines.iter().all(|line| !line.text.contains("old")));
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_git_quoted_paths() {
|
|
let diff = "diff --git \"a/caf\\303\\251\" \"b/caf\\303\\251\"\n--- \"a/caf\\303\\251\"\n+++ \"b/caf\\303\\251\"\n@@ -0,0 +1 @@\n+hello\n";
|
|
assert_eq!(
|
|
parse_file(diff, "café").lines.last().unwrap().kind,
|
|
"addition"
|
|
);
|
|
}
|
|
}
|