Skip to content

Commit 6d18639

Browse files
committed
feat: provide env::executable_invocation() to know how to invoke Git.
That way we can make it easier to rely on Git even if finding it is a bit more involved.
1 parent f71b7a0 commit 6d18639

File tree

3 files changed

+127
-27
lines changed

3 files changed

+127
-27
lines changed

gix-path/src/env/git.rs

+47-9
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,67 @@
1+
use std::path::PathBuf;
12
use std::{
23
path::Path,
34
process::{Command, Stdio},
45
};
56

67
use bstr::{BStr, BString, ByteSlice};
78

9+
/// Other places to find Git in.
10+
#[cfg(windows)]
11+
pub(super) static ALTERNATIVE_LOCATIONS: &[&str] = &[
12+
"C:/Program Files/Git/mingw64/bin",
13+
"C:/Program Files (x86)/Git/mingw32/bin",
14+
];
15+
#[cfg(not(windows))]
16+
pub(super) static ALTERNATIVE_LOCATIONS: &[&str] = &[];
17+
18+
#[cfg(windows)]
19+
pub(super) static EXE_NAME: &str = "git.exe";
20+
#[cfg(not(windows))]
21+
pub(super) static EXE_NAME: &str = "git";
22+
23+
/// Invoke the git executable in PATH to obtain the origin configuration, which is cached and returned.
24+
pub(super) static EXE_INFO: once_cell::sync::Lazy<Option<BString>> = once_cell::sync::Lazy::new(|| {
25+
let git_cmd = |executable: PathBuf| {
26+
let mut cmd = Command::new(executable);
27+
cmd.args(["config", "-l", "--show-origin"])
28+
.stdin(Stdio::null())
29+
.stderr(Stdio::null());
30+
cmd
31+
};
32+
let mut cmd = git_cmd(EXE_NAME.into());
33+
gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path");
34+
let cmd_output = match cmd.output() {
35+
Ok(out) => out.stdout,
36+
#[cfg(windows)]
37+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
38+
let executable = ALTERNATIVE_LOCATIONS.into_iter().find_map(|prefix| {
39+
let candidate = Path::new(prefix).join(EXE_NAME);
40+
candidate.is_file().then_some(candidate)
41+
})?;
42+
gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path in alternate location");
43+
git_cmd(executable).output().ok()?.stdout
44+
}
45+
Err(_) => return None,
46+
};
47+
48+
first_file_from_config_with_origin(cmd_output.as_slice().into()).map(ToOwned::to_owned)
49+
});
50+
851
/// Returns the file that contains git configuration coming with the installation of the `git` file in the current `PATH`, or `None`
952
/// if no `git` executable was found or there were other errors during execution.
10-
pub(crate) fn install_config_path() -> Option<&'static BStr> {
53+
pub(super) fn install_config_path() -> Option<&'static BStr> {
1154
let _span = gix_trace::detail!("gix_path::git::install_config_path()");
1255
static PATH: once_cell::sync::Lazy<Option<BString>> = once_cell::sync::Lazy::new(|| {
13-
// Shortcut: in Msys shells this variable is set which allows to deduce the installation directory
56+
// Shortcut: in Msys shells this variable is set which allows to deduce the installation directory,
1457
// so we can save the `git` invocation.
1558
#[cfg(windows)]
1659
if let Some(mut exec_path) = std::env::var_os("EXEPATH").map(std::path::PathBuf::from) {
1760
exec_path.push("etc");
1861
exec_path.push("gitconfig");
1962
return crate::os_string_into_bstring(exec_path.into()).ok();
2063
}
21-
let mut cmd = Command::new(if cfg!(windows) { "git.exe" } else { "git" });
22-
cmd.args(["config", "-l", "--show-origin"])
23-
.stdin(Stdio::null())
24-
.stderr(Stdio::null());
25-
gix_trace::debug!(cmd = ?cmd, "invoking git for installation config path");
26-
first_file_from_config_with_origin(cmd.output().ok()?.stdout.as_slice().into()).map(ToOwned::to_owned)
64+
EXE_INFO.clone()
2765
});
2866
PATH.as_ref().map(AsRef::as_ref)
2967
}
@@ -35,7 +73,7 @@ fn first_file_from_config_with_origin(source: &BStr) -> Option<&BStr> {
3573
}
3674

3775
/// Given `config_path` as obtained from `install_config_path()`, return the path of the git installation base.
38-
pub(crate) fn config_to_base_path(config_path: &Path) -> &Path {
76+
pub(super) fn config_to_base_path(config_path: &Path) -> &Path {
3977
config_path
4078
.parent()
4179
.expect("config file paths always have a file name to pop")

gix-path/src/env/mod.rs

+25-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{
33
path::{Path, PathBuf},
44
};
55

6+
use crate::env::git::EXE_NAME;
67
use bstr::{BString, ByteSlice};
78

89
mod git;
@@ -27,6 +28,29 @@ pub fn installation_config_prefix() -> Option<&'static Path> {
2728
installation_config().map(git::config_to_base_path)
2829
}
2930

31+
/// Return the name of the Git executable to invoke it.
32+
///
33+
/// Note that on Windows, we will find the executable in the PATH and return it when found.
34+
/// and trying alternate locations, whereas on other platforms it will just return the name of the
35+
/// executable to find in the `PATH` naturally when invoking the command.
36+
pub fn executable_invocation() -> &'static Path {
37+
if cfg!(windows) {
38+
/// The path to the Git executable as located in the `PATH` or in other locations that it's known to be installed to.
39+
/// It's `None` if environment variables couldn't be read or if no executable could be found.
40+
static EXECUTABLE_PATH: once_cell::sync::Lazy<Option<PathBuf>> = once_cell::sync::Lazy::new(|| {
41+
std::env::split_paths(&std::env::var_os("PATH")?)
42+
.chain(git::ALTERNATIVE_LOCATIONS.iter().map(Into::into))
43+
.find_map(|prefix| {
44+
let full_path = prefix.join(EXE_NAME);
45+
full_path.is_file().then_some(full_path)
46+
})
47+
});
48+
EXECUTABLE_PATH.as_deref().unwrap_or(Path::new(git::EXE_NAME))
49+
} else {
50+
Path::new("git")
51+
}
52+
}
53+
3054
/// Returns the fully qualified path in the *xdg-home* directory (or equivalent in the home dir) to `file`,
3155
/// accessing `env_var(<name>)` to learn where these bases are.
3256
///
@@ -74,7 +98,7 @@ pub fn system_prefix() -> Option<&'static Path> {
7498
}
7599
}
76100

77-
let mut cmd = std::process::Command::new("git.exe");
101+
let mut cmd = std::process::Command::new(executable_invocation());
78102
cmd.arg("--exec-path").stderr(std::process::Stdio::null());
79103
gix_trace::debug!(cmd = ?cmd, "invoking git to get system prefix/exec path");
80104
let path = cmd.output().ok()?.stdout;

gix-path/tests/path.rs

+55-17
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,67 @@ mod home_dir {
1414
}
1515
}
1616

17-
mod xdg_config_path {
18-
use std::ffi::OsStr;
17+
mod env {
18+
#[test]
19+
fn executable_invocation() {
20+
let actual = gix_path::env::executable_invocation();
21+
assert!(
22+
!actual.as_os_str().is_empty(),
23+
"it finds something as long as git is installed somewhere on the system (or a default location)"
24+
);
25+
}
26+
27+
#[test]
28+
fn installation_config() {
29+
assert_ne!(
30+
gix_path::env::installation_config().map(|p| p.components().count()),
31+
gix_path::env::installation_config_prefix().map(|p| p.components().count()),
32+
"the prefix is a bit shorter than the installation config path itself"
33+
);
34+
}
1935

2036
#[test]
21-
fn prefers_xdg_config_bases() {
22-
let actual = gix_path::env::xdg_config("test", &mut |n| {
23-
(n == OsStr::new("XDG_CONFIG_HOME")).then(|| "marker".into())
24-
})
25-
.expect("set");
26-
#[cfg(unix)]
27-
assert_eq!(actual.to_str(), Some("marker/git/test"));
28-
#[cfg(windows)]
29-
assert_eq!(actual.to_str(), Some("marker\\git\\test"));
37+
fn system_prefix() {
38+
assert_ne!(
39+
gix_path::env::system_prefix(),
40+
None,
41+
"git should be present when running tests"
42+
);
3043
}
3144

3245
#[test]
33-
fn falls_back_to_home() {
34-
let actual = gix_path::env::xdg_config("test", &mut |n| (n == OsStr::new("HOME")).then(|| "marker".into()))
46+
fn home_dir() {
47+
assert_ne!(
48+
gix_path::env::home_dir(),
49+
None,
50+
"we find a home on every system these tests execute"
51+
);
52+
}
53+
54+
mod xdg_config {
55+
use std::ffi::OsStr;
56+
57+
#[test]
58+
fn prefers_xdg_config_bases() {
59+
let actual = gix_path::env::xdg_config("test", &mut |n| {
60+
(n == OsStr::new("XDG_CONFIG_HOME")).then(|| "marker".into())
61+
})
3562
.expect("set");
36-
#[cfg(unix)]
37-
assert_eq!(actual.to_str(), Some("marker/.config/git/test"));
38-
#[cfg(windows)]
39-
assert_eq!(actual.to_str(), Some("marker\\.config\\git\\test"));
63+
#[cfg(unix)]
64+
assert_eq!(actual.to_str(), Some("marker/git/test"));
65+
#[cfg(windows)]
66+
assert_eq!(actual.to_str(), Some("marker\\git\\test"));
67+
}
68+
69+
#[test]
70+
fn falls_back_to_home() {
71+
let actual = gix_path::env::xdg_config("test", &mut |n| (n == OsStr::new("HOME")).then(|| "marker".into()))
72+
.expect("set");
73+
#[cfg(unix)]
74+
assert_eq!(actual.to_str(), Some("marker/.config/git/test"));
75+
#[cfg(windows)]
76+
assert_eq!(actual.to_str(), Some("marker\\.config\\git\\test"));
77+
}
4078
}
4179
}
4280
mod util;

0 commit comments

Comments
 (0)