Add pull request and repository browsing
This commit is contained in:
190
crates/app/src/diff.rs
Normal file
190
crates/app/src/diff.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
#[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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user