diff options
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/cli.rs | 10 | ||||
-rw-r--r-- | src/config.rs | 6 | ||||
-rw-r--r-- | src/delta.rs | 42 | ||||
-rw-r--r-- | src/options/set.rs | 2 | ||||
-rw-r--r-- | src/parse.rs | 108 |
7 files changed, 157 insertions, 19 deletions
@@ -272,6 +272,7 @@ dependencies = [ "git2", "itertools", "lazy_static", + "pathdiff", "regex", "shell-words", "structopt", @@ -497,6 +498,12 @@ dependencies = [ ] [[package]] +name = "pathdiff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34" + +[[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -25,6 +25,7 @@ console = "0.14.1" dirs-next = "2.0.0" itertools = "0.10.0" lazy_static = "1.4" +pathdiff = "0.2.0" regex = "1.4.6" shell-words = "1.0.0" structopt = "0.3.21" @@ -231,6 +231,11 @@ pub struct Opt { /// --file-renamed-label. pub navigate: bool, + #[structopt(long = "relative-paths")] + /// Output all file paths relative to the current directory so that they + /// resolve correctly when clicked on or used in shell commands. + pub relative_paths: bool, + #[structopt(long = "hyperlinks")] /// Render commit hashes, file names, and line numbers as hyperlinks, according to the /// hyperlink spec for terminal emulators: @@ -482,6 +487,11 @@ pub struct Opt { #[structopt(short = "w", long = "width")] pub width: Option<String>, + /// Width allocated for file paths in a diff stat section. If a relativized + /// file path exceeds this width then the diff stat will be misaligned. + #[structopt(long = "diff-stat-align-width", default_value = "48")] + pub diff_stat_align_width: usize, + /// The number of spaces to replace tab characters with. Use --tabs=0 to pass tab characters /// through directly, but note that in that case delta will calculate line widths assuming tabs /// occupy one character's width on the screen: if your terminal renders tabs as more than than diff --git a/src/config.rs b/src/config.rs index f822efc2..fbad2e41 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,7 +23,9 @@ pub struct Config { pub background_color_extends_to_terminal_width: bool, pub commit_style: Style, pub color_only: bool, + pub cwd_relative_to_repo_root: Option<String>, pub decorations_width: cli::Width, + pub diff_stat_align_width: usize, pub error_exit_code: i32, pub file_added_label: String, pub file_copied_label: String, @@ -70,6 +72,7 @@ pub struct Config { pub plus_style: Style, pub git_minus_style: Style, pub git_plus_style: Style, + pub relative_paths: bool, pub show_themes: bool, pub side_by_side: bool, pub side_by_side_data: side_by_side::SideBySideData, @@ -182,7 +185,9 @@ impl From<cli::Opt> for Config { .background_color_extends_to_terminal_width, commit_style, color_only: opt.color_only, + cwd_relative_to_repo_root: std::env::var("GIT_PREFIX").ok(), decorations_width: opt.computed.decorations_width, + diff_stat_align_width: opt.diff_stat_align_width, error_exit_code: 2, // Use 2 for error because diff uses 0 and 1 for non-error. file_added_label, file_copied_label, @@ -235,6 +240,7 @@ impl From<cli::Opt> for Config { plus_style, git_minus_style, git_plus_style, + relative_paths: opt.relative_paths, show_themes: opt.show_themes, side_by_side: opt.side_by_side, side_by_side_data, diff --git a/src/delta.rs b/src/delta.rs index 7d52eae1..26dbc687 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -113,6 +113,8 @@ impl<'a> StateMachine<'a> { let mut handled_line = if line.starts_with("commit ") { self.handle_commit_meta_header_line()? + } else if self.state == State::CommitMeta && line.starts_with(' ') { + self.handle_diff_stat_line()? } else if line.starts_with("diff ") { self.handle_file_meta_diff_line()? } else if (self.state == State::FileMeta || self.source == Source::DiffUnified) @@ -226,6 +228,24 @@ impl<'a> StateMachine<'a> { Ok(()) } + fn handle_diff_stat_line(&mut self) -> std::io::Result<bool> { + let mut handled_line = false; + if self.config.relative_paths { + if let Some(cwd) = self.config.cwd_relative_to_repo_root.as_deref() { + if let Some(replacement_line) = parse::relativize_path_in_diff_stat_line( + &self.raw_line, + cwd, + self.config.diff_stat_align_width, + ) { + self.painter.emit()?; + writeln!(self.painter.writer, "{}", replacement_line)?; + handled_line = true + } + } + } + Ok(handled_line) + } + #[allow(clippy::unnecessary_wraps)] fn handle_file_meta_diff_line(&mut self) -> std::io::Result<bool> { self.painter.paint_buffered_minus_and_plus_lines(); @@ -237,8 +257,15 @@ impl<'a> StateMachine<'a> { fn handle_file_meta_minus_line(&mut self) -> std::io::Result<bool> { let mut handled_line = false; - let parsed_file_meta_line = - parse::parse_file_meta_line(&self.line, self.source == Source::GitDiff); + let parsed_file_meta_line = parse::parse_file_meta_line( + &self.line, + self.source == Source::GitDiff, + if self.config.relative_paths { + self.config.cwd_relative_to_repo_root.as_deref() + } else { + None + }, + ); self.minus_file = parsed_file_meta_line.0; self.file_event = parsed_file_meta_line.1; @@ -271,8 +298,15 @@ impl<'a> StateMachine<'a> { fn handle_file_meta_plus_line(&mut self) -> std::io::Result<bool> { let mut handled_line = false; - let parsed_file_meta_line = - parse::parse_file_meta_line(&self.line, self.source == Source::GitDiff); + let parsed_file_meta_line = parse::parse_file_meta_line( + &self.line, + self.source == Source::GitDiff, + if self.config.relative_paths { + self.config.cwd_relative_to_repo_root.as_deref() + } else { + None + }, + ); self.plus_file = parsed_file_meta_line.0; self.painter .set_syntax(parse::get_file_extension_from_file_meta_line_file_path( diff --git a/src/options/set.rs b/src/options/set.rs index 2d112f87..6a1a7ad0 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -128,6 +128,7 @@ pub fn set_options( color_only, commit_decoration_style, commit_style, + diff_stat_align_width, file_added_label, file_copied_label, file_decoration_style, @@ -170,6 +171,7 @@ pub fn set_options( plus_empty_line_marker_style, plus_non_emph_style, raw, + relative_paths, show_themes, side_by_side, tab_width, diff --git a/src/parse.rs b/src/parse.rs index 4c2cf233..d811fb8a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -28,8 +28,12 @@ pub enum FileEvent { NoEvent, } -pub fn parse_file_meta_line(line: &str, git_diff_name: bool) -> (String, FileEvent) { - match line { +pub fn parse_file_meta_line( + line: &str, + git_diff_name: bool, + relative_path_base: Option<&str>, +) -> (String, FileEvent) { + let (mut path, file_event) = match line { line if line.starts_with("--- ") || line.starts_with("+++ ") => { let offset = 4; let file = match &line[offset..] { @@ -56,7 +60,47 @@ pub fn parse_file_meta_line(line: &str, git_diff_name: bool) -> (String, FileEve (line[8..].to_string(), FileEvent::Copy) // "copy to ".len() } _ => ("".to_string(), FileEvent::NoEvent), + }; + + if let Some(base) = relative_path_base { + if let Some(relative_path) = pathdiff::diff_paths(&path, base) { + if let Some(relative_path) = relative_path.to_str() { + path = relative_path.to_owned(); + } + } + } + + (path, file_event) +} + +// A regex to capture the path, and the content from the pipe onwards, in lines +// like these: +// " src/delta.rs | 14 ++++++++++----" +// " src/config.rs | 2 ++" +lazy_static! { + static ref DIFF_STAT_LINE_REGEX: Regex = + Regex::new(r" ([^\| ][^\|]+[^\| ]) +(\| +[0-9]+ .+)").unwrap(); +} + +pub fn relativize_path_in_diff_stat_line( + line: &str, + cwd_relative_to_repo_root: &str, + diff_stat_align_width: usize, +) -> Option<String> { + if let Some(caps) = DIFF_STAT_LINE_REGEX.captures(line) { + let path_relative_to_repo_root = caps.get(1).unwrap().as_str(); + if let Some(relative_path) = + pathdiff::diff_paths(path_relative_to_repo_root, cwd_relative_to_repo_root) + { + if let Some(relative_path) = relative_path.to_str() { + let suffix = caps.get(2).unwrap().as_str(); + let pad_width = diff_stat_align_width.saturating_sub(relative_path.len()); + let padding = " ".repeat(pad_width); + return Some(format!(" {}{}{}", relative_path, padding, suffix)); + } + } } + None } pub fn get_file_extension_from_file_meta_line_file_path(path: &str) -> Option<&str> { @@ -245,21 +289,21 @@ mod tests { #[test] fn test_get_file_path_from_git_file_meta_line() { assert_eq!( - parse_file_meta_line("--- /dev/null", true), + parse_file_meta_line("--- /dev/null", true, None), ("/dev/null".to_string(), FileEvent::Change) ); for prefix in &DIFF_PREFIXES { assert_eq!( - parse_file_meta_line(&format!("--- {}src/delta.rs", prefix), true), + parse_file_meta_line(&format!("--- {}src/delta.rs", prefix), true, None), ("src/delta.rs".to_string(), FileEvent::Change) ); } assert_eq!( - parse_file_meta_line("--- src/delta.rs", true), + parse_file_meta_line("--- src/delta.rs", true, None), ("src/delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_file_meta_line("+++ src/delta.rs", true), + parse_file_meta_line("+++ src/delta.rs", true, None), ("src/delta.rs".to_string(), FileEvent::Change) ); } @@ -267,23 +311,23 @@ mod tests { #[test] fn test_get_file_path_from_git_file_meta_line_containing_spaces() { assert_eq!( - parse_file_meta_line("+++ a/my src/delta.rs", true), + parse_file_meta_line("+++ a/my src/delta.rs", true, None), ("my src/delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_file_meta_line("+++ my src/delta.rs", true), + parse_file_meta_line("+++ my src/delta.rs", true, None), ("my src/delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_file_meta_line("+++ a/src/my delta.rs", true), + parse_file_meta_line("+++ a/src/my delta.rs", true, None), ("src/my delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_file_meta_line("+++ a/my src/my delta.rs", true), + parse_file_meta_line("+++ a/my src/my delta.rs", true, None), ("my src/my delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_file_meta_line("+++ b/my src/my enough/my delta.rs", true), + parse_file_meta_line("+++ b/my src/my enough/my delta.rs", true, None), ( "my src/my enough/my delta.rs".to_string(), FileEvent::Change @@ -294,7 +338,7 @@ mod tests { #[test] fn test_get_file_path_from_git_file_meta_line_rename() { assert_eq!( - parse_file_meta_line("rename from nospace/file2.el", true), + parse_file_meta_line("rename from nospace/file2.el", true, None), ("nospace/file2.el".to_string(), FileEvent::Rename) ); } @@ -302,7 +346,7 @@ mod tests { #[test] fn test_get_file_path_from_git_file_meta_line_rename_containing_spaces() { assert_eq!( - parse_file_meta_line("rename from with space/file1.el", true), + parse_file_meta_line("rename from with space/file1.el", true, None), ("with space/file1.el".to_string(), FileEvent::Rename) ); } @@ -310,11 +354,11 @@ mod tests { #[test] fn test_parse_file_meta_line() { assert_eq!( - parse_file_meta_line("--- src/delta.rs", false), + parse_file_meta_line("--- src/delta.rs", false, None), ("src/delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_file_meta_line("+++ src/delta.rs", false), + parse_file_meta_line("+++ src/delta.rs", false, None), ("src/delta.rs".to_string(), FileEvent::Change) ); } @@ -369,4 +413,38 @@ mod tests { assert_eq!(line_numbers_and_hunk_lengths[1], (358, 15),); assert_eq!(line_numbers_and_hunk_lengths[2], (358, 16),); } + + #[test] + fn test_relative_path() { + for (path, cwd_relative_to_repo_root, expected) in &[ + ("file.rs", "", "file.rs"), + ("file.rs", "a/", "../file.rs"), + ("a/file.rs", "a/", "file.rs"), + ("a/b/file.rs", "a", "b/file.rs"), + ("c/d/file.rs", "a/b/", "../../c/d/file.rs"), + ] { + assert_eq!( + pathdiff::diff_paths(path, cwd_relative_to_repo_root), + Some(expected.into()) + ) + } + } + + #[test] + fn test_diff_stat_line_regex_1() { + let caps = DIFF_STAT_LINE_REGEX.captures(" src/delta.rs | 14 ++++++++++----"); + assert!(caps.is_some()); + let caps = caps.unwrap(); + assert_eq!(caps.get(1).unwrap().as_str(), "src/delta.rs"); + assert_eq!(caps.get(2).unwrap().as_str(), "| 14 ++++++++++----"); + } + + #[test] + fn test_diff_stat_line_regex_2() { + let caps = DIFF_STAT_LINE_REGEX.captures(" src/config.rs | 2 ++"); + assert!(caps.is_some()); + let caps = caps.unwrap(); + assert_eq!(caps.get(1).unwrap().as_str(), "src/config.rs"); + assert_eq!(caps.get(2).unwrap().as_str(), "| 2 ++"); + } } |