diff --git a/Cargo.lock b/Cargo.lock index 1dbaf1d9568..2c65e3175af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -967,9 +967,9 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "git2" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ "bitflags 2.6.0", "libc", @@ -1879,6 +1879,7 @@ dependencies = [ "maplit", "minus", "once_cell", + "path-slash", "pest", "pest_derive", "pollster", @@ -1931,6 +1932,7 @@ dependencies = [ "maplit", "num_cpus", "once_cell", + "path-slash", "pest", "pest_derive", "pollster", @@ -2019,9 +2021,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libgit2-sys" -version = "0.16.2+1.7.2" +version = "0.17.0+1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" dependencies = [ "cc", "libc", @@ -2341,6 +2343,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + [[package]] name = "pathdiff" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 4545317d898..f78e71ce52a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ dunce = "1.0.4" either = "1.13.0" esl01-renderdag = "0.3.0" futures = "0.3.30" -git2 = { version = "0.18.3", features = [ +git2 = { version = "0.19.0", features = [ # Do *not* disable this feature even if you'd like dynamic linking. Instead, # set the environment variable `LIBGIT2_NO_VENDOR=1` if dynamic linking must # be used (this will override the Cargo feature), and allow static linking @@ -76,6 +76,7 @@ maplit = "1.0.2" minus = { version = "5.6.1", features = ["dynamic_output", "search"] } num_cpus = "1.16.0" once_cell = "1.19.0" +path-slash = "0.2.1" pest = "2.7.11" pest_derive = "2.7.11" pollster = "0.3.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ffb41e2997e..e8c6656c9f1 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -74,6 +74,7 @@ jj-lib = { workspace = true } maplit = { workspace = true } minus = { workspace = true } once_cell = { workspace = true } +path-slash = { workspace = true } pest = { workspace = true } pest_derive = { workspace = true } pollster = { workspace = true } diff --git a/cli/src/commands/git/clone.rs b/cli/src/commands/git/clone.rs index b7cfdf81011..9c5a4b9ed99 100644 --- a/cli/src/commands/git/clone.rs +++ b/cli/src/commands/git/clone.rs @@ -16,7 +16,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::{fs, io}; -use jj_lib::git::{self, GitFetchError, GitFetchStats}; +use jj_lib::git::{self, sanitize_git_url_if_path, GitFetchError, GitFetchStats}; use jj_lib::repo::Repo; use jj_lib::str_util::StringPattern; use jj_lib::workspace::Workspace; @@ -47,23 +47,6 @@ pub struct GitCloneArgs { colocate: bool, } -fn absolute_git_source(cwd: &Path, source: &str) -> String { - // Git appears to turn URL-like source to absolute path if local git directory - // exits, and fails because '$PWD/https' is unsupported protocol. Since it would - // be tedious to copy the exact git (or libgit2) behavior, we simply assume a - // source containing ':' is a URL, SSH remote, or absolute path with Windows - // drive letter. - if !source.contains(':') && Path::new(source).exists() { - // It's less likely that cwd isn't utf-8, so just fall back to original source. - cwd.join(source) - .into_os_string() - .into_string() - .unwrap_or_else(|_| source.to_owned()) - } else { - source.to_owned() - } -} - fn clone_destination_for_source(source: &str) -> Option<&str> { let destination = source.strip_suffix(".git").unwrap_or(source); let destination = destination.strip_suffix('/').unwrap_or(destination); @@ -89,7 +72,7 @@ pub fn cmd_git_clone( return Err(cli_error("--at-op is not respected")); } let remote_name = "origin"; - let source = absolute_git_source(command.cwd(), &args.source); + let source = sanitize_git_url_if_path(command.cwd(), &args.source); let wc_path_str = args .destination .as_deref() diff --git a/cli/src/commands/git/remote/add.rs b/cli/src/commands/git/remote/add.rs index a66a7faac89..986b35cad59 100644 --- a/cli/src/commands/git/remote/add.rs +++ b/cli/src/commands/git/remote/add.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use jj_lib::git; +use jj_lib::git::{self, sanitize_git_url_if_path}; use jj_lib::repo::Repo; use crate::cli_util::CommandHelper; @@ -37,6 +37,10 @@ pub fn cmd_git_remote_add( let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; - git::add_remote(&git_repo, &args.remote, &args.url)?; + git::add_remote( + &git_repo, + &args.remote, + &sanitize_git_url_if_path(command.cwd(), &args.url), + )?; Ok(()) } diff --git a/cli/src/commands/git/remote/set_url.rs b/cli/src/commands/git/remote/set_url.rs index cee2da0559f..ca386dbb2b9 100644 --- a/cli/src/commands/git/remote/set_url.rs +++ b/cli/src/commands/git/remote/set_url.rs @@ -37,6 +37,6 @@ pub fn cmd_git_remote_set_url( let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let git_repo = get_git_repo(repo.store())?; - git::set_remote_url(&git_repo, &args.remote, &args.url)?; + git::set_remote_url(&git_repo, &args.remote, &args.url, command.cwd())?; Ok(()) } diff --git a/cli/tests/common/mod.rs b/cli/tests/common/mod.rs index 72f4eb5a51e..32ce520b7fc 100644 --- a/cli/tests/common/mod.rs +++ b/cli/tests/common/mod.rs @@ -17,6 +17,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use itertools::Itertools as _; +use path_slash::PathBufExt; use regex::{Captures, Regex}; use tempfile::TempDir; @@ -320,13 +321,14 @@ 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()), + regex::escape(&self.env_root.to_slash_lossy()) )) .unwrap(); regex .replace_all(&text, |caps: &Captures| { - format!("$TEST_ENV{}", caps[1].replace('\\', "/")) + format!("$TEST_ENV{}", caps[2].replace('\\', "/")) }) .to_string() } diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 57503317856..b0a7189de81 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -52,6 +52,7 @@ itertools = { workspace = true } jj-lib-proc-macros = { workspace = true } maplit = { workspace = true } once_cell = { workspace = true } +path-slash = { workspace = true } pest = { workspace = true } pest_derive = { workspace = true } pollster = { workspace = true } diff --git a/lib/src/git.rs b/lib/src/git.rs index d7066a22f73..79f30544a25 100644 --- a/lib/src/git.rs +++ b/lib/src/git.rs @@ -18,11 +18,12 @@ use std::borrow::Cow; use std::collections::{BTreeMap, HashMap, HashSet}; use std::default::Default; use std::io::Read; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::{fmt, str}; use git2::Oid; use itertools::Itertools; +use path_slash::PathBufExt; use tempfile::NamedTempFile; use thiserror::Error; @@ -1143,10 +1144,42 @@ pub fn rename_remote( Ok(()) } +fn looks_like_a_path_to_git(source: &str) -> bool { + source.chars().nth(1) == Some(':') || // Looks like a Windows C:... path + // Match Git's behavior, the URL syntax [is only recognized if there are no + // slashes before the first + // colon](https://git-scm.com/docs/git-clone#_git_urls) + !source + .split_once("/") + .map_or(source, |(first, _)| first) + .contains(":") +} + +pub fn sanitize_git_url_if_path(cwd: &Path, source: &str) -> String { + // Git appears to turn URL-like source to absolute path if local git directory + // exits, and fails because '$PWD/https' is unsupported protocol. Since it would + // be tedious to copy the exact git (or libgit2) behavior, we simply assume a + // source containing ':' is a URL, SSH remote, or absolute path with Windows + // drive letter. + if looks_like_a_path_to_git(source) { + // TODO: This won't work for Windows UNC path or (less importantly) if the + // original source is a non-UTF Windows path. For Windows UNC paths, we + // could use dunce, though see also https://gitlab.com/kornelski/dunce/-/issues/7 + cwd.join(source).to_slash().map_or_else( + // It's less likely that cwd isn't utf-8, so just fall back to original source. + || source.to_owned(), + |s| s.to_string(), + ) + } else { + source.to_owned() + } +} + pub fn set_remote_url( git_repo: &git2::Repository, remote_name: &str, new_remote_url: &str, + cwd: &Path, ) -> Result<(), GitRemoteManagementError> { if remote_name == REMOTE_NAME_FOR_LOCAL_GIT_REPO { return Err(GitRemoteManagementError::RemoteReservedForLocalGitRepo); @@ -1164,7 +1197,7 @@ pub fn set_remote_url( })?; git_repo - .remote_set_url(remote_name, new_remote_url) + .remote_set_url(remote_name, &sanitize_git_url_if_path(cwd, new_remote_url)) .map_err(GitRemoteManagementError::InternalGitError)?; Ok(()) } @@ -1265,7 +1298,12 @@ pub fn fetch( tracing::debug!("remote.prune"); remote.prune(None)?; tracing::debug!("remote.update_tips"); - remote.update_tips(None, false, git2::AutotagOption::Unspecified, None)?; + remote.update_tips( + None, + git2::RemoteUpdateFlags::empty(), + git2::AutotagOption::Unspecified, + None, + )?; // TODO: We could make it optional to get the default branch since we only care // about it on clone. let mut default_branch = None;