diff --git a/lib/src/file_util.rs b/lib/src/file_util.rs index 3884f8f27..5076af831 100644 --- a/lib/src/file_util.rs +++ b/lib/src/file_util.rs @@ -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>( @@ -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(); diff --git a/src/cli_util.rs b/src/cli_util.rs index 73bc27a0c..9686cf69b 100644 --- a/src/cli_util.rs +++ b/src/cli_util.rs @@ -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() diff --git a/src/commands.rs b/src/commands.rs index bfa225738..a246b1dc7 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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( diff --git a/src/ui.rs b/src/ui.rs index 04ff7aefe..8283fb90a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -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 { - 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") - ); - } }