Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 123 additions & 29 deletions src/gitu_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ impl<'a> Parser<'a> {
fn skip_until_diff_header(&mut self) -> ThinResult<()> {
log::trace!("Parser::skip_until_diff_header\n{:?}", self);
while self.cursor < self.input.len() && !self.is_at_diff_header() {
self.consume_until(Self::newline_or_eof)?;
self.consume_until_after(Self::newline_or_eof)?;
}

Ok(())
Expand Down Expand Up @@ -304,59 +304,61 @@ impl<'a> Parser<'a> {

if self.peek("new file") {
diff_type = Status::Added;
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
} else if self.peek("deleted file") {
diff_type = Status::Deleted;
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
}

if self.consume("similarity index").is_ok() {
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
}

if self.consume("dissimilarity index").is_ok() {
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
}

if self.peek("index") {
} else if self.peek("old mode") {
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
if self.peek("new mode") {
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
}
} else if self.peek("new mode") {
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
} else if self.peek("deleted file mode") {
diff_type = Status::Deleted;
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
} else if self.peek("new file mode") {
diff_type = Status::Added;
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
} else if self.peek("copy from") {
diff_type = Status::Copied;
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
self.consume("copy to")?;
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
} else if self.peek("rename from") {
diff_type = Status::Renamed;
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
self.consume("rename to")?;
self.consume_until(Self::newline)?;
self.consume_until_after(Self::newline)?;
}

if self.peek("index") {
self.consume("index ")?;
self.consume_until(Self::newline_or_eof)?;
self.consume_until_after(Self::newline_or_eof)?;
}

if self.peek("Binary files ") {
self.consume_until(Self::newline_or_eof)?;
self.consume_until_after(Self::newline_or_eof)?;
}

if self.consume("--- ").is_ok() {
old_file = self.diff_header_path(Self::newline_or_eof)?;
self.newline_or_eof()?;
self.consume("+++ ")?;
new_file = self.diff_header_path(Self::newline_or_eof)?;
self.newline_or_eof()?;
}

Ok(DiffHeader {
Expand All @@ -371,6 +373,7 @@ impl<'a> Parser<'a> {
log::trace!("Parser::unmerged_file\n{:?}", self);
let unmerged_path_prefix = self.consume("* Unmerged path ")?;
let file = self.diff_header_path(Self::newline_or_eof)?;
self.newline_or_eof()?;

Ok(DiffHeader {
range: unmerged_path_prefix.start..self.cursor,
Expand All @@ -383,25 +386,49 @@ impl<'a> Parser<'a> {
fn old_new_file_header(&mut self) -> ThinResult<(FilePath, FilePath, bool)> {
log::trace!("Parser::old_new_file_header\n{:?}", self);
self.consume("diff --git ")?;
let old_path = self.diff_header_path(Self::ascii_whitespace)?;

// Vary the delimiter of file-paths depending on whether it is quoted, prefixed, or lacks prefix.
// e.g.
// "a/file-1.txt" "b/file-1.txt" -> whitespace delimiter
// a/file-1.txt b/file-1.txt -> whitespace delimiter, followed by a prefix ("b/")
// file-1.txt file-1.txt -> whitespace delimiter
let delim: ParseFn<Range<usize>> = if self.peek("\"") {
Self::ascii_whitespace
} else if self.peek_fn(Self::diff_header_path_prefix) {
|p: &mut Parser<'_>| {
let start = p.cursor;
p.ascii_whitespace()
.and_then(|_| p.diff_header_path_prefix())?;
Ok(start..p.cursor)
}
} else {
Self::ascii_whitespace
};

let old_path = self.diff_header_path(delim)?;
self.ascii_whitespace()?;
let new_path = self.diff_header_path(Self::newline_or_eof)?;
self.newline_or_eof()?;

Ok((old_path, new_path, false))
}

fn diff_header_path(&mut self, end: ParseFn<'a, Range<usize>>) -> ThinResult<FilePath> {
/// When the header path is not quoted ("..."), then `stop_lookahead` is used.
fn diff_header_path(
&mut self,
stop_lookahead: ParseFn<'a, Range<usize>>,
) -> ThinResult<FilePath> {
log::trace!("Parser::diff_header_path\n{:?}", self);
if self.consume("\"").ok().is_some() {
self.diff_header_path_prefix().ok();
let quoted = self.quoted()?;
self.ascii_whitespace().ok();
Ok(FilePath {
range: quoted,
is_quoted: true,
})
} else {
self.diff_header_path_prefix().ok();
let (consumed, _) = self.consume_until(end)?;
let consumed = self.consume_until_before(stop_lookahead)?;
Ok(FilePath {
range: consumed,
is_quoted: false,
Expand Down Expand Up @@ -467,7 +494,7 @@ impl<'a> Parser<'a> {
.map_err(|_| {
self.cursor = start;
array_vec![ThinParseError {
expected: "<diff header path prefix (' a/...' or ' b/...')>",
expected: "<diff header path prefix (e.g. 'a/' or 'b/')>",
}]
})?;

Expand Down Expand Up @@ -497,6 +524,7 @@ impl<'a> Parser<'a> {
log::trace!("Parser::conflicted_file\n{:?}", self);
self.consume("diff --cc ")?;
let file = self.diff_header_path(Self::newline_or_eof)?;
self.newline_or_eof()?;
Ok((file.clone(), file, true))
}

Expand Down Expand Up @@ -565,7 +593,7 @@ impl<'a> Parser<'a> {
self.consume(" @@")?;
self.consume(" ").ok();

let (fn_ctx, newline) = self.consume_until(Self::newline_or_eof)?;
let (fn_ctx, newline) = self.consume_until_after(Self::newline_or_eof)?;

Ok(HunkHeader {
range: hunk_header_start..self.cursor,
Expand Down Expand Up @@ -597,7 +625,7 @@ impl<'a> Parser<'a> {
log::trace!("Parser::consume_lines_while_prefixed\n{:?}", self);
let start = self.cursor;
while self.cursor < self.input.len() && pred(self) {
self.consume_until(Self::newline_or_eof)?;
self.consume_until_after(Self::newline_or_eof)?;
}

Ok(start..self.cursor)
Expand Down Expand Up @@ -672,14 +700,35 @@ impl<'a> Parser<'a> {
}
}

/// Scans through the input, moving the cursor byte-by-byte
/// until the provided parse_fn will succeed, or the input has been exhausted.
/// Returns a tuple of the bytes scanned up until the match, excluding the match itself.
/// The resulting cursor will end up before the match.
fn consume_until_before<T: ParsedRange>(
&mut self,
parse_fn: fn(&mut Parser<'a>) -> ThinResult<T>,
) -> ThinResult<Range<usize>> {
log::trace!("Parser::consume_until_before\n{:?}", self);
let start = self.cursor;
let found = self.find(parse_fn).map_err(|mut err| {
err.try_push(ThinParseError {
expected: "to consume the match",
});
err
})?;
self.cursor = found.range().start;
Ok(start..found.range().start)
}

/// Scans through the input, moving the cursor byte-by-byte
/// until the provided parse_fn will succeed, or the input has been exhausted.
/// Returns a tuple of the bytes scanned up until the match, and the match itself.
fn consume_until<T: ParsedRange>(
/// The resulting cursor will end up after the match.
fn consume_until_after<T: ParsedRange>(
&mut self,
parse_fn: fn(&mut Parser<'a>) -> ThinResult<T>,
) -> ThinResult<(Range<usize>, T)> {
log::trace!("Parser::consume_until\n{:?}", self);
log::trace!("Parser::consume_until_after\n{:?}", self);
let start = self.cursor;
let found = self.find(parse_fn).map_err(|mut err| {
err.try_push(ThinParseError {
Expand Down Expand Up @@ -720,6 +769,14 @@ impl<'a> Parser<'a> {
Err(errors)
}

fn peek_fn<T: ParsedRange>(&self, parse_fn: ParseFn<'a, T>) -> bool {
let mut p = Parser {
input: self.input,
cursor: self.cursor,
};
parse_fn(&mut p).is_ok()
}

/// Consumes `expected` from the input and moves the cursor past it.
/// Returns an error if `expected` was not found at the cursor.
fn consume(&mut self, expected: &'static str) -> ThinResult<Range<usize>> {
Expand Down Expand Up @@ -1110,7 +1167,6 @@ mod tests {

#[test]
fn filenames_with_spaces() {
// This case is ambiguous, normally if there's ---/+++ headers, we can use that.
let input = "\
diff --git a/file one.txt b/file two.txt\n\
index 5626abf..f719efd 100644\n\
Expand All @@ -1120,8 +1176,41 @@ mod tests {
";
let mut parser = Parser::new(input);
let diff = parser.parse_diff().unwrap();
assert_eq!(diff[0].header.old_file.fmt(input), "file");
assert_eq!(diff[0].header.new_file.fmt(input), "one.txt b/file two.txt");
assert_eq!(diff[0].header.old_file.fmt(input), "file one.txt");
assert_eq!(diff[0].header.new_file.fmt(input), "file two.txt");
}

// #[test]
// fn filenames_with_spaces_ambiguous() {
// let input = "\
// diff --git a/file one.txt b/file two.txt b/file three.txt\n\
// index 5626abf..f719efd 100644\n\
// @@ -1 +1 @@\n\
// -one\n\
// +two\n\
// ";
// let mut parser = Parser::new(input);
// let result = parser.parse_diff();
// std::assert_matches!(result, Err(_));
// }

#[test]
fn added_file_with_spaces_in_name() {
let input = "diff --git a/something with space.md b/something with space.md\n\
new file mode 100644\n\
index 0000000..e69de29\n";
let mut parser = Parser::new(input);
let diff = parser.parse_diff().unwrap();
assert_eq!(diff.len(), 1);
assert_eq!(diff[0].header.status, Status::Added);
assert_eq!(
diff[0].header.old_file.fmt(input),
"something with space.md"
);
assert_eq!(
diff[0].header.new_file.fmt(input),
"something with space.md"
);
}

#[test]
Expand Down Expand Up @@ -1207,11 +1296,14 @@ mod tests {
index 0000000..e69de29\n\
diff --git \"a/\\a\" \"b/\\a\"\n\
new file mode 100644\n\
index 0000000..e69de29\n\
diff --git \"a/l\\303\\266l space\" \"b/l\\303\\266l space\"\n\
new file mode 100644\n\
index 0000000..e69de29";

let mut parser = Parser::new(input);
let diffs = parser.parse_diff().unwrap();
assert_eq!(diffs.len(), 8);
assert_eq!(diffs.len(), 9);
assert_eq!(diffs[0].header.old_file.fmt(input), "ö", "Old file");
assert_eq!(diffs[0].header.new_file.fmt(input), "ö", "New file");
assert_eq!(diffs[1].header.old_file.fmt(input), "\"", "Old file");
Expand All @@ -1222,6 +1314,8 @@ mod tests {
assert_eq!(diffs[3].header.new_file.fmt(input), "\r", "New file");
assert_eq!(diffs[4].header.old_file.fmt(input), "\n", "Old file");
assert_eq!(diffs[4].header.new_file.fmt(input), "\u{c}", "New file");
assert_eq!(diffs[8].header.old_file.fmt(input), "löl space", "Old file");
assert_eq!(diffs[8].header.new_file.fmt(input), "löl space", "New file");
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: src/tests/stage.rs
expression: ctx.redact_buffer()
---
On branch main |
Your branch is up to date with 'origin/main'. |
|
▌Staged changes (1) |
▌added file with space.txt |
|
Recent commits |
b66a0bf main origin/main add initial-file |
|
|
|
|
|
|
|
|
|
|
────────────────────────────────────────────────────────────────────────────────|
$ git add file with space.txt |
styles_hash: 5372e7d632c4cf90
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: src/tests/unstage.rs
expression: ctx.redact_buffer()
---
On branch main |
Your branch is up to date with 'origin/main'. |
|
Untracked files |
▌file with space.txt |
|
Recent commits |
b66a0bf main origin/main add initial-file |
|
|
|
|
|
|
|
|
|
|
────────────────────────────────────────────────────────────────────────────────|
$ git restore --staged file with space.txt |
styles_hash: 4d31ee8109cfa98e
7 changes: 7 additions & 0 deletions src/tests/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,10 @@ fn stage_deleted_executable_file() {
run(&ctx.dir, &["rm", "script.sh"]);
snapshot!(ctx, "jjs");
}

#[test]
fn stage_file_with_spaces_in_name() {
let ctx = setup_clone!();
run(&ctx.dir, &["touch", "file with space.txt"]);
snapshot!(ctx, "js");
}
8 changes: 8 additions & 0 deletions src/tests/unstage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,11 @@ fn unstage_deleted_executable_file() {
run(&ctx.dir, &["git", "rm", "script.sh"]);
snapshot!(ctx, "jju");
}

#[test]
fn unstage_added_file_with_spaces_in_name() {
let ctx = setup_clone!();
run(&ctx.dir, &["touch", "file with space.txt"]);
run(&ctx.dir, &["git", "add", "file with space.txt"]);
snapshot!(ctx, "jju");
}
Loading