Skip to content

Commit 97671f7

Browse files
authored
Merge pull request #565 from dandavison/file-regex-replacement
Output paths relative to current working directory
2 parents a15b122 + dc8a6fc commit 97671f7

File tree

7 files changed

+157
-19
lines changed

7 files changed

+157
-19
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ console = "0.14.1"
2525
dirs-next = "2.0.0"
2626
itertools = "0.10.0"
2727
lazy_static = "1.4"
28+
pathdiff = "0.2.0"
2829
regex = "1.4.6"
2930
shell-words = "1.0.0"
3031
structopt = "0.3.21"

src/cli.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ pub struct Opt {
231231
/// --file-renamed-label.
232232
pub navigate: bool,
233233

234+
#[structopt(long = "relative-paths")]
235+
/// Output all file paths relative to the current directory so that they
236+
/// resolve correctly when clicked on or used in shell commands.
237+
pub relative_paths: bool,
238+
234239
#[structopt(long = "hyperlinks")]
235240
/// Render commit hashes, file names, and line numbers as hyperlinks, according to the
236241
/// hyperlink spec for terminal emulators:
@@ -482,6 +487,11 @@ pub struct Opt {
482487
#[structopt(short = "w", long = "width")]
483488
pub width: Option<String>,
484489

490+
/// Width allocated for file paths in a diff stat section. If a relativized
491+
/// file path exceeds this width then the diff stat will be misaligned.
492+
#[structopt(long = "diff-stat-align-width", default_value = "48")]
493+
pub diff_stat_align_width: usize,
494+
485495
/// The number of spaces to replace tab characters with. Use --tabs=0 to pass tab characters
486496
/// through directly, but note that in that case delta will calculate line widths assuming tabs
487497
/// occupy one character's width on the screen: if your terminal renders tabs as more than than

src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ pub struct Config {
2323
pub background_color_extends_to_terminal_width: bool,
2424
pub commit_style: Style,
2525
pub color_only: bool,
26+
pub cwd_relative_to_repo_root: Option<String>,
2627
pub decorations_width: cli::Width,
28+
pub diff_stat_align_width: usize,
2729
pub error_exit_code: i32,
2830
pub file_added_label: String,
2931
pub file_copied_label: String,
@@ -70,6 +72,7 @@ pub struct Config {
7072
pub plus_style: Style,
7173
pub git_minus_style: Style,
7274
pub git_plus_style: Style,
75+
pub relative_paths: bool,
7376
pub show_themes: bool,
7477
pub side_by_side: bool,
7578
pub side_by_side_data: side_by_side::SideBySideData,
@@ -182,7 +185,9 @@ impl From<cli::Opt> for Config {
182185
.background_color_extends_to_terminal_width,
183186
commit_style,
184187
color_only: opt.color_only,
188+
cwd_relative_to_repo_root: std::env::var("GIT_PREFIX").ok(),
185189
decorations_width: opt.computed.decorations_width,
190+
diff_stat_align_width: opt.diff_stat_align_width,
186191
error_exit_code: 2, // Use 2 for error because diff uses 0 and 1 for non-error.
187192
file_added_label,
188193
file_copied_label,
@@ -235,6 +240,7 @@ impl From<cli::Opt> for Config {
235240
plus_style,
236241
git_minus_style,
237242
git_plus_style,
243+
relative_paths: opt.relative_paths,
238244
show_themes: opt.show_themes,
239245
side_by_side: opt.side_by_side,
240246
side_by_side_data,

src/delta.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ impl<'a> StateMachine<'a> {
113113

114114
let mut handled_line = if line.starts_with("commit ") {
115115
self.handle_commit_meta_header_line()?
116+
} else if self.state == State::CommitMeta && line.starts_with(' ') {
117+
self.handle_diff_stat_line()?
116118
} else if line.starts_with("diff ") {
117119
self.handle_file_meta_diff_line()?
118120
} else if (self.state == State::FileMeta || self.source == Source::DiffUnified)
@@ -226,6 +228,24 @@ impl<'a> StateMachine<'a> {
226228
Ok(())
227229
}
228230

231+
fn handle_diff_stat_line(&mut self) -> std::io::Result<bool> {
232+
let mut handled_line = false;
233+
if self.config.relative_paths {
234+
if let Some(cwd) = self.config.cwd_relative_to_repo_root.as_deref() {
235+
if let Some(replacement_line) = parse::relativize_path_in_diff_stat_line(
236+
&self.raw_line,
237+
cwd,
238+
self.config.diff_stat_align_width,
239+
) {
240+
self.painter.emit()?;
241+
writeln!(self.painter.writer, "{}", replacement_line)?;
242+
handled_line = true
243+
}
244+
}
245+
}
246+
Ok(handled_line)
247+
}
248+
229249
#[allow(clippy::unnecessary_wraps)]
230250
fn handle_file_meta_diff_line(&mut self) -> std::io::Result<bool> {
231251
self.painter.paint_buffered_minus_and_plus_lines();
@@ -237,8 +257,15 @@ impl<'a> StateMachine<'a> {
237257
fn handle_file_meta_minus_line(&mut self) -> std::io::Result<bool> {
238258
let mut handled_line = false;
239259

240-
let parsed_file_meta_line =
241-
parse::parse_file_meta_line(&self.line, self.source == Source::GitDiff);
260+
let parsed_file_meta_line = parse::parse_file_meta_line(
261+
&self.line,
262+
self.source == Source::GitDiff,
263+
if self.config.relative_paths {
264+
self.config.cwd_relative_to_repo_root.as_deref()
265+
} else {
266+
None
267+
},
268+
);
242269
self.minus_file = parsed_file_meta_line.0;
243270
self.file_event = parsed_file_meta_line.1;
244271

@@ -271,8 +298,15 @@ impl<'a> StateMachine<'a> {
271298

272299
fn handle_file_meta_plus_line(&mut self) -> std::io::Result<bool> {
273300
let mut handled_line = false;
274-
let parsed_file_meta_line =
275-
parse::parse_file_meta_line(&self.line, self.source == Source::GitDiff);
301+
let parsed_file_meta_line = parse::parse_file_meta_line(
302+
&self.line,
303+
self.source == Source::GitDiff,
304+
if self.config.relative_paths {
305+
self.config.cwd_relative_to_repo_root.as_deref()
306+
} else {
307+
None
308+
},
309+
);
276310
self.plus_file = parsed_file_meta_line.0;
277311
self.painter
278312
.set_syntax(parse::get_file_extension_from_file_meta_line_file_path(

src/options/set.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ pub fn set_options(
128128
color_only,
129129
commit_decoration_style,
130130
commit_style,
131+
diff_stat_align_width,
131132
file_added_label,
132133
file_copied_label,
133134
file_decoration_style,
@@ -170,6 +171,7 @@ pub fn set_options(
170171
plus_empty_line_marker_style,
171172
plus_non_emph_style,
172173
raw,
174+
relative_paths,
173175
show_themes,
174176
side_by_side,
175177
tab_width,

src/parse.rs

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ pub enum FileEvent {
2828
NoEvent,
2929
}
3030

31-
pub fn parse_file_meta_line(line: &str, git_diff_name: bool) -> (String, FileEvent) {
32-
match line {
31+
pub fn parse_file_meta_line(
32+
line: &str,
33+
git_diff_name: bool,
34+
relative_path_base: Option<&str>,
35+
) -> (String, FileEvent) {
36+
let (mut path, file_event) = match line {
3337
line if line.starts_with("--- ") || line.starts_with("+++ ") => {
3438
let offset = 4;
3539
let file = match &line[offset..] {
@@ -56,7 +60,47 @@ pub fn parse_file_meta_line(line: &str, git_diff_name: bool) -> (String, FileEve
5660
(line[8..].to_string(), FileEvent::Copy) // "copy to ".len()
5761
}
5862
_ => ("".to_string(), FileEvent::NoEvent),
63+
};
64+
65+
if let Some(base) = relative_path_base {
66+
if let Some(relative_path) = pathdiff::diff_paths(&path, base) {
67+
if let Some(relative_path) = relative_path.to_str() {
68+
path = relative_path.to_owned();
69+
}
70+
}
71+
}
72+
73+
(path, file_event)
74+
}
75+
76+
// A regex to capture the path, and the content from the pipe onwards, in lines
77+
// like these:
78+
// " src/delta.rs | 14 ++++++++++----"
79+
// " src/config.rs | 2 ++"
80+
lazy_static! {
81+
static ref DIFF_STAT_LINE_REGEX: Regex =
82+
Regex::new(r" ([^\| ][^\|]+[^\| ]) +(\| +[0-9]+ .+)").unwrap();
83+
}
84+
85+
pub fn relativize_path_in_diff_stat_line(
86+
line: &str,
87+
cwd_relative_to_repo_root: &str,
88+
diff_stat_align_width: usize,
89+
) -> Option<String> {
90+
if let Some(caps) = DIFF_STAT_LINE_REGEX.captures(line) {
91+
let path_relative_to_repo_root = caps.get(1).unwrap().as_str();
92+
if let Some(relative_path) =
93+
pathdiff::diff_paths(path_relative_to_repo_root, cwd_relative_to_repo_root)
94+
{
95+
if let Some(relative_path) = relative_path.to_str() {
96+
let suffix = caps.get(2).unwrap().as_str();
97+
let pad_width = diff_stat_align_width.saturating_sub(relative_path.len());
98+
let padding = " ".repeat(pad_width);
99+
return Some(format!(" {}{}{}", relative_path, padding, suffix));
100+
}
101+
}
59102
}
103+
None
60104
}
61105

62106
pub fn get_file_extension_from_file_meta_line_file_path(path: &str) -> Option<&str> {
@@ -245,45 +289,45 @@ mod tests {
245289
#[test]
246290
fn test_get_file_path_from_git_file_meta_line() {
247291
assert_eq!(
248-
parse_file_meta_line("--- /dev/null", true),
292+
parse_file_meta_line("--- /dev/null", true, None),
249293
("/dev/null".to_string(), FileEvent::Change)
250294
);
251295
for prefix in &DIFF_PREFIXES {
252296
assert_eq!(
253-
parse_file_meta_line(&format!("--- {}src/delta.rs", prefix), true),
297+
parse_file_meta_line(&format!("--- {}src/delta.rs", prefix), true, None),
254298
("src/delta.rs".to_string(), FileEvent::Change)
255299
);
256300
}
257301
assert_eq!(
258-
parse_file_meta_line("--- src/delta.rs", true),
302+
parse_file_meta_line("--- src/delta.rs", true, None),
259303
("src/delta.rs".to_string(), FileEvent::Change)
260304
);
261305
assert_eq!(
262-
parse_file_meta_line("+++ src/delta.rs", true),
306+
parse_file_meta_line("+++ src/delta.rs", true, None),
263307
("src/delta.rs".to_string(), FileEvent::Change)
264308
);
265309
}
266310

267311
#[test]
268312
fn test_get_file_path_from_git_file_meta_line_containing_spaces() {
269313
assert_eq!(
270-
parse_file_meta_line("+++ a/my src/delta.rs", true),
314+
parse_file_meta_line("+++ a/my src/delta.rs", true, None),
271315
("my src/delta.rs".to_string(), FileEvent::Change)
272316
);
273317
assert_eq!(
274-
parse_file_meta_line("+++ my src/delta.rs", true),
318+
parse_file_meta_line("+++ my src/delta.rs", true, None),
275319
("my src/delta.rs".to_string(), FileEvent::Change)
276320
);
277321
assert_eq!(
278-
parse_file_meta_line("+++ a/src/my delta.rs", true),
322+
parse_file_meta_line("+++ a/src/my delta.rs", true, None),
279323
("src/my delta.rs".to_string(), FileEvent::Change)
280324
);
281325
assert_eq!(
282-
parse_file_meta_line("+++ a/my src/my delta.rs", true),
326+
parse_file_meta_line("+++ a/my src/my delta.rs", true, None),
283327
("my src/my delta.rs".to_string(), FileEvent::Change)
284328
);
285329
assert_eq!(
286-
parse_file_meta_line("+++ b/my src/my enough/my delta.rs", true),
330+
parse_file_meta_line("+++ b/my src/my enough/my delta.rs", true, None),
287331
(
288332
"my src/my enough/my delta.rs".to_string(),
289333
FileEvent::Change
@@ -294,27 +338,27 @@ mod tests {
294338
#[test]
295339
fn test_get_file_path_from_git_file_meta_line_rename() {
296340
assert_eq!(
297-
parse_file_meta_line("rename from nospace/file2.el", true),
341+
parse_file_meta_line("rename from nospace/file2.el", true, None),
298342
("nospace/file2.el".to_string(), FileEvent::Rename)
299343
);
300344
}
301345

302346
#[test]
303347
fn test_get_file_path_from_git_file_meta_line_rename_containing_spaces() {
304348
assert_eq!(
305-
parse_file_meta_line("rename from with space/file1.el", true),
349+
parse_file_meta_line("rename from with space/file1.el", true, None),
306350
("with space/file1.el".to_string(), FileEvent::Rename)
307351
);
308352
}
309353

310354
#[test]
311355
fn test_parse_file_meta_line() {
312356
assert_eq!(
313-
parse_file_meta_line("--- src/delta.rs", false),
357+
parse_file_meta_line("--- src/delta.rs", false, None),
314358
("src/delta.rs".to_string(), FileEvent::Change)
315359
);
316360
assert_eq!(
317-
parse_file_meta_line("+++ src/delta.rs", false),
361+
parse_file_meta_line("+++ src/delta.rs", false, None),
318362
("src/delta.rs".to_string(), FileEvent::Change)
319363
);
320364
}
@@ -369,4 +413,38 @@ mod tests {
369413
assert_eq!(line_numbers_and_hunk_lengths[1], (358, 15),);
370414
assert_eq!(line_numbers_and_hunk_lengths[2], (358, 16),);
371415
}
416+
417+
#[test]
418+
fn test_relative_path() {
419+
for (path, cwd_relative_to_repo_root, expected) in &[
420+
("file.rs", "", "file.rs"),
421+
("file.rs", "a/", "../file.rs"),
422+
("a/file.rs", "a/", "file.rs"),
423+
("a/b/file.rs", "a", "b/file.rs"),
424+
("c/d/file.rs", "a/b/", "../../c/d/file.rs"),
425+
] {
426+
assert_eq!(
427+
pathdiff::diff_paths(path, cwd_relative_to_repo_root),
428+
Some(expected.into())
429+
)
430+
}
431+
}
432+
433+
#[test]
434+
fn test_diff_stat_line_regex_1() {
435+
let caps = DIFF_STAT_LINE_REGEX.captures(" src/delta.rs | 14 ++++++++++----");
436+
assert!(caps.is_some());
437+
let caps = caps.unwrap();
438+
assert_eq!(caps.get(1).unwrap().as_str(), "src/delta.rs");
439+
assert_eq!(caps.get(2).unwrap().as_str(), "| 14 ++++++++++----");
440+
}
441+
442+
#[test]
443+
fn test_diff_stat_line_regex_2() {
444+
let caps = DIFF_STAT_LINE_REGEX.captures(" src/config.rs | 2 ++");
445+
assert!(caps.is_some());
446+
let caps = caps.unwrap();
447+
assert_eq!(caps.get(1).unwrap().as_str(), "src/config.rs");
448+
assert_eq!(caps.get(2).unwrap().as_str(), "| 2 ++");
449+
}
372450
}

0 commit comments

Comments
 (0)