From 9c840f6f4cc25530368e21c6cc54a716655e920d Mon Sep 17 00:00:00 2001 From: William Escande Date: Sun, 6 Feb 2022 23:29:41 +0100 Subject: [PATCH] Add support for irreversible-delete Fix #128 Add support for `git diff -D` (aka irreversible delete) The patch is adding a title for removed file, even when there is no file content in the diff output Without the patch, there was no output related to the file at all --- src/delta.rs | 3 +- src/handlers/diff_header.rs | 123 +++++++++++++++++++++++-------- src/handlers/diff_header_diff.rs | 2 +- src/tests/test_example_diffs.rs | 33 ++++++++- 4 files changed, 127 insertions(+), 34 deletions(-) diff --git a/src/delta.rs b/src/delta.rs index 7ba29973..27b30512 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -157,6 +157,7 @@ impl<'a> StateMachine<'a> { let _ = self.handle_commit_meta_header_line()? || self.handle_diff_stat_line()? || self.handle_diff_header_diff_line()? + || self.handle_diff_header_file_operation_line()? || self.handle_diff_header_minus_line()? || self.handle_diff_header_plus_line()? || self.handle_hunk_header_line()? @@ -173,7 +174,7 @@ impl<'a> StateMachine<'a> { || self.emit_line_unchanged()?; } - self.handle_pending_mode_line_with_diff_name()?; + self.handle_pending_line_with_diff_name()?; self.painter.paint_buffered_minus_and_plus_lines(); self.painter.emit()?; Ok(()) diff --git a/src/handlers/diff_header.rs b/src/handlers/diff_header.rs index 3e317bee..39262c1b 100644 --- a/src/handlers/diff_header.rs +++ b/src/handlers/diff_header.rs @@ -14,9 +14,11 @@ const DIFF_PREFIXES: [&str; 6] = ["a/", "b/", "c/", "i/", "o/", "w/"]; #[derive(Debug, PartialEq)] pub enum FileEvent { + Added, Change, Copy, Rename, + Removed, NoEvent, } @@ -49,6 +51,25 @@ impl<'a> StateMachine<'a> { Ok(handled_line) } + fn should_write_generic_diff_header_header_line(&mut self) -> std::io::Result { + // In color_only mode, raw_line's structure shouldn't be changed. + // So it needs to avoid fn _handle_diff_header_header_line + // (it connects the plus_file and minus_file), + // and to call fn handle_generic_diff_header_header_line directly. + if self.config.color_only { + write_generic_diff_header_header_line( + &self.line, + &self.raw_line, + &mut self.painter, + &mut self.mode_info, + self.config, + )?; + Ok(true) + } else { + Ok(false) + } + } + #[inline] fn test_diff_header_minus_line(&self) -> bool { (matches!(self.state, State::DiffHeader(_)) || self.source == Source::DiffUnified) @@ -62,7 +83,6 @@ impl<'a> StateMachine<'a> { if !self.test_diff_header_minus_line() { return Ok(false); } - let mut handled_line = false; let (path_or_mode, file_event) = parse_diff_header_line(&self.line, self.source == Source::GitDiff); @@ -84,21 +104,7 @@ impl<'a> StateMachine<'a> { } self.painter.paint_buffered_minus_and_plus_lines(); - // In color_only mode, raw_line's structure shouldn't be changed. - // So it needs to avoid fn _handle_diff_header_header_line - // (it connects the plus_file and minus_file), - // and to call fn handle_generic_diff_header_header_line directly. - if self.config.color_only { - write_generic_diff_header_header_line( - &self.line, - &self.raw_line, - &mut self.painter, - &mut self.mode_info, - self.config, - )?; - handled_line = true; - } - Ok(handled_line) + self.should_write_generic_diff_header_header_line() } #[inline] @@ -129,25 +135,58 @@ impl<'a> StateMachine<'a> { self.current_file_pair = Some((self.minus_file.clone(), self.plus_file.clone())); self.painter.paint_buffered_minus_and_plus_lines(); - // In color_only mode, raw_line's structure shouldn't be changed. - // So it needs to avoid fn _handle_diff_header_header_line - // (it connects the plus_file and minus_file), - // and to call fn handle_generic_diff_header_header_line directly. - if self.config.color_only { - write_generic_diff_header_header_line( - &self.line, - &self.raw_line, - &mut self.painter, - &mut self.mode_info, - self.config, - )?; - handled_line = true + if self.should_write_generic_diff_header_header_line()? { + handled_line = true; } else if self.should_handle() && self.handled_diff_header_header_line_file_pair != self.current_file_pair { self.painter.emit()?; self._handle_diff_header_header_line(self.source == Source::DiffUnified)?; - self.handled_diff_header_header_line_file_pair = self.current_file_pair.clone() + self.handled_diff_header_header_line_file_pair = self.current_file_pair.clone(); + } + Ok(handled_line) + } + + #[inline] + fn test_diff_header_file_operation_line(&self) -> bool { + (matches!(self.state, State::DiffHeader(_)) || self.source == Source::DiffUnified) + && (self.line.starts_with("deleted file mode ") + || self.line.starts_with("new file mode ")) + } + + /// Check for and handle the "deleted file ..." line. + pub fn handle_diff_header_file_operation_line(&mut self) -> std::io::Result { + if !self.test_diff_header_file_operation_line() { + return Ok(false); + } + let mut handled_line = false; + let (_mode_info, file_event) = + parse_diff_header_line(&self.line, self.source == Source::GitDiff); + let name = get_repeated_file_path_from_diff_line(&self.diff_line) + .unwrap_or_else(|| "".to_string()); + match file_event { + FileEvent::Removed => { + self.minus_file = name; + self.plus_file = "/dev/null".into(); + self.minus_file_event = FileEvent::Change; + self.plus_file_event = FileEvent::Change; + self.current_file_pair = Some((self.minus_file.clone(), self.plus_file.clone())); + } + FileEvent::Added => { + self.minus_file = "/dev/null".into(); + self.plus_file = name; + self.minus_file_event = FileEvent::Change; + self.plus_file_event = FileEvent::Change; + self.current_file_pair = Some((self.minus_file.clone(), self.plus_file.clone())); + } + _ => (), + } + + if self.should_write_generic_diff_header_header_line()? + || (self.should_handle() + && self.handled_diff_header_header_line_file_pair != self.current_file_pair) + { + handled_line = true; } Ok(handled_line) } @@ -172,7 +211,16 @@ impl<'a> StateMachine<'a> { ) } - pub fn handle_pending_mode_line_with_diff_name(&mut self) -> std::io::Result<()> { + #[inline] + fn test_pending_line_with_diff_name(&self) -> bool { + matches!(self.state, State::DiffHeader(_)) || self.source == Source::DiffUnified + } + + pub fn handle_pending_line_with_diff_name(&mut self) -> std::io::Result<()> { + if !self.test_pending_line_with_diff_name() { + return Ok(()); + } + if !self.mode_info.is_empty() { let format_label = |label: &str| { if !label.is_empty() { @@ -204,6 +252,13 @@ impl<'a> StateMachine<'a> { &mut self.mode_info, self.config, ) + } else if !self.config.color_only + && self.should_handle() + && self.handled_diff_header_header_line_file_pair != self.current_file_pair + { + self._handle_diff_header_header_line(self.source == Source::DiffUnified)?; + self.handled_diff_header_header_line_file_pair = self.current_file_pair.clone(); + Ok(()) } else { Ok(()) } @@ -292,6 +347,12 @@ fn parse_diff_header_line(line: &str, git_diff_name: bool) -> (String, FileEvent line if line.starts_with("copy to ") => { (line[8..].to_string(), FileEvent::Copy) // "copy to ".len() } + line if line.starts_with("new file mode ") => { + (line[14..].to_string(), FileEvent::Added) // "new file mode ".len() + } + line if line.starts_with("deleted file mode ") => { + (line[18..].to_string(), FileEvent::Removed) // "deleted file mode ".len() + } _ => ("".to_string(), FileEvent::NoEvent), } } diff --git a/src/handlers/diff_header_diff.rs b/src/handlers/diff_header_diff.rs index fdd2d39a..a79f6e1c 100644 --- a/src/handlers/diff_header_diff.rs +++ b/src/handlers/diff_header_diff.rs @@ -22,7 +22,7 @@ impl<'a> StateMachine<'a> { } else { State::DiffHeader(DiffType::Unified) }; - self.handle_pending_mode_line_with_diff_name()?; + self.handle_pending_line_with_diff_name()?; self.handled_diff_header_header_line_file_pair = None; self.diff_line = self.line.clone(); if !self.should_skip_line() { diff --git a/src/tests/test_example_diffs.rs b/src/tests/test_example_diffs.rs index 2eca792f..325f2e9a 100644 --- a/src/tests/test_example_diffs.rs +++ b/src/tests/test_example_diffs.rs @@ -17,7 +17,6 @@ mod tests { } #[test] - #[ignore] // #128 fn test_added_empty_file() { DeltaTest::with_args(&[]) .with_input(ADDED_EMPTY_FILE) @@ -1570,6 +1569,21 @@ src/align.rs:71: impl<'a> Alignment<'a> { │ .expect_contains("Δ src/delta.rs (mode 100700 -> 100644)"); } + #[test] + fn test_file_deleted_without_preimage() { + DeltaTest::with_args(&[]) + .with_input(GIT_DIFF_FILE_DELETED_WITHOUT_PREIMAGE) + .expect_contains("removed: foo.bar"); + } + + #[test] + fn test_files_deleted_without_preimage() { + DeltaTest::with_args(&[]) + .with_input(GIT_DIFF_FILES_DELETED_WITHOUT_PREIMAGE) + .expect_contains("removed: foo") + .expect_contains("removed: bar"); + } + #[test] fn test_file_mode_change_with_diff() { DeltaTest::with_args(&["--navigate", "--keep-plus-minus-markers"]) @@ -2332,6 +2346,23 @@ new mode 100644 diff --git a/src/delta.rs b/src/delta.rs old mode 100700 new mode 100644 +"; + + // This output can be generated with `git diff -D` + const GIT_DIFF_FILE_DELETED_WITHOUT_PREIMAGE: &str = " +diff --git a/foo.bar b/foo.bar +deleted file mode 100644 +index e019be0..0000000 +"; + + // This output can be generated with `git diff -D` + const GIT_DIFF_FILES_DELETED_WITHOUT_PREIMAGE: &str = " +diff --git a/foo b/foo +deleted file mode 100644 +index e019be0..0000000 +diff --git a/bar b/bar +deleted file mode 100644 +index e019be0..0000000 "; const GIT_DIFF_FILE_MODE_CHANGE_WITH_DIFF: &str = "