file_util: move path handling functions from ui module

We'll need this kind of functions in the library crate to parse user file
patterns.
This commit is contained in:
Yuya Nishihara 2022-10-21 13:08:23 +09:00
parent 756c4fedb6
commit 689332aedd
4 changed files with 74 additions and 73 deletions

View file

@ -13,10 +13,59 @@
// limitations under the License.
use std::fs::File;
use std::path::Path;
use std::iter;
use std::path::{Component, Path, PathBuf};
use tempfile::{NamedTempFile, PersistError};
/// Turns the given `to` path into relative path starting from the `from` path.
///
/// Both `from` and `to` paths are supposed to be absolute and normalized in the
/// same manner.
pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
// Find common prefix.
for (i, base) in from.ancestors().enumerate() {
if let Ok(suffix) = to.strip_prefix(base) {
if i == 0 && suffix.as_os_str().is_empty() {
return ".".into();
} else {
let mut result = PathBuf::from_iter(iter::repeat("..").take(i));
result.push(suffix);
return result;
}
}
}
// No common prefix found. Return the original (absolute) path.
to.to_owned()
}
/// Consumes as much `..` and `.` as possible without considering symlinks.
pub fn normalize_path(path: &Path) -> PathBuf {
let mut result = PathBuf::new();
for c in path.components() {
match c {
Component::CurDir => {}
Component::ParentDir
if matches!(result.components().next_back(), Some(Component::Normal(_))) =>
{
// Do not pop ".."
let popped = result.pop();
assert!(popped);
}
_ => {
result.push(c);
}
}
}
if result.as_os_str().is_empty() {
".".into()
} else {
result
}
}
// Like NamedTempFile::persist(), but also succeeds if the target already
// exists.
pub fn persist_content_addressed_temp_file<P: AsRef<Path>>(
@ -44,6 +93,20 @@ mod tests {
use super::*;
use crate::testutils;
#[test]
fn normalize_too_many_dot_dot() {
assert_eq!(normalize_path(Path::new("foo/..")), Path::new("."));
assert_eq!(normalize_path(Path::new("foo/../..")), Path::new(".."));
assert_eq!(
normalize_path(Path::new("foo/../../..")),
Path::new("../..")
);
assert_eq!(
normalize_path(Path::new("foo/../../../bar/baz/..")),
Path::new("../../bar")
);
}
#[test]
fn test_persist_no_existing_file() {
let temp_dir = testutils::new_temp_dir();

View file

@ -41,13 +41,12 @@ use jujutsu_lib::working_copy::{
CheckoutStats, LockedWorkingCopy, ResetError, SnapshotError, WorkingCopy,
};
use jujutsu_lib::workspace::{Workspace, WorkspaceInitError, WorkspaceLoadError};
use jujutsu_lib::{dag_walk, git, revset};
use jujutsu_lib::{dag_walk, file_util, git, revset};
use crate::config::read_config;
use crate::diff_edit::DiffEditError;
use crate::formatter::Formatter;
use crate::templater::TemplateFormatter;
use crate::ui;
use crate::ui::{ColorChoice, FilePathParseError, Ui};
pub enum CommandError {
@ -496,7 +495,7 @@ impl WorkspaceCommandHelper {
}
pub fn format_file_path(&self, file: &RepoPath) -> String {
ui::relative_path(&self.cwd, &file.to_fs_path(self.workspace_root()))
file_util::relative_path(&self.cwd, &file.to_fs_path(self.workspace_root()))
.to_str()
.unwrap()
.to_owned()

View file

@ -46,7 +46,7 @@ use jujutsu_lib::store::Store;
use jujutsu_lib::tree::{merge_trees, Tree, TreeDiffIterator};
use jujutsu_lib::view::View;
use jujutsu_lib::workspace::Workspace;
use jujutsu_lib::{conflicts, diff, files, git, revset, tree};
use jujutsu_lib::{conflicts, diff, file_util, files, git, revset, tree};
use maplit::{hashmap, hashset};
use pest::Parser;
@ -60,7 +60,6 @@ use crate::formatter::Formatter;
use crate::graphlog::{AsciiGraphDrawer, Edge};
use crate::template_parser::TemplateParser;
use crate::templater::Template;
use crate::ui;
use crate::ui::Ui;
#[derive(clap::Parser, Clone, Debug)]
@ -1122,7 +1121,7 @@ Set `ui.allow-init-native` to allow initializing a repo with the native backend.
Workspace::init_local(ui.settings(), &wc_path)?;
};
let cwd = ui.cwd().canonicalize().unwrap();
let relative_wc_path = ui::relative_path(&cwd, &wc_path);
let relative_wc_path = file_util::relative_path(&cwd, &wc_path);
writeln!(ui, "Initialized repo in \"{}\"", relative_wc_path.display())?;
Ok(())
}
@ -3797,7 +3796,8 @@ fn cmd_workspace_add(
writeln!(
ui,
"Created workspace in \"{}\"",
ui::relative_path(old_workspace_command.workspace_root(), &destination_path).display()
file_util::relative_path(old_workspace_command.workspace_root(), &destination_path)
.display()
)?;
let mut new_workspace_command = WorkspaceCommandHelper::new(

View file

@ -15,9 +15,10 @@
use std::io::{Stderr, Stdout, Write};
use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
use std::{fmt, io, iter};
use std::{fmt, io};
use atty::Stream;
use jujutsu_lib::file_util;
use jujutsu_lib::repo_path::{RepoPath, RepoPathComponent};
use jujutsu_lib::settings::UserSettings;
@ -176,8 +177,8 @@ impl Ui {
wc_path: &Path,
input: &str,
) -> Result<RepoPath, FilePathParseError> {
let abs_input_path = normalize_path(&self.cwd.join(input));
let repo_relative_path = relative_path(wc_path, &abs_input_path);
let abs_input_path = file_util::normalize_path(&self.cwd.join(input));
let repo_relative_path = file_util::relative_path(wc_path, &abs_input_path);
if repo_relative_path == Path::new(".") {
return Ok(RepoPath::root());
}
@ -201,54 +202,6 @@ pub enum FilePathParseError {
InputNotInRepo(String),
}
/// Turns the given `to` path into relative path starting from the `from` path.
///
/// Both `from` and `to` paths are supposed to be absolute and normalized in the
/// same manner.
pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
// Find common prefix.
for (i, base) in from.ancestors().enumerate() {
if let Ok(suffix) = to.strip_prefix(base) {
if i == 0 && suffix.as_os_str().is_empty() {
return ".".into();
} else {
let mut result = PathBuf::from_iter(iter::repeat("..").take(i));
result.push(suffix);
return result;
}
}
}
// No common prefix found. Return the original (absolute) path.
to.to_owned()
}
/// Consumes as much `..` and `.` as possible without considering symlinks.
fn normalize_path(path: &Path) -> PathBuf {
let mut result = PathBuf::new();
for c in path.components() {
match c {
Component::CurDir => {}
Component::ParentDir
if matches!(result.components().next_back(), Some(Component::Normal(_))) =>
{
// Do not pop ".."
let popped = result.pop();
assert!(popped);
}
_ => {
result.push(c);
}
}
}
if result.as_os_str().is_empty() {
".".into()
} else {
result
}
}
#[cfg(test)]
mod tests {
use jujutsu_lib::testutils;
@ -355,18 +308,4 @@ mod tests {
Ok(RepoPath::from_internal_string("dir/file"))
);
}
#[test]
fn normalize_too_many_dot_dot() {
assert_eq!(normalize_path(Path::new("foo/..")), Path::new("."));
assert_eq!(normalize_path(Path::new("foo/../..")), Path::new(".."));
assert_eq!(
normalize_path(Path::new("foo/../../..")),
Path::new("../..")
);
assert_eq!(
normalize_path(Path::new("foo/../../../bar/baz/..")),
Path::new("../../bar")
);
}
}