Skip to content

Commit f0a4431

Browse files
committed
Merge branch 'git-executable'
2 parents f71b7a0 + 5bf7f89 commit f0a4431

File tree

8 files changed

+155
-43
lines changed

8 files changed

+155
-43
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gitoxide-core/src/repository/attributes/validate_baseline.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub(crate) mod function {
7474
let tx_base = tx_base.clone();
7575
let mut progress = progress.add_child("attributes");
7676
move || -> anyhow::Result<()> {
77-
let mut child = std::process::Command::new(GIT_NAME)
77+
let mut child = std::process::Command::new(gix::path::env::exe_invocation())
7878
.args(["check-attr", "--stdin", "-a"])
7979
.stdin(std::process::Stdio::piped())
8080
.stdout(std::process::Stdio::piped())
@@ -125,7 +125,7 @@ pub(crate) mod function {
125125
let tx_base = tx_base.clone();
126126
let mut progress = progress.add_child("excludes");
127127
move || -> anyhow::Result<()> {
128-
let mut child = std::process::Command::new(GIT_NAME)
128+
let mut child = std::process::Command::new(gix::path::env::exe_invocation())
129129
.args(["check-ignore", "--stdin", "-nv", "--no-index"])
130130
.stdin(std::process::Stdio::piped())
131131
.stdout(std::process::Stdio::piped())
@@ -254,8 +254,6 @@ pub(crate) mod function {
254254
}
255255
}
256256

257-
static GIT_NAME: &str = if cfg!(windows) { "git.exe" } else { "git" };
258-
259257
enum Baseline {
260258
Attribute { assignments: Vec<gix::attrs::Assignment> },
261259
Exclude { location: Option<ExcludeLocation> },

gix-credentials/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ gix-trace = { version = "^0.1.8", path = "../gix-trace" }
2727

2828
thiserror = "1.0.32"
2929
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
30-
bstr = { version = "1.3.0", default-features = false, features = ["std"]}
30+
bstr = { version = "1.3.0", default-features = false, features = ["std"] }
3131

3232

3333

@@ -36,6 +36,7 @@ document-features = { version = "0.2.1", optional = true }
3636
[dev-dependencies]
3737
gix-testtools = { path = "../tests/tools" }
3838
gix-sec = { path = "../gix-sec" }
39+
once_cell = "1.19.0"
3940

4041
[package.metadata.docs.rs]
4142
all-features = true

gix-credentials/src/program/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ impl Program {
6868

6969
/// Convert the program into the respective command, suitable to invoke `action`.
7070
pub fn to_command(&self, action: &helper::Action) -> std::process::Command {
71-
let git_program = cfg!(windows).then(|| "git.exe").unwrap_or("git");
71+
let git_program = gix_path::env::exe_invocation();
7272
let mut cmd = match &self.kind {
7373
Kind::Builtin => {
7474
let mut cmd = Command::new(git_program);
@@ -79,7 +79,7 @@ impl Program {
7979
let mut args = name_and_args.clone();
8080
args.insert_str(0, "credential-");
8181
args.insert_str(0, " ");
82-
args.insert_str(0, git_program);
82+
args.insert_str(0, git_program.to_string_lossy().as_ref());
8383
gix_command::prepare(gix_path::from_bstr(args.as_ref()).into_owned())
8484
.arg(action.as_arg(true))
8585
.with_shell_allow_argument_splitting()

gix-credentials/tests/program/from_custom_definition.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
use gix_credentials::{helper, program::Kind, Program};
22

3-
#[cfg(windows)]
4-
const GIT: &str = "git.exe";
5-
#[cfg(not(windows))]
6-
const GIT: &str = "git";
3+
static GIT: once_cell::sync::Lazy<&'static str> =
4+
once_cell::sync::Lazy::new(|| gix_path::env::exe_invocation().to_str().expect("not illformed"));
75

86
#[cfg(windows)]
97
const SH: &str = "sh";
@@ -13,10 +11,11 @@ const SH: &str = "/bin/sh";
1311
#[test]
1412
fn empty() {
1513
let prog = Program::from_custom_definition("");
14+
let git = *GIT;
1615
assert!(matches!(&prog.kind, Kind::ExternalName { name_and_args } if name_and_args == ""));
1716
assert_eq!(
1817
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
19-
format!(r#""{GIT}" "credential-" "store""#),
18+
format!(r#""{git}" "credential-" "store""#),
2019
"not useful, but allowed, would have to be caught elsewhere"
2120
);
2221
}
@@ -36,32 +35,35 @@ fn simple_script_in_path() {
3635
fn name_with_args() {
3736
let input = "name --arg --bar=\"a b\"";
3837
let prog = Program::from_custom_definition(input);
38+
let git = *GIT;
3939
assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input));
4040
assert_eq!(
4141
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
42-
format!(r#""{GIT}" "credential-name" "--arg" "--bar=a b" "store""#)
42+
format!(r#""{git}" "credential-name" "--arg" "--bar=a b" "store""#)
4343
);
4444
}
4545

4646
#[test]
4747
fn name_with_special_args() {
4848
let input = "name --arg --bar=~/folder/in/home";
4949
let prog = Program::from_custom_definition(input);
50+
let git = *GIT;
5051
assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input));
5152
assert_eq!(
5253
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
53-
format!(r#""{SH}" "-c" "{GIT} credential-name --arg --bar=~/folder/in/home \"$@\"" "--" "store""#)
54+
format!(r#""{SH}" "-c" "{git} credential-name --arg --bar=~/folder/in/home \"$@\"" "--" "store""#)
5455
);
5556
}
5657

5758
#[test]
5859
fn name() {
5960
let input = "name";
6061
let prog = Program::from_custom_definition(input);
62+
let git = *GIT;
6163
assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input));
6264
assert_eq!(
6365
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
64-
format!(r#""{GIT}" "credential-name" "store""#),
66+
format!(r#""{git}" "credential-name" "store""#),
6567
"we detect that this can run without shell, which is also more portable on windows"
6668
);
6769
}

gix-path/src/env/git.rs

Lines changed: 47 additions & 9 deletions
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

Lines changed: 36 additions & 2 deletions
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,39 @@ 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+
/// If it's in the `PATH`, it will always be a short name.
33+
///
34+
/// Note that on Windows, we will find the executable in the `PATH` if it exists there, or search it
35+
/// in alternative locations which when found yields the full path to it.
36+
pub fn exe_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+
.map(|exe_path| {
48+
let is_in_alternate_location = git::ALTERNATIVE_LOCATIONS
49+
.iter()
50+
.any(|prefix| exe_path.strip_prefix(prefix).is_ok());
51+
if is_in_alternate_location {
52+
exe_path
53+
} else {
54+
EXE_NAME.into()
55+
}
56+
})
57+
});
58+
EXECUTABLE_PATH.as_deref().unwrap_or(Path::new(git::EXE_NAME))
59+
} else {
60+
Path::new("git")
61+
}
62+
}
63+
3064
/// Returns the fully qualified path in the *xdg-home* directory (or equivalent in the home dir) to `file`,
3165
/// accessing `env_var(<name>)` to learn where these bases are.
3266
///
@@ -55,7 +89,7 @@ pub fn xdg_config(file: &str, env_var: &mut dyn FnMut(&str) -> Option<OsString>)
5589
///
5690
/// ### Performance
5791
///
58-
/// On windows, the slowest part is the launch of the `git.exe` executable in the PATH, which only happens when launched
92+
/// On windows, the slowest part is the launch of the Git executable in the PATH, which only happens when launched
5993
/// from outside of the `msys2` shell.
6094
///
6195
/// ### When `None` is returned
@@ -74,7 +108,7 @@ pub fn system_prefix() -> Option<&'static Path> {
74108
}
75109
}
76110

77-
let mut cmd = std::process::Command::new("git.exe");
111+
let mut cmd = std::process::Command::new(exe_invocation());
78112
cmd.arg("--exec-path").stderr(std::process::Stdio::null());
79113
gix_trace::debug!(cmd = ?cmd, "invoking git to get system prefix/exec path");
80114
let path = cmd.output().ok()?.stdout;

gix-path/tests/path.rs

Lines changed: 55 additions & 17 deletions
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 exe_invocation() {
20+
let actual = gix_path::env::exe_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)