diff --git a/Cargo.lock b/Cargo.lock index b63a375fd5b..6acc46ef52c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1583,6 +1583,7 @@ dependencies = [ "gix-testtools", "gix-trace 0.1.9", "gix-url", + "once_cell", "serde", "thiserror", ] diff --git a/gitoxide-core/src/repository/attributes/validate_baseline.rs b/gitoxide-core/src/repository/attributes/validate_baseline.rs index 686c668add6..82b69c82ef6 100644 --- a/gitoxide-core/src/repository/attributes/validate_baseline.rs +++ b/gitoxide-core/src/repository/attributes/validate_baseline.rs @@ -74,7 +74,7 @@ pub(crate) mod function { let tx_base = tx_base.clone(); let mut progress = progress.add_child("attributes"); move || -> anyhow::Result<()> { - let mut child = std::process::Command::new(GIT_NAME) + let mut child = std::process::Command::new(gix::path::env::exe_invocation()) .args(["check-attr", "--stdin", "-a"]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) @@ -125,7 +125,7 @@ pub(crate) mod function { let tx_base = tx_base.clone(); let mut progress = progress.add_child("excludes"); move || -> anyhow::Result<()> { - let mut child = std::process::Command::new(GIT_NAME) + let mut child = std::process::Command::new(gix::path::env::exe_invocation()) .args(["check-ignore", "--stdin", "-nv", "--no-index"]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) @@ -254,8 +254,6 @@ pub(crate) mod function { } } - static GIT_NAME: &str = if cfg!(windows) { "git.exe" } else { "git" }; - enum Baseline { Attribute { assignments: Vec }, Exclude { location: Option }, diff --git a/gix-credentials/Cargo.toml b/gix-credentials/Cargo.toml index bb170e0e4d7..a14a59f1264 100644 --- a/gix-credentials/Cargo.toml +++ b/gix-credentials/Cargo.toml @@ -27,7 +27,7 @@ gix-trace = { version = "^0.1.8", path = "../gix-trace" } thiserror = "1.0.32" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } -bstr = { version = "1.3.0", default-features = false, features = ["std"]} +bstr = { version = "1.3.0", default-features = false, features = ["std"] } @@ -36,6 +36,7 @@ document-features = { version = "0.2.1", optional = true } [dev-dependencies] gix-testtools = { path = "../tests/tools" } gix-sec = { path = "../gix-sec" } +once_cell = "1.19.0" [package.metadata.docs.rs] all-features = true diff --git a/gix-credentials/src/program/mod.rs b/gix-credentials/src/program/mod.rs index 213c0272128..5f705f93b5e 100644 --- a/gix-credentials/src/program/mod.rs +++ b/gix-credentials/src/program/mod.rs @@ -68,7 +68,7 @@ impl Program { /// Convert the program into the respective command, suitable to invoke `action`. pub fn to_command(&self, action: &helper::Action) -> std::process::Command { - let git_program = cfg!(windows).then(|| "git.exe").unwrap_or("git"); + let git_program = gix_path::env::exe_invocation(); let mut cmd = match &self.kind { Kind::Builtin => { let mut cmd = Command::new(git_program); @@ -79,7 +79,7 @@ impl Program { let mut args = name_and_args.clone(); args.insert_str(0, "credential-"); args.insert_str(0, " "); - args.insert_str(0, git_program); + args.insert_str(0, git_program.to_string_lossy().as_ref()); gix_command::prepare(gix_path::from_bstr(args.as_ref()).into_owned()) .arg(action.as_arg(true)) .with_shell_allow_argument_splitting() diff --git a/gix-credentials/tests/program/from_custom_definition.rs b/gix-credentials/tests/program/from_custom_definition.rs index d470deb17b2..4ef71f4ee4a 100644 --- a/gix-credentials/tests/program/from_custom_definition.rs +++ b/gix-credentials/tests/program/from_custom_definition.rs @@ -1,9 +1,7 @@ use gix_credentials::{helper, program::Kind, Program}; -#[cfg(windows)] -const GIT: &str = "git.exe"; -#[cfg(not(windows))] -const GIT: &str = "git"; +static GIT: once_cell::sync::Lazy<&'static str> = + once_cell::sync::Lazy::new(|| gix_path::env::exe_invocation().to_str().expect("not illformed")); #[cfg(windows)] const SH: &str = "sh"; @@ -13,10 +11,11 @@ const SH: &str = "/bin/sh"; #[test] fn empty() { let prog = Program::from_custom_definition(""); + let git = *GIT; assert!(matches!(&prog.kind, Kind::ExternalName { name_and_args } if name_and_args == "")); assert_eq!( format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), - format!(r#""{GIT}" "credential-" "store""#), + format!(r#""{git}" "credential-" "store""#), "not useful, but allowed, would have to be caught elsewhere" ); } @@ -36,10 +35,11 @@ fn simple_script_in_path() { fn name_with_args() { let input = "name --arg --bar=\"a b\""; let prog = Program::from_custom_definition(input); + let git = *GIT; assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input)); assert_eq!( format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), - format!(r#""{GIT}" "credential-name" "--arg" "--bar=a b" "store""#) + format!(r#""{git}" "credential-name" "--arg" "--bar=a b" "store""#) ); } @@ -47,10 +47,11 @@ fn name_with_args() { fn name_with_special_args() { let input = "name --arg --bar=~/folder/in/home"; let prog = Program::from_custom_definition(input); + let git = *GIT; assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input)); assert_eq!( format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), - format!(r#""{SH}" "-c" "{GIT} credential-name --arg --bar=~/folder/in/home \"$@\"" "--" "store""#) + format!(r#""{SH}" "-c" "{git} credential-name --arg --bar=~/folder/in/home \"$@\"" "--" "store""#) ); } @@ -58,10 +59,11 @@ fn name_with_special_args() { fn name() { let input = "name"; let prog = Program::from_custom_definition(input); + let git = *GIT; assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input)); assert_eq!( format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), - format!(r#""{GIT}" "credential-name" "store""#), + format!(r#""{git}" "credential-name" "store""#), "we detect that this can run without shell, which is also more portable on windows" ); } diff --git a/gix-path/src/env/git.rs b/gix-path/src/env/git.rs index 864a4d49aff..59996827961 100644 --- a/gix-path/src/env/git.rs +++ b/gix-path/src/env/git.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::{ path::Path, process::{Command, Stdio}, @@ -5,12 +6,54 @@ use std::{ use bstr::{BStr, BString, ByteSlice}; +/// Other places to find Git in. +#[cfg(windows)] +pub(super) static ALTERNATIVE_LOCATIONS: &[&str] = &[ + "C:/Program Files/Git/mingw64/bin", + "C:/Program Files (x86)/Git/mingw32/bin", +]; +#[cfg(not(windows))] +pub(super) static ALTERNATIVE_LOCATIONS: &[&str] = &[]; + +#[cfg(windows)] +pub(super) static EXE_NAME: &str = "git.exe"; +#[cfg(not(windows))] +pub(super) static EXE_NAME: &str = "git"; + +/// Invoke the git executable in PATH to obtain the origin configuration, which is cached and returned. +pub(super) static EXE_INFO: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { + let git_cmd = |executable: PathBuf| { + let mut cmd = Command::new(executable); + cmd.args(["config", "-l", "--show-origin"]) + .stdin(Stdio::null()) + .stderr(Stdio::null()); + cmd + }; + let mut cmd = git_cmd(EXE_NAME.into()); + gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path"); + let cmd_output = match cmd.output() { + Ok(out) => out.stdout, + #[cfg(windows)] + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + let executable = ALTERNATIVE_LOCATIONS.into_iter().find_map(|prefix| { + let candidate = Path::new(prefix).join(EXE_NAME); + candidate.is_file().then_some(candidate) + })?; + gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path in alternate location"); + git_cmd(executable).output().ok()?.stdout + } + Err(_) => return None, + }; + + first_file_from_config_with_origin(cmd_output.as_slice().into()).map(ToOwned::to_owned) +}); + /// Returns the file that contains git configuration coming with the installation of the `git` file in the current `PATH`, or `None` /// if no `git` executable was found or there were other errors during execution. -pub(crate) fn install_config_path() -> Option<&'static BStr> { +pub(super) fn install_config_path() -> Option<&'static BStr> { let _span = gix_trace::detail!("gix_path::git::install_config_path()"); static PATH: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { - // Shortcut: in Msys shells this variable is set which allows to deduce the installation directory + // Shortcut: in Msys shells this variable is set which allows to deduce the installation directory, // so we can save the `git` invocation. #[cfg(windows)] if let Some(mut exec_path) = std::env::var_os("EXEPATH").map(std::path::PathBuf::from) { @@ -18,12 +61,7 @@ pub(crate) fn install_config_path() -> Option<&'static BStr> { exec_path.push("gitconfig"); return crate::os_string_into_bstring(exec_path.into()).ok(); } - let mut cmd = Command::new(if cfg!(windows) { "git.exe" } else { "git" }); - cmd.args(["config", "-l", "--show-origin"]) - .stdin(Stdio::null()) - .stderr(Stdio::null()); - gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path"); - first_file_from_config_with_origin(cmd.output().ok()?.stdout.as_slice().into()).map(ToOwned::to_owned) + EXE_INFO.clone() }); PATH.as_ref().map(AsRef::as_ref) } @@ -35,7 +73,7 @@ fn first_file_from_config_with_origin(source: &BStr) -> Option<&BStr> { } /// Given `config_path` as obtained from `install_config_path()`, return the path of the git installation base. -pub(crate) fn config_to_base_path(config_path: &Path) -> &Path { +pub(super) fn config_to_base_path(config_path: &Path) -> &Path { config_path .parent() .expect("config file paths always have a file name to pop") diff --git a/gix-path/src/env/mod.rs b/gix-path/src/env/mod.rs index 1368815e751..06baff53db2 100644 --- a/gix-path/src/env/mod.rs +++ b/gix-path/src/env/mod.rs @@ -3,6 +3,7 @@ use std::{ path::{Path, PathBuf}, }; +use crate::env::git::EXE_NAME; use bstr::{BString, ByteSlice}; mod git; @@ -27,6 +28,39 @@ pub fn installation_config_prefix() -> Option<&'static Path> { installation_config().map(git::config_to_base_path) } +/// Return the name of the Git executable to invoke it. +/// If it's in the `PATH`, it will always be a short name. +/// +/// Note that on Windows, we will find the executable in the `PATH` if it exists there, or search it +/// in alternative locations which when found yields the full path to it. +pub fn exe_invocation() -> &'static Path { + if cfg!(windows) { + /// The path to the Git executable as located in the `PATH` or in other locations that it's known to be installed to. + /// It's `None` if environment variables couldn't be read or if no executable could be found. + static EXECUTABLE_PATH: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { + std::env::split_paths(&std::env::var_os("PATH")?) + .chain(git::ALTERNATIVE_LOCATIONS.iter().map(Into::into)) + .find_map(|prefix| { + let full_path = prefix.join(EXE_NAME); + full_path.is_file().then_some(full_path) + }) + .map(|exe_path| { + let is_in_alternate_location = git::ALTERNATIVE_LOCATIONS + .iter() + .any(|prefix| exe_path.strip_prefix(prefix).is_ok()); + if is_in_alternate_location { + exe_path + } else { + EXE_NAME.into() + } + }) + }); + EXECUTABLE_PATH.as_deref().unwrap_or(Path::new(git::EXE_NAME)) + } else { + Path::new("git") + } +} + /// Returns the fully qualified path in the *xdg-home* directory (or equivalent in the home dir) to `file`, /// accessing `env_var()` to learn where these bases are. /// @@ -55,7 +89,7 @@ pub fn xdg_config(file: &str, env_var: &mut dyn FnMut(&str) -> Option) /// /// ### Performance /// -/// On windows, the slowest part is the launch of the `git.exe` executable in the PATH, which only happens when launched +/// On windows, the slowest part is the launch of the Git executable in the PATH, which only happens when launched /// from outside of the `msys2` shell. /// /// ### When `None` is returned @@ -74,7 +108,7 @@ pub fn system_prefix() -> Option<&'static Path> { } } - let mut cmd = std::process::Command::new("git.exe"); + let mut cmd = std::process::Command::new(exe_invocation()); cmd.arg("--exec-path").stderr(std::process::Stdio::null()); gix_trace::debug!(cmd = ?cmd, "invoking git to get system prefix/exec path"); let path = cmd.output().ok()?.stdout; diff --git a/gix-path/tests/path.rs b/gix-path/tests/path.rs index 0bf9398056a..72cf2547b89 100644 --- a/gix-path/tests/path.rs +++ b/gix-path/tests/path.rs @@ -14,29 +14,67 @@ mod home_dir { } } -mod xdg_config_path { - use std::ffi::OsStr; +mod env { + #[test] + fn exe_invocation() { + let actual = gix_path::env::exe_invocation(); + assert!( + !actual.as_os_str().is_empty(), + "it finds something as long as git is installed somewhere on the system (or a default location)" + ); + } + + #[test] + fn installation_config() { + assert_ne!( + gix_path::env::installation_config().map(|p| p.components().count()), + gix_path::env::installation_config_prefix().map(|p| p.components().count()), + "the prefix is a bit shorter than the installation config path itself" + ); + } #[test] - fn prefers_xdg_config_bases() { - let actual = gix_path::env::xdg_config("test", &mut |n| { - (n == OsStr::new("XDG_CONFIG_HOME")).then(|| "marker".into()) - }) - .expect("set"); - #[cfg(unix)] - assert_eq!(actual.to_str(), Some("marker/git/test")); - #[cfg(windows)] - assert_eq!(actual.to_str(), Some("marker\\git\\test")); + fn system_prefix() { + assert_ne!( + gix_path::env::system_prefix(), + None, + "git should be present when running tests" + ); } #[test] - fn falls_back_to_home() { - let actual = gix_path::env::xdg_config("test", &mut |n| (n == OsStr::new("HOME")).then(|| "marker".into())) + fn home_dir() { + assert_ne!( + gix_path::env::home_dir(), + None, + "we find a home on every system these tests execute" + ); + } + + mod xdg_config { + use std::ffi::OsStr; + + #[test] + fn prefers_xdg_config_bases() { + let actual = gix_path::env::xdg_config("test", &mut |n| { + (n == OsStr::new("XDG_CONFIG_HOME")).then(|| "marker".into()) + }) .expect("set"); - #[cfg(unix)] - assert_eq!(actual.to_str(), Some("marker/.config/git/test")); - #[cfg(windows)] - assert_eq!(actual.to_str(), Some("marker\\.config\\git\\test")); + #[cfg(unix)] + assert_eq!(actual.to_str(), Some("marker/git/test")); + #[cfg(windows)] + assert_eq!(actual.to_str(), Some("marker\\git\\test")); + } + + #[test] + fn falls_back_to_home() { + let actual = gix_path::env::xdg_config("test", &mut |n| (n == OsStr::new("HOME")).then(|| "marker".into())) + .expect("set"); + #[cfg(unix)] + assert_eq!(actual.to_str(), Some("marker/.config/git/test")); + #[cfg(windows)] + assert_eq!(actual.to_str(), Some("marker\\.config\\git\\test")); + } } } mod util;