fix: Normalize filesystem paths with `dunce`

Microsoft Windows has Universal Naming Convention (UNC) paths, see
<https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#unc-paths>

The Rust standard library normalizes paths to this form when calling
`std::fs::canonicalize`, see
<https://doc.rust-lang.org/std/fs/fn.canonicalize.html>

However, some programs, do not parse these paths correctly, including
our ally `git`:

    $ git clone \\?\C:\Users\lorenz\tmp
    Cloning into 'tmp'...
    ssh: Could not resolve hostname \\\\?\\c: No such host is known.
    fatal: Could not read from remote repository.

    Please make sure you have the correct access rights
    and the repository exists.

The `dunce` crate solves the problem of having to deal with
normalization and avoiding weird UNC paths for us. Note that on
non-Windows platforms, it *behaves exactly like*
`std::fs::canonicalize`!
This commit is contained in:
Lorenz Leutgeb 2025-08-22 10:48:48 +02:00 committed by Fintan Halpenny
parent 31039bbceb
commit 5229fb8a59
9 changed files with 18 additions and 10 deletions

9
Cargo.lock generated
View File

@ -758,6 +758,12 @@ dependencies = [
"litrs", "litrs",
] ]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.17" version = "1.0.17"
@ -2662,6 +2668,7 @@ dependencies = [
"colored", "colored",
"crossbeam-channel", "crossbeam-channel",
"cyphernet", "cyphernet",
"dunce",
"emojis", "emojis",
"fast-glob", "fast-glob",
"fastrand", "fastrand",
@ -2698,6 +2705,7 @@ version = "0.15.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"dunce",
"git-ref-format", "git-ref-format",
"human-panic", "human-panic",
"lexopt", "lexopt",
@ -2901,6 +2909,7 @@ dependencies = [
name = "radicle-remote-helper" name = "radicle-remote-helper"
version = "0.12.0" version = "0.12.0"
dependencies = [ dependencies = [
"dunce",
"log", "log",
"radicle", "radicle",
"radicle-cli", "radicle-cli",

View File

@ -27,6 +27,7 @@ chrono = { version = "0.4.26", default-features = false }
colored = "2.1.0" colored = "2.1.0"
crossbeam-channel = "0.5.6" crossbeam-channel = "0.5.6"
cyphernet = "0.5.2" cyphernet = "0.5.2"
dunce = "1.0.5"
fastrand = { version = "2.0.0", default-features = false } fastrand = { version = "2.0.0", default-features = false }
git2 = { version = "0.19.0", default-features = false } git2 = { version = "0.19.0", default-features = false }
human-panic = "2" human-panic = "2"

View File

@ -16,6 +16,7 @@ path = "src/main.rs"
[dependencies] [dependencies]
anyhow = { workspace = true } anyhow = { workspace = true }
chrono = { workspace = true, features = ["clock", "std"] } chrono = { workspace = true, features = ["clock", "std"] }
dunce = { workspace = true }
git-ref-format = { version = "0.3.0", features = ["macro"] } git-ref-format = { version = "0.3.0", features = ["macro"] }
human-panic.workspace = true human-panic.workspace = true
lexopt = { workspace = true } lexopt = { workspace = true }

View File

@ -212,10 +212,7 @@ pub fn init(
options: Options, options: Options,
profile: &profile::Profile, profile: &profile::Profile,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let path = repo let path = dunce::canonicalize(repo.workdir().unwrap_or_else(|| repo.path()))?;
.workdir()
.unwrap_or_else(|| repo.path())
.canonicalize()?;
let interactive = options.interactive; let interactive = options.interactive;
let default_branch = match find_default_branch(&repo) { let default_branch = match find_default_branch(&repo) {

View File

@ -14,6 +14,7 @@ name = "git-remote-rad"
path = "src/git-remote-rad.rs" path = "src/git-remote-rad.rs"
[dependencies] [dependencies]
dunce = { workspace = true }
log = { workspace = true } log = { workspace = true }
radicle = { workspace = true } radicle = { workspace = true }
radicle-cli = { workspace = true } radicle-cli = { workspace = true }

View File

@ -162,7 +162,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
// N.b. `working` is the `.git` folder and `fetch::run` // N.b. `working` is the `.git` folder and `fetch::run`
// requires the working directory. // requires the working directory.
let working = working.map_err(|_| Error::NoGitDir)?.canonicalize()?; let working = dunce::canonicalize(working.map_err(|_| Error::NoGitDir)?)?;
let working = working.parent().ok_or_else(|| Error::NoWorkingCopy { let working = working.parent().ok_or_else(|| Error::NoWorkingCopy {
path: working.clone(), path: working.clone(),
})?; })?;

View File

@ -22,6 +22,7 @@ chrono = { workspace = true, features = ["clock"], optional = true }
colored = { workspace = true, optional = true } colored = { workspace = true, optional = true }
crossbeam-channel = { workspace = true } crossbeam-channel = { workspace = true }
cyphernet = { workspace = true, features = ["tor", "dns", "p2p-ed25519"] } cyphernet = { workspace = true, features = ["tor", "dns", "p2p-ed25519"] }
dunce = { workspace = true }
fast-glob = { version = "0.3.2" } fast-glob = { version = "0.3.2" }
fastrand = { workspace = true } fastrand = { workspace = true }
git2 = { workspace = true, features = ["vendored-libgit2"] } git2 = { workspace = true, features = ["vendored-libgit2"] }

View File

@ -541,7 +541,7 @@ impl Home {
fs::create_dir_all(path.clone())?; fs::create_dir_all(path.clone())?;
} }
let home = Self { let home = Self {
path: path.canonicalize()?, path: dunce::canonicalize(path)?,
}; };
for dir in &[home.storage(), home.keys(), home.node(), home.cobs()] { for dir in &[home.storage(), home.keys(), home.node(), home.cobs()] {

View File

@ -127,7 +127,7 @@ where
})?, })?,
[ [
"push", "push",
&format!("{}", stored.path().canonicalize()?.display()), &format!("{}", dunce::canonicalize(stored.path())?.display()),
&pushspec.to_string(), &pushspec.to_string(),
], ],
[], [],
@ -295,9 +295,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
"fetch", "fetch",
&format!( &format!(
"{}", "{}",
stored dunce::canonicalize(stored.path())
.path()
.canonicalize()
.map_err(CheckoutError::Fetch)? .map_err(CheckoutError::Fetch)?
.display() .display()
), ),