diff --git a/cli/tests/common/mod.rs b/cli/tests/common/mod.rs index c70beca6ee..5c3b269670 100644 --- a/cli/tests/common/mod.rs +++ b/cli/tests/common/mod.rs @@ -18,6 +18,7 @@ use std::path::Path; use std::path::PathBuf; use itertools::Itertools as _; +use jj_lib::git::format_gitfile_path; use regex::Captures; use regex::Regex; use tempfile::TempDir; @@ -323,8 +324,10 @@ impl TestEnvironment { pub fn normalize_output(&self, text: &str) -> String { let text = text.replace("jj.exe", "jj"); let regex = Regex::new(&format!( - r"{}(\S+)", - regex::escape(&self.env_root.display().to_string()) + r"(?:{}|{})(\S+)", + regex::escape(&self.env_root.display().to_string()), + // Needed on windows + regex::escape(&format_gitfile_path(self.env_root())) )) .unwrap(); regex diff --git a/cli/tests/test_git_colocated.rs b/cli/tests/test_git_colocated.rs index a032327939..6fceee1bd4 100644 --- a/cli/tests/test_git_colocated.rs +++ b/cli/tests/test_git_colocated.rs @@ -1014,7 +1014,6 @@ fn test_colocated_workspace_fail_existing_git_worktree() { let test_env = TestEnvironment::default(); let repo_path = test_env.env_root().join("repo"); - let second_path = test_env.env_root().join("second"); let third_path = test_env.env_root().join("third"); // Initialize and make a commit so git can create worktrees @@ -1022,16 +1021,14 @@ fn test_colocated_workspace_fail_existing_git_worktree() { std::fs::write(repo_path.join("file.txt"), "contents").unwrap(); test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "initial commit"]); - // Add a a git worktree at `.git/worktrees/second` that points to "../third" + // Add a git worktree at `.git/worktrees/second` that points to "../third" Command::new("git") - .args(["worktree", "add"]) - .arg(&second_path) + .args(["worktree", "add", "../second"]) .current_dir(&repo_path) .assert() .success(); Command::new("git") - .args(["worktree", "move", "second"]) - .arg(&third_path) + .args(["worktree", "move", "second", "../third"]) .current_dir(&repo_path) .assert() .success(); diff --git a/lib/src/git.rs b/lib/src/git.rs index 9d9b89f2a1..b0134648df 100644 --- a/lib/src/git.rs +++ b/lib/src/git.rs @@ -269,6 +269,98 @@ impl CreateWorktreeError { } } +pub fn format_gitfile_path(path: &Path) -> Cow<'_, str> { + return to_slash_lossy(path); + + // This is from https://docs.rs/path-slash (MIT licensed) + // but tweaked to change any verbatim prefix to non-verbatim, as git does. + // + // Copyright (c) 2018 rhysd + // + // Permission is hereby granted, free of charge, to any person obtaining a copy + // of this software and associated documentation files (the "Software"), to deal + // in the Software without restriction, including without limitation the rights + // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + // copies of the Software, and to permit persons to whom the Software is + // furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in + // all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + // OTHER DEALINGS IN THE SOFTWARE. + // + + #[cfg(target_os = "windows")] + mod windows { + use std::os::windows::ffi::OsStrExt as _; + use std::path::MAIN_SEPARATOR; + + use super::*; + + // Workaround for Windows. There is no way to extract raw byte sequence from + // `OsStr` (in `Path`). And `OsStr::to_string_lossy` may cause extra + // heap allocation. + pub(crate) fn ends_with_main_sep(p: &Path) -> bool { + p.as_os_str().encode_wide().last() == Some(MAIN_SEPARATOR as u16) + } + } + #[cfg(not(target_os = "windows"))] + fn to_slash_lossy(path: &Path) -> Cow<'_, str> { + path.to_string_lossy() + } + #[cfg(target_os = "windows")] + fn to_slash_lossy(path: &Path) -> Cow<'_, str> { + use std::path::Component; + use std::path::Prefix; + + let mut buf = String::new(); + for c in path.components() { + match c { + Component::RootDir => { /* empty */ } + Component::CurDir => buf.push('.'), + Component::ParentDir => buf.push_str(".."), + Component::Prefix(prefix_component) => { + match prefix_component.kind() { + Prefix::Disk(disk) | Prefix::VerbatimDisk(disk) => { + if let Some(c) = char::from_u32(disk as u32) { + buf.push(c); + buf.push(':'); + } + } + Prefix::UNC(host, share) | Prefix::VerbatimUNC(host, share) => { + // Write it as non-verbatim but with two forward slashes // instead of + // \\. I think this sounds right? https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/unc-path-recognition-unix + buf.push_str("//"); + buf.push_str(&host.to_string_lossy()); + buf.push_str("/"); + buf.push_str(&share.to_string_lossy()); + } + // Just ignore it and hope for the best? + Prefix::Verbatim(_) => {} + Prefix::DeviceNS(_) => {} + } + // C:\foo is [Prefix, RootDir, Normal]. Avoid C:// + continue; + } + Component::Normal(s) => buf.push_str(&s.to_string_lossy()), + } + buf.push('/'); + } + + if !windows::ends_with_main_sep(path) && buf != "/" && buf.ends_with('/') { + buf.pop(); // Pop last '/' + } + + Cow::Owned(buf) + } +} + /// `git worktree add` implementation /// /// This function has to be implemented from scratch. @@ -318,12 +410,12 @@ pub fn git_worktree_add( { let mut commondir_file = file_create_new(worktree_data.join("commondir"))?; - commondir_file.write_all(Path::new("..").join("..").as_os_str().as_encoded_bytes())?; + commondir_file.write_all(format_gitfile_path(&Path::new("..").join("..")).as_bytes())?; writeln!(commondir_file)?; } { let mut gitdir_file = file_create_new(gitdir_file_path)?; - gitdir_file.write_all(worktree_dotgit_path.as_os_str().as_encoded_bytes())?; + gitdir_file.write_all(format_gitfile_path(&worktree_dotgit_path).as_bytes())?; writeln!(gitdir_file)?; } @@ -343,7 +435,7 @@ pub fn git_worktree_add( { let mut dot_git = file_create_new(&worktree_dotgit_path)?; write!(dot_git, "gitdir: ")?; - dot_git.write_all(worktree_data.as_os_str().as_encoded_bytes())?; + dot_git.write_all(format_gitfile_path(&worktree_data).as_bytes())?; writeln!(dot_git)?; }