diff --git a/src/config.rs b/src/config.rs index 62edb3b35..e06d84af5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,8 +25,8 @@ use crate::parse_styles; use crate::style; use crate::style::Style; use crate::tests::TESTING; +use crate::utils; use crate::utils::bat::output::PagingMode; -use crate::utils::cwd::cwd_of_user_shell_process; use crate::utils::regex_replacement::RegexReplacement; use crate::utils::syntect::FromDeltaStyle; use crate::wrapping::WrapConfig; @@ -245,9 +245,14 @@ impl From for Config { let wrap_max_lines_plus1 = adapt_wrap_max_lines_argument(opt.wrap_max_lines); + #[cfg(not(test))] let cwd_of_delta_process = std::env::current_dir().ok(); + #[cfg(test)] + let cwd_of_delta_process = Some(utils::path::fake_delta_cwd_for_tests()); + let cwd_relative_to_repo_root = std::env::var("GIT_PREFIX").ok(); - let cwd_of_user_shell_process = cwd_of_user_shell_process( + + let cwd_of_user_shell_process = utils::path::cwd_of_user_shell_process( cwd_of_delta_process.as_ref(), cwd_relative_to_repo_root.as_deref(), ); diff --git a/src/features/hyperlinks.rs b/src/features/hyperlinks.rs index 2fad166fd..a26c8c233 100644 --- a/src/features/hyperlinks.rs +++ b/src/features/hyperlinks.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::path::Path; use std::str::FromStr; use lazy_static::lazy_static; @@ -7,6 +8,7 @@ use regex::{Captures, Regex}; use crate::config::Config; use crate::features::OptionValueFunction; use crate::git_config::{GitConfig, GitConfigEntry, GitRemoteRepo}; + pub fn make_feature() -> Vec<(String, OptionValueFunction)> { builtin_feature!([ ( @@ -52,27 +54,27 @@ fn get_remote_url(git_config: &GitConfig) -> Option { }) } -/// Create a file hyperlink to `path`, displaying `text`. -pub fn format_osc8_file_hyperlink<'a>( - relative_path: &'a str, +/// Create a file hyperlink, displaying `text`. +pub fn format_osc8_file_hyperlink<'a, P>( + absolute_path: P, line_number: Option, text: &str, config: &Config, -) -> Cow<'a, str> { - if let Some(cwd) = &config.cwd_of_user_shell_process { - let absolute_path = cwd.join(relative_path); - let mut url = config - .hyperlinks_file_link_format - .replace("{path}", &absolute_path.to_string_lossy()); - if let Some(n) = line_number { - url = url.replace("{line}", &format!("{}", n)) - } else { - url = url.replace("{line}", "") - }; - Cow::from(format_osc8_hyperlink(&url, text)) +) -> Cow<'a, str> +where + P: AsRef, + P: std::fmt::Debug, +{ + debug_assert!(absolute_path.as_ref().is_absolute()); + let mut url = config + .hyperlinks_file_link_format + .replace("{path}", &absolute_path.as_ref().to_string_lossy()); + if let Some(n) = line_number { + url = url.replace("{line}", &format!("{}", n)) } else { - Cow::from(relative_path) - } + url = url.replace("{line}", "") + }; + Cow::from(format_osc8_hyperlink(&url, text)) } fn format_osc8_hyperlink(url: &str, text: &str) -> String { @@ -109,57 +111,367 @@ fn format_github_commit_url(commit: &str, github_repo: &str) -> String { format!("https://github.com/{}/commit/{}", github_repo, commit) } +#[cfg(not(target_os = "windows"))] #[cfg(test)] pub mod tests { - #[cfg(not(target_os = "windows"))] - pub mod unix { - use std::path::PathBuf; - - use super::super::*; - use crate::tests::integration_test_utils; - - fn assert_file_hyperlink_matches( - relative_path: &str, - expected_hyperlink_path: &str, - config: &Config, - ) { - let link_text = "link text"; - assert_eq!( - format_osc8_hyperlink( - &PathBuf::from(expected_hyperlink_path).to_string_lossy(), - link_text + use std::iter::FromIterator; + use std::path::PathBuf; + + use super::*; + use crate::{ + tests::integration_test_utils::{self, DeltaTest}, + utils, + }; + + #[test] + fn test_paths_and_hyperlinks_user_in_repo_root_dir() { + // Expectations are uninfluenced by git's --relative and delta's relative_paths options. + let input_type = InputType::GitDiff; + let true_location_of_file_relative_to_repo_root = PathBuf::from("a"); + let git_prefix_env_var = Some(""); + + for (delta_relative_paths_option, calling_cmd) in vec![ + (false, Some("git diff")), + (false, Some("git diff --relative")), + (true, Some("git diff")), + (true, Some("git diff --relative")), + ] { + run_test(FilePathsTestCase { + name: &format!( + "delta relative_paths={} calling_cmd={:?}", + delta_relative_paths_option, calling_cmd ), - format_osc8_file_hyperlink(relative_path, None, link_text, config) - ) + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var, + delta_relative_paths_option, + input_type, + calling_cmd, + path_in_delta_input: "a", + expected_displayed_path: "a", + }) } + } + + #[test] + fn test_paths_and_hyperlinks_user_in_subdir_file_in_same_subdir() { + let input_type = InputType::GitDiff; + let true_location_of_file_relative_to_repo_root = PathBuf::from_iter(&["b", "a"]); + let git_prefix_env_var = Some("b"); + + run_test(FilePathsTestCase { + name: "b/a from b", + input_type, + calling_cmd: Some("git diff"), + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var, + delta_relative_paths_option: false, + path_in_delta_input: "b/a", + expected_displayed_path: "b/a", + }); + run_test(FilePathsTestCase { + name: "b/a from b", + input_type, + calling_cmd: Some("git diff --relative"), + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var, + delta_relative_paths_option: false, + path_in_delta_input: "a", + // delta saw a and wasn't configured to make any changes + expected_displayed_path: "a", + }); + run_test(FilePathsTestCase { + name: "b/a from b", + input_type, + calling_cmd: Some("git diff"), + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var, + delta_relative_paths_option: true, + path_in_delta_input: "b/a", + // delta saw b/a and changed it to a + expected_displayed_path: "a", + }); + run_test(FilePathsTestCase { + name: "b/a from b", + input_type, + calling_cmd: Some("git diff --relative"), + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var, + delta_relative_paths_option: true, + path_in_delta_input: "a", + // delta saw a and didn't change it + expected_displayed_path: "a", + }); + } + + #[test] + fn test_paths_and_hyperlinks_user_in_subdir_file_in_different_subdir() { + let input_type = InputType::GitDiff; + let true_location_of_file_relative_to_repo_root = PathBuf::from_iter(&["b", "a"]); + let git_prefix_env_var = Some("c"); + + run_test(FilePathsTestCase { + name: "b/a from c", + input_type, + calling_cmd: Some("git diff"), + delta_relative_paths_option: false, + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var, + path_in_delta_input: "b/a", + expected_displayed_path: "b/a", + }); + run_test(FilePathsTestCase { + name: "b/a from c", + input_type, + calling_cmd: Some("git diff --relative"), + delta_relative_paths_option: false, + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var, + path_in_delta_input: "../b/a", + expected_displayed_path: "../b/a", + }); + run_test(FilePathsTestCase { + name: "b/a from c", + input_type, + calling_cmd: Some("git diff"), + delta_relative_paths_option: true, + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var, + path_in_delta_input: "b/a", + expected_displayed_path: "../b/a", + }); + } + + #[test] + fn test_paths_and_hyperlinks_git_grep_user_in_root() { + let input_type = InputType::Grep; + let true_location_of_file_relative_to_repo_root = PathBuf::from_iter(&["b", "a.txt"]); + + run_test(FilePathsTestCase { + name: "git grep: b/a.txt from root dir", + input_type, + calling_cmd: Some("git grep foo"), + delta_relative_paths_option: false, + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var: Some(""), + path_in_delta_input: "b/a.txt", + expected_displayed_path: "b/a.txt:", + }); + } + + #[test] + fn test_paths_and_hyperlinks_grep_user_in_subdir_file_in_same_subdir() { + _run_test_grep_user_in_subdir_file_in_same_subdir(Some("git grep foo")); + _run_test_grep_user_in_subdir_file_in_same_subdir(Some("rg foo")); + } + + fn _run_test_grep_user_in_subdir_file_in_same_subdir(calling_cmd: Option<&str>) { + let input_type = InputType::Grep; + let true_location_of_file_relative_to_repo_root = PathBuf::from_iter(&["b", "a.txt"]); + run_test(FilePathsTestCase { + name: "git grep: b/a.txt from b/ dir", + input_type, + calling_cmd, + delta_relative_paths_option: false, + true_location_of_file_relative_to_repo_root: + true_location_of_file_relative_to_repo_root.as_path(), + git_prefix_env_var: Some("b/"), + path_in_delta_input: "a.txt", + expected_displayed_path: "a.txt:", + }); + } + + const GIT_DIFF_OUTPUT: &str = r#" +diff --git a/__path__ b/__path__ +index 587be6b..975fbec 100644 +--- a/__path__ ++++ b/__path__ +@@ -1 +1 @@ +-x ++y + "#; - #[test] - fn test_relative_path_file_hyperlink_when_not_child_process_of_git() { - // The current process is not a child process of git. - // Delta receives a file path 'a'. - // The hyperlink should be $cwd/a. - let mut config = integration_test_utils::make_config_from_args(&[ - "--hyperlinks", - "--hyperlinks-file-link-format", - "{path}", - ]); - config.cwd_of_user_shell_process = Some(PathBuf::from("/some/cwd")); - assert_file_hyperlink_matches("a", "/some/cwd/a", &config) + const GIT_GREP_OUTPUT: &str = "\ +__path__: some matching line +"; + + struct FilePathsTestCase<'a> { + // True location of file in repo + true_location_of_file_relative_to_repo_root: &'a Path, + + // Git spawns delta from repo root, and stores in this env var the cwd in which the user invoked delta. + git_prefix_env_var: Option<&'a str>, + + delta_relative_paths_option: bool, + input_type: InputType, + calling_cmd: Option<&'a str>, + path_in_delta_input: &'a str, + expected_displayed_path: &'a str, + #[allow(dead_code)] + name: &'a str, + } + + #[derive(Debug)] + enum CallingProcess { + GitDiff(bool), + GitGrep, + OtherGrep, + } + + #[derive(Clone, Copy, Debug)] + enum InputType { + GitDiff, + Grep, + } + + impl<'a> FilePathsTestCase<'a> { + pub fn get_args(&self) -> Vec { + let mut args = vec![ + "--navigate".to_string(), // helps locate the file path in the output + "--hyperlinks".to_string(), + "--hyperlinks-file-link-format".to_string(), + "{path}".to_string(), + "--grep-file-style".to_string(), + "raw".to_string(), + "--grep-line-number-style".to_string(), + "raw".to_string(), + ]; + if self.delta_relative_paths_option { + args.push("--relative-paths".to_string()); + } + args } - #[test] - fn test_relative_path_file_hyperlink_when_child_process_of_git() { - // The current process is a child process of git. - // Delta receives a file path 'a'. - // We are in directory b/ relative to the repo root. - // The hyperlink should be $repo_root/b/a. - let mut config = integration_test_utils::make_config_from_args(&[ - "--hyperlinks", - "--hyperlinks-file-link-format", - "{path}", - ]); - config.cwd_of_user_shell_process = Some(PathBuf::from("/some/repo-root/b")); - assert_file_hyperlink_matches("a", "/some/repo-root/b/a", &config) + pub fn calling_process(&self) -> CallingProcess { + match (&self.input_type, self.calling_cmd) { + (InputType::GitDiff, Some(s)) if s.starts_with("git diff --relative") => { + CallingProcess::GitDiff(true) + } + (InputType::GitDiff, Some(s)) if s.starts_with("git diff") => { + CallingProcess::GitDiff(false) + } + (InputType::Grep, Some(s)) if s.starts_with("git grep") => CallingProcess::GitGrep, + (InputType::Grep, Some(s)) if s.starts_with("rg") => CallingProcess::OtherGrep, + (InputType::Grep, None) => CallingProcess::GitGrep, + _ => panic!( + "Unexpected calling spec: {:?} {:?}", + self.input_type, self.calling_cmd + ), + } } + + pub fn path_in_git_output(&self) -> String { + match self.calling_process() { + CallingProcess::GitDiff(false) => self + .true_location_of_file_relative_to_repo_root + .to_string_lossy() + .to_string(), + CallingProcess::GitDiff(true) => pathdiff::diff_paths( + self.true_location_of_file_relative_to_repo_root, + self.git_prefix_env_var.unwrap(), + ) + .unwrap() + .to_string_lossy() + .into(), + _ => panic!("Unexpected calling process: {:?}", self.calling_process()), + } + } + + /// Return the relative path as it would appear in grep output, i.e. accounting for facts + /// such as that that the user may have invoked the grep command from a non-root directory + /// in the repo. + pub fn path_in_grep_output(&self) -> String { + use CallingProcess::*; + match (self.calling_process(), self.git_prefix_env_var) { + (GitGrep, None) => self + .true_location_of_file_relative_to_repo_root + .to_string_lossy() + .into(), + (GitGrep, Some(dir)) => { + // Delta must have been invoked as core.pager since GIT_PREFIX env var is set. + // Note that it is possible that `true_location_of_file_relative_to_repo_root` + // is not under `git_prefix_env_var` since one can do things like `git grep foo + // ..` + pathdiff::diff_paths(self.true_location_of_file_relative_to_repo_root, dir) + .unwrap() + .to_string_lossy() + .into() + } + (OtherGrep, None) => { + // Output from e.g. rg has been piped to delta. + // Therefore + // (a) the cwd that the delta process reports is the user's shell process cwd + // (b) the file in question must be under this cwd + // (c) grep output will contain the path relative to this cwd + + // So to compute the path as it would appear in grep output, we could form the + // absolute path to the file and strip off the config.cwd_of_delta_process + // prefix. The absolute path to the file could be constructed as (absolute path + // to repo root) + true_location_of_file_relative_to_repo_root). But I don't + // think we know the absolute path to repo root. + panic!("Not implemented") + } + _ => panic!("Not implemented"), + } + } + + pub fn expected_hyperlink_path(&self) -> PathBuf { + utils::path::fake_delta_cwd_for_tests() + .join(self.true_location_of_file_relative_to_repo_root) + } + } + + fn run_test(test_case: FilePathsTestCase) { + let mut config = integration_test_utils::make_config_from_args( + &test_case + .get_args() + .iter() + .map(|s| s.as_str()) + .collect::>() + .as_slice(), + ); + // The test is simulating delta invoked by git hence these are the same + config.cwd_relative_to_repo_root = test_case.git_prefix_env_var.map(|s| s.to_string()); + config.cwd_of_user_shell_process = utils::path::cwd_of_user_shell_process( + config.cwd_of_delta_process.as_ref(), + config.cwd_relative_to_repo_root.as_deref(), + ); + let mut delta_test = DeltaTest::with_config(&config); + if let Some(cmd) = test_case.calling_cmd { + delta_test = delta_test.with_calling_process(cmd) + } + let delta_test = match test_case.calling_process() { + CallingProcess::GitDiff(_) => { + assert_eq!( + test_case.path_in_delta_input, + test_case.path_in_git_output() + ); + delta_test + .with_input(&GIT_DIFF_OUTPUT.replace("__path__", test_case.path_in_delta_input)) + } + CallingProcess::GitGrep => { + assert_eq!( + test_case.path_in_delta_input, + test_case.path_in_grep_output() + ); + delta_test.with_input( + &GIT_GREP_OUTPUT.replace("__path__", &test_case.path_in_delta_input), + ) + } + CallingProcess::OtherGrep => delta_test + .with_input(&GIT_GREP_OUTPUT.replace("__path__", &test_case.path_in_delta_input)), + }; + delta_test.expect_raw_contains(&format_osc8_hyperlink( + &PathBuf::from(test_case.expected_hyperlink_path()).to_string_lossy(), + test_case.expected_displayed_path, + )); } } diff --git a/src/handlers/diff_header.rs b/src/handlers/diff_header.rs index e3b29a34b..9b2205b83 100644 --- a/src/handlers/diff_header.rs +++ b/src/handlers/diff_header.rs @@ -6,8 +6,8 @@ use unicode_segmentation::UnicodeSegmentation; use super::draw; use crate::config::Config; use crate::delta::{DiffType, Source, State, StateMachine}; -use crate::features; use crate::paint::Painter; +use crate::{features, utils}; // https://git-scm.com/docs/git-config#Documentation/git-config.txt-diffmnemonicPrefix const DIFF_PREFIXES: [&str; 6] = ["a/", "b/", "c/", "i/", "o/", "w/"]; @@ -64,16 +64,12 @@ impl<'a> StateMachine<'a> { } let mut handled_line = false; - let (path_or_mode, file_event) = parse_diff_header_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 = path_or_mode; + let (path_or_mode, file_event) = + parse_diff_header_line(&self.line, self.source == Source::GitDiff); + + self.minus_file = utils::path::relativize_path_maybe(&path_or_mode, self.config) + .map(|p| p.to_string_lossy().to_owned().to_string()) + .unwrap_or(path_or_mode); self.minus_file_event = file_event; if self.source == Source::DiffUnified { @@ -118,16 +114,12 @@ impl<'a> StateMachine<'a> { return Ok(false); } let mut handled_line = false; - let (path_or_mode, file_event) = parse_diff_header_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 = path_or_mode; + let (path_or_mode, file_event) = + parse_diff_header_line(&self.line, self.source == Source::GitDiff); + + self.plus_file = utils::path::relativize_path_maybe(&path_or_mode, self.config) + .map(|p| p.to_string_lossy().to_owned().to_string()) + .unwrap_or(path_or_mode); self.plus_file_event = file_event; self.painter .set_syntax(get_file_extension_from_diff_header_line_file_path( @@ -274,12 +266,8 @@ pub fn get_extension(s: &str) -> Option<&str> { .or_else(|| path.file_name().and_then(|s| s.to_str())) } -fn parse_diff_header_line( - line: &str, - git_diff_name: bool, - relative_path_base: Option<&str>, -) -> (String, FileEvent) { - let (mut path_or_mode, file_event) = match line { +fn parse_diff_header_line(line: &str, git_diff_name: bool) -> (String, FileEvent) { + match line { line if line.starts_with("--- ") || line.starts_with("+++ ") => { let offset = 4; let file = _parse_file_path(&line[offset..], git_diff_name); @@ -298,17 +286,7 @@ fn parse_diff_header_line( (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_or_mode, base) { - if let Some(relative_path) = relative_path.to_str() { - path_or_mode = relative_path.to_owned(); - } - } } - - (path_or_mode, file_event) } /// Given input like "diff --git a/src/my file.rs b/src/my file.rs" @@ -369,12 +347,12 @@ pub fn get_file_change_description_from_file_paths( plus_file ) } else { - let format_file = |file| { - if config.hyperlinks { - features::hyperlinks::format_osc8_file_hyperlink(file, None, file, config) - } else { - Cow::from(file) + let format_file = |file| match (config.hyperlinks, utils::path::absolute_path(file, config)) + { + (true, Some(absolute_path)) => { + features::hyperlinks::format_osc8_file_hyperlink(absolute_path, None, file, config) } + _ => Cow::from(file), }; match (minus_file, plus_file, minus_file_event, plus_file_event) { (minus_file, plus_file, _, _) if minus_file == plus_file => format!( @@ -479,21 +457,21 @@ mod tests { #[test] fn test_get_file_path_from_git_diff_header_line() { assert_eq!( - parse_diff_header_line("--- /dev/null", true, None), + parse_diff_header_line("--- /dev/null", true), ("/dev/null".to_string(), FileEvent::Change) ); for prefix in &DIFF_PREFIXES { assert_eq!( - parse_diff_header_line(&format!("--- {}src/delta.rs", prefix), true, None), + parse_diff_header_line(&format!("--- {}src/delta.rs", prefix), true), ("src/delta.rs".to_string(), FileEvent::Change) ); } assert_eq!( - parse_diff_header_line("--- src/delta.rs", true, None), + parse_diff_header_line("--- src/delta.rs", true), ("src/delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_diff_header_line("+++ src/delta.rs", true, None), + parse_diff_header_line("+++ src/delta.rs", true), ("src/delta.rs".to_string(), FileEvent::Change) ); } @@ -501,23 +479,23 @@ mod tests { #[test] fn test_get_file_path_from_git_diff_header_line_containing_spaces() { assert_eq!( - parse_diff_header_line("+++ a/my src/delta.rs", true, None), + parse_diff_header_line("+++ a/my src/delta.rs", true), ("my src/delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_diff_header_line("+++ my src/delta.rs", true, None), + parse_diff_header_line("+++ my src/delta.rs", true), ("my src/delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_diff_header_line("+++ a/src/my delta.rs", true, None), + parse_diff_header_line("+++ a/src/my delta.rs", true), ("src/my delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_diff_header_line("+++ a/my src/my delta.rs", true, None), + parse_diff_header_line("+++ a/my src/my delta.rs", true), ("my src/my delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_diff_header_line("+++ b/my src/my enough/my delta.rs", true, None), + parse_diff_header_line("+++ b/my src/my enough/my delta.rs", true), ( "my src/my enough/my delta.rs".to_string(), FileEvent::Change @@ -528,7 +506,7 @@ mod tests { #[test] fn test_get_file_path_from_git_diff_header_line_rename() { assert_eq!( - parse_diff_header_line("rename from nospace/file2.el", true, None), + parse_diff_header_line("rename from nospace/file2.el", true), ("nospace/file2.el".to_string(), FileEvent::Rename) ); } @@ -536,7 +514,7 @@ mod tests { #[test] fn test_get_file_path_from_git_diff_header_line_rename_containing_spaces() { assert_eq!( - parse_diff_header_line("rename from with space/file1.el", true, None), + parse_diff_header_line("rename from with space/file1.el", true), ("with space/file1.el".to_string(), FileEvent::Rename) ); } @@ -544,11 +522,11 @@ mod tests { #[test] fn test_parse_diff_header_line() { assert_eq!( - parse_diff_header_line("--- src/delta.rs", false, None), + parse_diff_header_line("--- src/delta.rs", false), ("src/delta.rs".to_string(), FileEvent::Change) ); assert_eq!( - parse_diff_header_line("+++ src/delta.rs", false, None), + parse_diff_header_line("+++ src/delta.rs", false), ("src/delta.rs".to_string(), FileEvent::Change) ); } diff --git a/src/handlers/hunk_header.rs b/src/handlers/hunk_header.rs index b52ba5b42..7a9e2a1c5 100644 --- a/src/handlers/hunk_header.rs +++ b/src/handlers/hunk_header.rs @@ -399,8 +399,11 @@ pub mod tests { } #[test] - #[cfg(not(target_os = "windows"))] fn test_paint_file_path_with_line_number_hyperlinks() { + use std::{iter::FromIterator, path::PathBuf}; + + use crate::utils; + // hunk-header-style (by default) includes 'line-number' but not 'file'. // Normally, `paint_file_path_with_line_number` would return a painted line number. // But in this test hyperlinks are activated, and the test ensures that delta.__workdir__ is @@ -408,14 +411,21 @@ pub mod tests { // This test confirms that, under those circumstances, `paint_file_path_with_line_number` // returns a hyperlinked file path with line number. - let mut config = - integration_test_utils::make_config_from_args(&["--features", "hyperlinks"]); - config.cwd_of_user_shell_process = - Some(std::path::PathBuf::from("/some/current/directory")); + let config = integration_test_utils::make_config_from_args(&["--features", "hyperlinks"]); + let relative_path = PathBuf::from_iter(["some-dir", "some-file"]); - let result = paint_file_path_with_line_number(Some(3), "some-file", &config); + let result = + paint_file_path_with_line_number(Some(3), &relative_path.to_string_lossy(), &config); - assert_eq!(result, "\u{1b}]8;;file:///some/current/directory/some-file\u{1b}\\\u{1b}[34m3\u{1b}[0m\u{1b}]8;;\u{1b}\\"); + assert_eq!( + result, + format!( + "\u{1b}]8;;file://{}\u{1b}\\\u{1b}[34m3\u{1b}[0m\u{1b}]8;;\u{1b}\\", + utils::path::fake_delta_cwd_for_tests() + .join(relative_path) + .to_string_lossy() + ) + ); } #[test] diff --git a/src/paint.rs b/src/paint.rs index ed9185a09..709d7d6c6 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -11,7 +11,6 @@ use unicode_segmentation::UnicodeSegmentation; use crate::config::{self, delta_unreachable, Config}; use crate::delta::{DiffType, InMergeConflict, MergeParents, State}; -use crate::edits; use crate::features::hyperlinks; use crate::features::line_numbers::{self, LineNumbersData}; use crate::features::side_by_side::ansifill; @@ -21,6 +20,7 @@ use crate::minusplus::*; use crate::paint::superimpose_style_sections::superimpose_style_sections; use crate::style::Style; use crate::{ansi, style}; +use crate::{edits, utils}; pub type LineSections<'a, S> = Vec<(S, &'a str)>; @@ -775,7 +775,7 @@ pub fn parse_style_sections<'a>( #[allow(clippy::too_many_arguments)] pub fn paint_file_path_with_line_number( line_number: Option, - plus_file: &str, + file_path: &str, pad_line_number: bool, separator: &str, terminate_with_separator: bool, @@ -785,12 +785,12 @@ 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) + let file_path = if let Some(regex_replacement) = &config.file_regex_replacement { + regex_replacement.execute(file_path) } else { - Cow::from(plus_file) + Cow::from(file_path) }; - file_with_line_number.push(file_style.paint(plus_file)) + file_with_line_number.push(file_style.paint(file_path)) }; if let Some(line_number) = line_number { if let Some(line_number_style) = line_number_style { @@ -820,16 +820,19 @@ pub fn paint_file_path_with_line_number( } } let file_with_line_number = ansi_term::ANSIStrings(&file_with_line_number).to_string(); - if config.hyperlinks && !file_with_line_number.is_empty() { - hyperlinks::format_osc8_file_hyperlink( - plus_file, + match if config.hyperlinks && !file_with_line_number.is_empty() { + utils::path::absolute_path(file_path, config) + } else { + None + } { + Some(absolute_path) => hyperlinks::format_osc8_file_hyperlink( + absolute_path, line_number, &file_with_line_number, config, ) - .into() - } else { - file_with_line_number + .into(), + _ => file_with_line_number, } } diff --git a/src/utils/cwd.rs b/src/utils/cwd.rs deleted file mode 100644 index 78b477197..000000000 --- a/src/utils/cwd.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::path::PathBuf; - -/// Return current working directory of the user's shell process. I.e. the directory which they are -/// in when delta exits. This is the directory relative to which the file paths in delta output are -/// constructed if they are using either (a) delta's relative-paths option or (b) git's --relative -/// flag. -pub fn cwd_of_user_shell_process( - cwd_of_delta_process: Option<&PathBuf>, - cwd_relative_to_repo_root: Option<&str>, -) -> Option { - match (cwd_of_delta_process, cwd_relative_to_repo_root) { - (Some(cwd), None) => { - // We are not a child process of git - Some(PathBuf::from(cwd)) - } - (Some(repo_root), Some(cwd_relative_to_repo_root)) => { - // We are a child process of git; git spawned us from repo_root and preserved the user's - // original cwd in the GIT_PREFIX env var (available as config.cwd_relative_to_repo_root) - Some(PathBuf::from(repo_root).join(cwd_relative_to_repo_root)) - } - (None, _) => { - // Unexpected - None - } - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 630895312..20adef24a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,6 @@ #[cfg(not(tarpaulin_include))] pub mod bat; -pub mod cwd; +pub mod path; pub mod process; pub mod regex_replacement; pub mod syntect; diff --git a/src/utils/path.rs b/src/utils/path.rs new file mode 100644 index 000000000..6891e81a8 --- /dev/null +++ b/src/utils/path.rs @@ -0,0 +1,109 @@ +use std::path::{Component, Path, PathBuf}; + +use crate::config::Config; + +use super::process::calling_process; + +// Infer absolute path to `relative_path`. +pub fn absolute_path(relative_path: &str, config: &Config) -> Option { + match ( + &config.cwd_of_delta_process, + &config.cwd_of_user_shell_process, + calling_process().paths_in_input_are_relative_to_cwd() || config.relative_paths, + ) { + // Note that if we were invoked by git then cwd_of_delta_process == repo_root + (Some(cwd_of_delta_process), _, false) => Some(cwd_of_delta_process.join(relative_path)), + (_, Some(cwd_of_user_shell_process), true) => { + Some(cwd_of_user_shell_process.join(relative_path)) + } + (Some(cwd_of_delta_process), None, true) => { + // This might occur when piping from git to delta? + Some(cwd_of_delta_process.join(relative_path)) + } + _ => None, + } + .map(normalize_path) +} + +/// Relativize path if delta config demands that and paths are not already relativized by git. +pub fn relativize_path_maybe(path: &str, config: &Config) -> Option { + if config.relative_paths && !calling_process().paths_in_input_are_relative_to_cwd() { + if let Some(base) = config.cwd_relative_to_repo_root.as_deref() { + pathdiff::diff_paths(&path, base) + } else { + None + } + } else { + None + } +} + +/// Return current working directory of the user's shell process. I.e. the directory which they are +/// in when delta exits. This is the directory relative to which the file paths in delta output are +/// constructed if they are using either (a) delta's relative-paths option or (b) git's --relative +/// flag. +pub fn cwd_of_user_shell_process( + cwd_of_delta_process: Option<&PathBuf>, + cwd_relative_to_repo_root: Option<&str>, +) -> Option { + match (cwd_of_delta_process, cwd_relative_to_repo_root) { + (Some(cwd), None) => { + // We are not a child process of git + Some(PathBuf::from(cwd)) + } + (Some(repo_root), Some(cwd_relative_to_repo_root)) => { + // We are a child process of git; git spawned us from repo_root and preserved the user's + // original cwd in the GIT_PREFIX env var (available as config.cwd_relative_to_repo_root) + Some(PathBuf::from(repo_root).join(cwd_relative_to_repo_root)) + } + (None, _) => { + // Unexpected + None + } + } +} + +// Copied from +// https://github.com/rust-lang/cargo/blob/c6745a3d7fcea3a949c3e13e682b8ddcbd213add/crates/cargo-util/src/paths.rs#L73-L106 +// as suggested by matklad: https://www.reddit.com/r/rust/comments/hkkquy/comment/fwtw53s/?utm_source=share&utm_medium=web2x&context=3 +fn normalize_path

(path: P) -> PathBuf +where + P: AsRef, +{ + let mut components = path.as_ref().components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +#[cfg(test)] +pub fn fake_delta_cwd_for_tests() -> PathBuf { + #[cfg(not(target_os = "windows"))] + { + PathBuf::from("/fake/delta/cwd") + } + #[cfg(target_os = "windows")] + { + PathBuf::from(r"C:\fake\delta\cwd") + } +} diff --git a/src/utils/process.rs b/src/utils/process.rs index 0e98024af..a29840900 100644 --- a/src/utils/process.rs +++ b/src/utils/process.rs @@ -20,6 +20,18 @@ pub enum CallingProcess { } // TODO: Git blame is currently handled differently +impl CallingProcess { + pub fn paths_in_input_are_relative_to_cwd(&self) -> bool { + match self { + CallingProcess::GitDiff(cmd) if cmd.long_options.contains("--relative") => true, + CallingProcess::GitShow(cmd, _) if cmd.long_options.contains("--relative") => true, + CallingProcess::GitLog(cmd) if cmd.long_options.contains("--relative") => true, + CallingProcess::GitGrep(_) | CallingProcess::OtherGrep => true, + _ => false, + } + } +} + #[derive(Clone, Debug, PartialEq)] pub struct CommandLine { pub long_options: HashSet,