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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ name = "git-remote-rad"
path = "src/git-remote-rad.rs"
[dependencies]
dunce = { workspace = true }
log = { workspace = true }
radicle = { 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`
// 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 {
path: working.clone(),
})?;

View File

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

View File

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

View File

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