Fix hyperlink absolute paths (#939)

Fix file paths and hyperlinks

With this commit the target of a hyperlink should always be an absolute path. This should be true for all file hyperlinks, e.g.

- File hyperlink
- Hunk header hyperlink
- Line number hyperlink

Fixes #890
This commit is contained in:
Dan Davison 2022-02-14 12:30:30 -05:00 committed by GitHub
parent b66d1d3717
commit 3d5b6852a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 641 additions and 178 deletions

View File

@ -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<cli::Opt> 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(),
);

View File

@ -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<GitConfigEntry> {
})
}
/// 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<usize>,
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<Path>,
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,413 @@ 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 std::iter::FromIterator;
use std::path::PathBuf;
use super::super::*;
use crate::tests::integration_test_utils;
use super::*;
use crate::{
tests::integration_test_utils::{self, DeltaTest},
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
#[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
"#;
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 GitDiffRelative {
Yes,
No,
}
#[derive(Debug)]
enum CallingProcess {
GitDiff(GitDiffRelative),
GitGrep,
OtherGrep,
}
#[derive(Clone, Copy, Debug)]
enum InputType {
GitDiff,
Grep,
}
impl<'a> FilePathsTestCase<'a> {
pub fn get_args(&self) -> Vec<String> {
let mut args = vec![
"--navigate", // helps locate the file path in the output
"--line-numbers",
"--hyperlinks",
"--hyperlinks-file-link-format",
"{path}",
"--grep-file-style",
"raw",
"--grep-line-number-style",
"raw",
"--hunk-header-file-style",
"raw",
"--hunk-header-line-number-style",
"raw",
"--line-numbers-plus-style",
"raw",
"--line-numbers-left-style",
"raw",
"--line-numbers-right-style",
"raw",
"--line-numbers-left-format",
"{nm}અ",
"--line-numbers-right-format",
"{np}જ",
];
if self.delta_relative_paths_option {
args.push("--relative-paths");
}
args.iter().map(|s| s.to_string()).collect()
}
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(GitDiffRelative::Yes)
}
(InputType::GitDiff, Some(s)) if s.starts_with("git diff") => {
CallingProcess::GitDiff(GitDiffRelative::No)
}
(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(GitDiffRelative::No) => self
.true_location_of_file_relative_to_repo_root
.to_string_lossy()
.to_string(),
CallingProcess::GitDiff(GitDiffRelative::Yes) => 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::<Vec<&str>>()
.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)),
};
let make_expected_hyperlink = |text| {
format_osc8_hyperlink(
&PathBuf::from(test_case.expected_hyperlink_path()).to_string_lossy(),
text,
)
}
#[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)
}
#[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)
};
match test_case.calling_process() {
CallingProcess::GitDiff(_) => {
let line_number = "1";
delta_test
.inspect_raw()
// file hyperlink
.expect_raw_contains(&format!(
"Δ {}",
make_expected_hyperlink(test_case.expected_displayed_path)
))
// hunk header hyperlink
.expect_raw_contains(&format!("{}", make_expected_hyperlink(line_number)))
// line number hyperlink
.expect_raw_contains(&format!("{}", make_expected_hyperlink(line_number)));
}
CallingProcess::GitGrep | CallingProcess::OtherGrep => {
delta_test
.inspect_raw()
.expect_raw_contains(&make_expected_hyperlink(
test_case.expected_displayed_path,
));
}
}
}
}

View File

@ -12,6 +12,7 @@ use crate::features::OptionValueFunction;
use crate::format::{self, Align, Placeholder};
use crate::minusplus::*;
use crate::style::Style;
use crate::utils;
pub fn make_feature() -> Vec<(String, OptionValueFunction)> {
builtin_feature!([
@ -304,9 +305,13 @@ fn format_line_number(
let pad = |n| format::pad(n, width, alignment, precision);
match (line_number, config.hyperlinks, plus_file) {
(None, _, _) => " ".repeat(width),
(Some(n), true, Some(file)) => {
hyperlinks::format_osc8_file_hyperlink(file, line_number, &pad(n), config).to_string()
}
(Some(n), true, Some(file)) => match utils::path::absolute_path(file, config) {
Some(absolute_path) => {
hyperlinks::format_osc8_file_hyperlink(absolute_path, line_number, &pad(n), config)
.to_string()
}
None => file.to_owned(),
},
(Some(n), _, _) => pad(n),
}
}

View File

@ -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(
@ -187,12 +179,17 @@ impl<'a> StateMachine<'a> {
"".to_string()
}
};
let format_file = |file| {
if self.config.hyperlinks {
features::hyperlinks::format_osc8_file_hyperlink(file, None, file, self.config)
} else {
Cow::from(file)
}
let format_file = |file| match (
self.config.hyperlinks,
utils::path::absolute_path(file, self.config),
) {
(true, Some(absolute_path)) => features::hyperlinks::format_osc8_file_hyperlink(
absolute_path,
None,
file,
self.config,
),
_ => Cow::from(file),
};
let label = format_label(&self.config.file_modified_label);
let name = get_repeated_file_path_from_diff_line(&self.diff_line)
@ -274,12 +271,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 +291,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 +352,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 +462,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 +484,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 +511,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 +519,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 +527,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)
);
}

View File

@ -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]

View File

@ -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<usize>,
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,
}
}

View File

@ -26,6 +26,8 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
plus-non-emph-style = {plus_non_emph_style}
plus-emph-style = {plus_emph_style}
plus-empty-line-marker-style = {plus_empty_line_marker_style}
grep-file-style = {grep_file_style}
grep-line-number-style = {grep_line_number_style}
whitespace-error-style = {whitespace_error_style}
blame-palette = {blame_palette}",
blame_palette = config
@ -44,6 +46,8 @@ pub fn show_config(config: &config::Config, writer: &mut dyn Write) -> std::io::
plus_empty_line_marker_style = config.plus_empty_line_marker_style.to_painted_string(),
plus_non_emph_style = config.plus_non_emph_style.to_painted_string(),
plus_style = config.plus_style.to_painted_string(),
grep_file_style = config.grep_file_style.to_painted_string(),
grep_line_number_style = config.grep_line_number_style.to_painted_string(),
whitespace_error_style = config.whitespace_error_style.to_painted_string(),
zero_style = config.zero_style.to_painted_string(),
)?;

View File

@ -244,7 +244,7 @@ impl DeltaTestOutput {
pub fn expect_contains(self, expected: &str) -> Self {
assert!(
self.output.contains(expected),
"Output does not contain \"{}\":\n{}",
"Output does not contain \"{}\":\n{}\n",
expected,
delineated_string(&self.output.as_str())
);
@ -254,7 +254,7 @@ impl DeltaTestOutput {
pub fn expect_raw_contains(self, expected: &str) -> Self {
assert!(
self.raw_output.contains(expected),
"Raw output does not contain \"{}\":\n{}",
"Raw output does not contain \"{}\":\n{}\n",
expected,
delineated_string(&self.raw_output.as_str())
);
@ -264,7 +264,7 @@ impl DeltaTestOutput {
pub fn expect_contains_once(self, expected: &str) -> Self {
assert!(
test_utils::contains_once(&self.output, expected),
"Output does not contain \"{}\" exactly once:\n{}",
"Output does not contain \"{}\" exactly once:\n{}\n",
expected,
delineated_string(&self.output.as_str())
);

View File

@ -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<PathBuf> {
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
}
}
}

View File

@ -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;

109
src/utils/path.rs Normal file
View File

@ -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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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<P>(path: P) -> PathBuf
where
P: AsRef<Path>,
{
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")
}
}

View File

@ -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<String>,

View File

@ -6,4 +6,4 @@ DELTA="$DELTA_BIN --no-gitconfig --raw --max-line-length 0"
ANSIFILTER="./etc/bin/ansifilter"
GIT_ARGS="log --patch --stat --numstat"
diff -u <(git $GIT_ARGS | $ANSIFILTER) <(git $GIT_ARGS | $DELTA | $ANSIFILTER)
diff -u <(git $GIT_ARGS | $ANSIFILTER) <(git $GIT_ARGS | DELTA_FEATURES= $DELTA | $ANSIFILTER)