fix(directory): Fix path contractions for symlinked git repos (#1299)
Fixes git repo path contractions in two situations: 1. When path obtained from `PWD` is a logical path but git libraries return physical paths. 2. When a git repository's subdirectory is symlinked to ouside of the repository tree. (1) is fixed by implementing a realpath()-like function, then reparsing the (possibly logical) `PWD` using realpath() to convert logical components into physical ones. The physical paths are then matched against each other. In the case of (2), the default behavior has been changed by simply contracting to the home directory, exactly the same as if we are not in a repo at all. Because determining the correct contraction is not obvious, we bail out and just pretend we are not in a repo at all.
This commit is contained in:
parent
16b648581e
commit
611a754ec7
|
@ -1,5 +1,6 @@
|
|||
use path_slash::PathExt;
|
||||
use std::collections::HashMap;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::{Path, PathBuf};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
|
@ -62,14 +63,15 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
|||
|
||||
let dir_string = match &repo.root {
|
||||
Some(repo_root) if config.truncate_to_repo && (repo_root != &home_dir) => {
|
||||
let repo_folder_name = repo_root.file_name().unwrap().to_str().unwrap();
|
||||
|
||||
log::debug!("Repo root: {:?}", repo_root);
|
||||
// Contract the path to the git repo root
|
||||
contract_path(current_dir, repo_root, repo_folder_name)
|
||||
contract_repo_path(current_dir, repo_root)
|
||||
.unwrap_or_else(|| contract_path(current_dir, &home_dir, HOME_SYMBOL))
|
||||
}
|
||||
// Contract the path to the home directory
|
||||
_ => contract_path(current_dir, &home_dir, HOME_SYMBOL),
|
||||
};
|
||||
log::debug!("Dir string: {}", dir_string);
|
||||
|
||||
let substituted_dir = substitute_path(dir_string, &config.substitutions);
|
||||
|
||||
|
@ -134,6 +136,57 @@ fn contract_path(full_path: &Path, top_level_path: &Path, top_level_replacement:
|
|||
)
|
||||
}
|
||||
|
||||
/// Contract the root component of a path based on the real path
|
||||
///
|
||||
/// Replaces the `top_level_path` in a given `full_path` with the provided
|
||||
/// `top_level_replacement` by walking ancestors and comparing its real path.
|
||||
fn contract_repo_path(full_path: &Path, top_level_path: &Path) -> Option<String> {
|
||||
let top_level_real_path = real_path(top_level_path);
|
||||
// Walk ancestors to preserve logical path in `full_path`.
|
||||
// If we'd just `full_real_path.strip_prefix(top_level_real_path)`,
|
||||
// then it wouldn't preserve logical path. It would've returned physical path.
|
||||
for (i, ancestor) in full_path.ancestors().enumerate() {
|
||||
let ancestor_real_path = real_path(ancestor);
|
||||
if ancestor_real_path != top_level_real_path {
|
||||
continue;
|
||||
}
|
||||
|
||||
let components: Vec<_> = full_path.components().collect();
|
||||
let repo_name = components[components.len() - i - 1].as_os_str().to_str()?;
|
||||
|
||||
if i == 0 {
|
||||
return Some(repo_name.to_string());
|
||||
}
|
||||
|
||||
let path = PathBuf::from_iter(&components[components.len() - i..]);
|
||||
return Some(format!(
|
||||
"{repo_name}{separator}{path}",
|
||||
repo_name = repo_name,
|
||||
separator = "/",
|
||||
path = path.to_slash()?
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn real_path<P: AsRef<Path>>(path: P) -> PathBuf {
|
||||
let path = path.as_ref();
|
||||
let mut buf = PathBuf::new();
|
||||
for component in path.components() {
|
||||
let next = buf.join(component);
|
||||
if let Ok(realpath) = next.read_link() {
|
||||
if realpath.is_absolute() {
|
||||
buf = realpath;
|
||||
} else {
|
||||
buf.push(realpath);
|
||||
}
|
||||
} else {
|
||||
buf = next;
|
||||
}
|
||||
}
|
||||
buf.canonicalize().unwrap_or_else(|_| path.into())
|
||||
}
|
||||
|
||||
/// Perform a list of string substitutions on the path
|
||||
///
|
||||
/// Given a list of (from, to) pairs, this will perform the string
|
||||
|
|
|
@ -3,6 +3,10 @@ use dirs::home_dir;
|
|||
use git2::Repository;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use std::os::unix::fs::symlink;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::fs::symlink_dir as symlink;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
|
@ -511,3 +515,281 @@ fn git_repo_in_home_directory_truncate_to_repo_true() -> io::Result<()> {
|
|||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn symlinked_git_repo_root() -> io::Result<()> {
|
||||
let tmp_dir = TempDir::new_in(dirs::home_dir().unwrap())?;
|
||||
let repo_dir = tmp_dir.path().join("rocket-controls");
|
||||
let symlink_dir = tmp_dir.path().join("rocket-controls-symlink");
|
||||
fs::create_dir(&repo_dir)?;
|
||||
Repository::init(&repo_dir).unwrap();
|
||||
symlink(&repo_dir, &symlink_dir)?;
|
||||
|
||||
let output = common::render_module("directory")
|
||||
.arg("--path")
|
||||
.arg(symlink_dir)
|
||||
.output()?;
|
||||
let actual = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let expected = format!(
|
||||
"in {} ",
|
||||
Color::Cyan.bold().paint("rocket-controls-symlink")
|
||||
);
|
||||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn directory_in_symlinked_git_repo() -> io::Result<()> {
|
||||
let tmp_dir = TempDir::new_in(dirs::home_dir().unwrap())?;
|
||||
let repo_dir = tmp_dir.path().join("rocket-controls");
|
||||
let src_dir = repo_dir.join("src");
|
||||
let symlink_dir = tmp_dir.path().join("rocket-controls-symlink");
|
||||
let symlink_src_dir = symlink_dir.join("src");
|
||||
fs::create_dir_all(&src_dir)?;
|
||||
Repository::init(&repo_dir).unwrap();
|
||||
symlink(&repo_dir, &symlink_dir)?;
|
||||
|
||||
let output = common::render_module("directory")
|
||||
.arg("--path")
|
||||
.arg(symlink_src_dir)
|
||||
.output()?;
|
||||
let actual = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let expected = format!(
|
||||
"in {} ",
|
||||
Color::Cyan.bold().paint("rocket-controls-symlink/src")
|
||||
);
|
||||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn truncated_directory_in_symlinked_git_repo() -> io::Result<()> {
|
||||
let tmp_dir = TempDir::new_in(dirs::home_dir().unwrap())?;
|
||||
let repo_dir = tmp_dir.path().join("rocket-controls");
|
||||
let src_dir = repo_dir.join("src/meters/fuel-gauge");
|
||||
let symlink_dir = tmp_dir.path().join("rocket-controls-symlink");
|
||||
let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge");
|
||||
fs::create_dir_all(&src_dir)?;
|
||||
Repository::init(&repo_dir).unwrap();
|
||||
symlink(&repo_dir, &symlink_dir)?;
|
||||
|
||||
let output = common::render_module("directory")
|
||||
.arg("--path")
|
||||
.arg(symlink_src_dir)
|
||||
.output()?;
|
||||
let actual = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let expected = format!("in {} ", Color::Cyan.bold().paint("src/meters/fuel-gauge"));
|
||||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn directory_in_symlinked_git_repo_truncate_to_repo_false() -> io::Result<()> {
|
||||
let tmp_dir = TempDir::new_in(dirs::home_dir().unwrap())?;
|
||||
let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls");
|
||||
let src_dir = repo_dir.join("src/meters/fuel-gauge");
|
||||
let symlink_dir = tmp_dir
|
||||
.path()
|
||||
.join("above-repo")
|
||||
.join("rocket-controls-symlink");
|
||||
let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge");
|
||||
fs::create_dir_all(&src_dir)?;
|
||||
Repository::init(&repo_dir).unwrap();
|
||||
symlink(&repo_dir, &symlink_dir)?;
|
||||
|
||||
let output = common::render_module("directory")
|
||||
.use_config(toml::toml! {
|
||||
[directory]
|
||||
// Don't truncate the path at all.
|
||||
truncation_length = 5
|
||||
truncate_to_repo = false
|
||||
})
|
||||
.arg("--path")
|
||||
.arg(symlink_src_dir)
|
||||
.output()?;
|
||||
let actual = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let expected = format!(
|
||||
"in {} ",
|
||||
Color::Cyan
|
||||
.bold()
|
||||
.paint("above-repo/rocket-controls-symlink/src/meters/fuel-gauge")
|
||||
);
|
||||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn fish_path_directory_in_symlinked_git_repo_truncate_to_repo_false() -> io::Result<()> {
|
||||
let tmp_dir = TempDir::new_in(dirs::home_dir().unwrap())?;
|
||||
let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls");
|
||||
let src_dir = repo_dir.join("src/meters/fuel-gauge");
|
||||
let symlink_dir = tmp_dir
|
||||
.path()
|
||||
.join("above-repo")
|
||||
.join("rocket-controls-symlink");
|
||||
let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge");
|
||||
fs::create_dir_all(&src_dir)?;
|
||||
Repository::init(&repo_dir).unwrap();
|
||||
symlink(&repo_dir, &symlink_dir)?;
|
||||
|
||||
let output = common::render_module("directory")
|
||||
.use_config(toml::toml! {
|
||||
[directory]
|
||||
// Don't truncate the path at all.
|
||||
truncation_length = 5
|
||||
truncate_to_repo = false
|
||||
fish_style_pwd_dir_length = 1
|
||||
})
|
||||
.arg("--path")
|
||||
.arg(symlink_src_dir)
|
||||
.output()?;
|
||||
let actual = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let expected = format!(
|
||||
"in {} ",
|
||||
Color::Cyan
|
||||
.bold()
|
||||
.paint("~/.t/above-repo/rocket-controls-symlink/src/meters/fuel-gauge")
|
||||
);
|
||||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn fish_path_directory_in_symlinked_git_repo_truncate_to_repo_true() -> io::Result<()> {
|
||||
let tmp_dir = TempDir::new_in(dirs::home_dir().unwrap())?;
|
||||
let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls");
|
||||
let src_dir = repo_dir.join("src/meters/fuel-gauge");
|
||||
let symlink_dir = tmp_dir
|
||||
.path()
|
||||
.join("above-repo")
|
||||
.join("rocket-controls-symlink");
|
||||
let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge");
|
||||
fs::create_dir_all(&src_dir)?;
|
||||
Repository::init(&repo_dir).unwrap();
|
||||
symlink(&repo_dir, &symlink_dir)?;
|
||||
|
||||
let output = common::render_module("directory")
|
||||
.use_config(toml::toml! {
|
||||
[directory]
|
||||
// `truncate_to_repo = true` should display the truncated path
|
||||
truncation_length = 5
|
||||
truncate_to_repo = true
|
||||
fish_style_pwd_dir_length = 1
|
||||
})
|
||||
.arg("--path")
|
||||
.arg(symlink_src_dir)
|
||||
.output()?;
|
||||
let actual = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let expected = format!(
|
||||
"in {} ",
|
||||
Color::Cyan
|
||||
.bold()
|
||||
.paint("~/.t/a/rocket-controls-symlink/src/meters/fuel-gauge")
|
||||
);
|
||||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn directory_in_symlinked_git_repo_truncate_to_repo_true() -> io::Result<()> {
|
||||
let tmp_dir = TempDir::new_in(dirs::home_dir().unwrap())?;
|
||||
let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls");
|
||||
let src_dir = repo_dir.join("src/meters/fuel-gauge");
|
||||
let symlink_dir = tmp_dir
|
||||
.path()
|
||||
.join("above-repo")
|
||||
.join("rocket-controls-symlink");
|
||||
let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge");
|
||||
fs::create_dir_all(&src_dir)?;
|
||||
Repository::init(&repo_dir).unwrap();
|
||||
symlink(&repo_dir, &symlink_dir)?;
|
||||
|
||||
let output = common::render_module("directory")
|
||||
.use_config(toml::toml! {
|
||||
[directory]
|
||||
// `truncate_to_repo = true` should display the truncated path
|
||||
truncation_length = 5
|
||||
truncate_to_repo = true
|
||||
})
|
||||
.arg("--path")
|
||||
.arg(symlink_src_dir)
|
||||
.output()?;
|
||||
let actual = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let expected = format!(
|
||||
"in {} ",
|
||||
Color::Cyan
|
||||
.bold()
|
||||
.paint("rocket-controls-symlink/src/meters/fuel-gauge")
|
||||
);
|
||||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn symlinked_directory_in_git_repo() -> io::Result<()> {
|
||||
let tmp_dir = TempDir::new_in(dirs::home_dir().unwrap())?;
|
||||
let repo_dir = tmp_dir.path().join("rocket-controls");
|
||||
let dir = repo_dir.join("src");
|
||||
fs::create_dir_all(&dir)?;
|
||||
Repository::init(&repo_dir).unwrap();
|
||||
symlink(&dir, repo_dir.join("src/loop"))?;
|
||||
|
||||
let output = common::render_module("directory")
|
||||
.use_config(toml::toml! {
|
||||
[directory]
|
||||
// `truncate_to_repo = true` should display the truncated path
|
||||
truncation_length = 5
|
||||
truncate_to_repo = true
|
||||
})
|
||||
.arg("--path")
|
||||
.arg(repo_dir.join("src/loop/loop"))
|
||||
.output()?;
|
||||
let actual = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let expected = format!(
|
||||
"in {} ",
|
||||
Color::Cyan.bold().paint("rocket-controls/src/loop/loop")
|
||||
);
|
||||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn symlinked_subdirectory_git_repo_out_of_tree() -> io::Result<()> {
|
||||
let tmp_dir = TempDir::new_in(dirs::home_dir().unwrap())?;
|
||||
let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls");
|
||||
let src_dir = repo_dir.join("src/meters/fuel-gauge");
|
||||
let symlink_dir = tmp_dir.path().join("fuel-gauge");
|
||||
fs::create_dir_all(&src_dir)?;
|
||||
Repository::init(&repo_dir).unwrap();
|
||||
symlink(&src_dir, &symlink_dir)?;
|
||||
|
||||
let output = common::render_module("directory")
|
||||
// Set home directory to the temp repository
|
||||
.env("HOME", tmp_dir.path())
|
||||
.arg("--path")
|
||||
.arg(symlink_dir)
|
||||
.output()?;
|
||||
let actual = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
let expected = format!("in {} ", Color::Cyan.bold().paint("~/fuel-gauge"));
|
||||
assert_eq!(expected, actual);
|
||||
tmp_dir.close()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue