From 76a0345c1fdfde5e6c228818e00f7e5e3a4e5597 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 19 Dec 2021 16:17:27 -0500 Subject: [PATCH] New option file-transformation to transform file paths --- src/cli.rs | 4 ++ src/config.rs | 7 +++ src/options/set.rs | 3 + src/paint.rs | 6 ++ src/utils/mod.rs | 1 + src/utils/regex_replacement.rs | 112 +++++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+) create mode 100644 src/utils/regex_replacement.rs diff --git a/src/cli.rs b/src/cli.rs index 569be55f7..ed1698092 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -392,6 +392,10 @@ pub struct Opt { /// (overline), or the combination 'ul ol'. pub file_decoration_style: String, + #[structopt(long = "file-transformation")] + /// A sed-style command specifying how file paths should be transformed for display. + pub file_regex_replacement: Option, + /// Format string for commit hyperlinks (requires --hyperlinks). The /// placeholder "{commit}" will be replaced by the commit hash. For example: /// --hyperlinks-commit-link-format='https://mygitrepo/{commit}/' diff --git a/src/config.rs b/src/config.rs index 1eb877860..1b8e759bd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,6 +25,7 @@ use crate::style; use crate::style::Style; use crate::tests::TESTING; use crate::utils::bat::output::PagingMode; +use crate::utils::regex_replacement::RegexReplacement; use crate::utils::syntect::FromDeltaStyle; use crate::wrapping::WrapConfig; @@ -80,6 +81,7 @@ pub struct Config { pub file_modified_label: String, pub file_removed_label: String, pub file_renamed_label: String, + pub file_regex_replacement: Option, pub right_arrow: String, pub file_style: Style, pub git_config_entries: HashMap, @@ -263,6 +265,11 @@ impl From for Config { file_modified_label, file_removed_label, file_renamed_label, + file_regex_replacement: opt + .file_regex_replacement + .as_deref() + .map(RegexReplacement::from_sed_command) + .flatten(), right_arrow, hunk_label, file_style: styles["file-style"], diff --git a/src/options/set.rs b/src/options/set.rs index be3d74a18..09327a0bb 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -143,6 +143,7 @@ pub fn set_options( file_modified_label, file_removed_label, file_renamed_label, + file_regex_replacement, right_arrow, hunk_label, file_style, @@ -704,6 +705,7 @@ pub mod tests { file-modified-label = xxxyyyzzz file-removed-label = xxxyyyzzz file-renamed-label = xxxyyyzzz + file-transformation = s/foo/bar/ right-arrow = xxxyyyzzz file-style = black black hunk-header-decoration-style = black black @@ -771,6 +773,7 @@ pub mod tests { assert_eq!(opt.file_renamed_label, "xxxyyyzzz"); assert_eq!(opt.right_arrow, "xxxyyyzzz"); assert_eq!(opt.file_style, "black black"); + assert_eq!(opt.file_regex_replacement, Some("s/foo/bar/".to_string())); assert_eq!(opt.hunk_header_decoration_style, "black black"); assert_eq!(opt.hunk_header_style, "black black"); assert_eq!(opt.keep_plus_minus_markers, true); diff --git a/src/paint.rs b/src/paint.rs index 6d00cfc57..ed9185a09 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::collections::HashMap; use std::io::Write; @@ -784,6 +785,11 @@ pub fn paint_file_path_with_line_number( ) -> String { let mut file_with_line_number = Vec::new(); if let Some(file_style) = file_style { + let plus_file = if let Some(regex_replacement) = &config.file_regex_replacement { + regex_replacement.execute(plus_file) + } else { + Cow::from(plus_file) + }; file_with_line_number.push(file_style.paint(plus_file)) }; if let Some(line_number) = line_number { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 70026dd3e..46ea99067 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,5 @@ #[cfg(not(tarpaulin_include))] pub mod bat; pub mod process; +pub mod regex_replacement; pub mod syntect; diff --git a/src/utils/regex_replacement.rs b/src/utils/regex_replacement.rs new file mode 100644 index 000000000..5f07902e6 --- /dev/null +++ b/src/utils/regex_replacement.rs @@ -0,0 +1,112 @@ +use std::borrow::Cow; + +use regex::{Regex, RegexBuilder}; + +#[derive(Clone, Debug)] +pub struct RegexReplacement { + regex: Regex, + replacement: String, + replace_all: bool, +} + +impl RegexReplacement { + pub fn from_sed_command(sed_command: &str) -> Option { + let sep = sed_command.chars().nth(1)?; + let mut parts = sed_command[2..].split(sep); + let regex = parts.next()?; + let replacement = parts.next()?.to_string(); + let flags = parts.next()?; + let mut re_builder = RegexBuilder::new(regex); + let mut replace_all = false; + for flag in flags.chars() { + match flag { + 'g' => { + replace_all = true; + } + 'i' => { + re_builder.case_insensitive(true); + } + 'm' => { + re_builder.multi_line(true); + } + 's' => { + re_builder.dot_matches_new_line(true); + } + 'U' => { + re_builder.swap_greed(true); + } + 'x' => { + re_builder.ignore_whitespace(true); + } + _ => {} + } + } + let regex = re_builder.build().ok()?; + Some(RegexReplacement { + regex, + replacement, + replace_all, + }) + } + + pub fn execute<'t>(&self, s: &'t str) -> Cow<'t, str> { + if self.replace_all { + self.regex.replace_all(s, &self.replacement) + } else { + self.regex.replace(s, &self.replacement) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sed_command() { + let command = "s,foo,bar,"; + let rr = RegexReplacement::from_sed_command(command).unwrap(); + assert_eq!(rr.regex.as_str(), "foo"); + assert_eq!(rr.replacement, "bar"); + assert_eq!(rr.replace_all, false); + assert_eq!(rr.execute("foo"), "bar"); + } + + #[test] + fn test_sed_command_i_flag() { + let command = "s,FOO,bar,"; + let rr = RegexReplacement::from_sed_command(command).unwrap(); + assert_eq!(rr.execute("foo"), "foo"); + let command = "s,FOO,bar,i"; + let rr = RegexReplacement::from_sed_command(command).unwrap(); + assert_eq!(rr.execute("foo"), "bar"); + } + + #[test] + fn test_sed_command_g_flag() { + let command = "s,foo,bar,"; + let rr = RegexReplacement::from_sed_command(command).unwrap(); + assert_eq!(rr.execute("foofoo"), "barfoo"); + let command = "s,foo,bar,g"; + let rr = RegexReplacement::from_sed_command(command).unwrap(); + assert_eq!(rr.execute("foofoo"), "barbar"); + } + + #[test] + fn test_sed_command_with_named_captures() { + let command = r"s/(?P[^,\s]+),\s+(?P\S+)/$first $last/"; + let rr = RegexReplacement::from_sed_command(command).unwrap(); + assert_eq!(rr.execute("Springsteen, Bruce"), "Bruce Springsteen"); + } + + #[test] + fn test_sed_command_invalid() { + assert!(RegexReplacement::from_sed_command("").is_none()); + assert!(RegexReplacement::from_sed_command("s").is_none()); + assert!(RegexReplacement::from_sed_command("s,").is_none()); + assert!(RegexReplacement::from_sed_command("s,,").is_none()); + assert!(RegexReplacement::from_sed_command("s,,i").is_none()); + assert!(RegexReplacement::from_sed_command("s,,,").is_some()); + assert!(RegexReplacement::from_sed_command("s,,,i").is_some()); + } +}