Skip to content

Commit

Permalink
git: fix windows gitfile paths
Browse files Browse the repository at this point in the history
Before

    gitdir: \\?\C:\Users\runneradmin\AppData\Local\Temp\jj-test-fy26a2\second

After

    gitdir: C:/Users/runneradmin/AppData/Local/Temp/jj-test-fy26a2/second

And same for the other worktree bits and pieces. We also change the way
the tests run git commands, because of yet another error to do with
Git being unable to handle verbatim paths:

    command=`"git" "worktree" "add" "\\\\?\\C:\\Users\\runneradmin\\AppData\\Local\\Temp\\jj-test-ti0MHE\\second"`
    code=128
    stdout=""
    stderr=```
    Preparing worktree (new branch \'second\')
    fatal: could not create leading directories of \'//?/C:/Users/runneradmin/AppData/Local/Temp/jj-test-ti0MHE/second/.git\': Invalid argument
  • Loading branch information
cormacrelf committed Oct 19, 2024
1 parent da96e63 commit 23f8317
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 11 deletions.
7 changes: 5 additions & 2 deletions cli/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions cli/tests/test_git_colocated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1317,24 +1317,21 @@ 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
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]);
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();
Expand Down
26 changes: 26 additions & 0 deletions lib/LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,29 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


-----

src/git_paths.rs is licensed under the MIT license:

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.

11 changes: 8 additions & 3 deletions lib/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use itertools::Itertools;
use tempfile::NamedTempFile;
use thiserror::Error;

use super::git_path_slash;
use crate::backend::BackendError;
use crate::backend::CommitId;
use crate::commit::Commit;
Expand Down Expand Up @@ -268,6 +269,10 @@ impl CreateWorktreeError {
}
}

pub fn format_gitfile_path(path: &Path) -> Cow<'_, str> {
return git_path_slash::to_slash_lossy(path);
}

/// `git worktree add` implementation
///
/// This function has to be implemented from scratch.
Expand Down Expand Up @@ -317,12 +322,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)?;
}

Expand All @@ -342,7 +347,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)?;
}

Expand Down
81 changes: 81 additions & 0 deletions lib/src/git_path_slash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//! This is from <https://github.com/rhysd/path-slash>, but tweaked to
//! change any verbatim prefix to non-verbatim, as git does.
//!
//! This is MIT licensed. See lib/LICENSE for a copy of the license.
use std::borrow::Cow;
use std::path::Path;

#[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)
}
}

/// On non-Windows targets, this is just [Path::to_string_lossy].
///
/// On Windows targets, this converts backslashes to forward slashes,
/// and removes the `\\?\` verbatim prefix.
#[cfg(not(target_os = "windows"))]
pub fn to_slash_lossy(path: &Path) -> Cow<'_, str> {
path.to_string_lossy()
}

/// On non-Windows targets, this is just [Path::to_string_lossy].
///
/// On Windows targets, this converts backslashes to forward slashes,
/// and removes the `\\?\` verbatim prefix.
#[cfg(target_os = "windows")]
pub 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)
}
2 changes: 2 additions & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub mod fsmonitor;
pub mod git;
#[cfg(feature = "git")]
pub mod git_backend;
#[cfg(feature = "git")]
pub mod git_path_slash;
pub mod gitignore;
pub mod gpg_signing;
pub mod graph;
Expand Down

0 comments on commit 23f8317

Please sign in to comment.