Skip to content

find git.exe harder on Windows #1419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions gitoxide-core/src/repository/attributes/validate_baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -254,8 +254,6 @@ pub(crate) mod function {
}
}

static GIT_NAME: &str = if cfg!(windows) { "git.exe" } else { "git" };

enum Baseline {
Attribute { assignments: Vec<gix::attrs::Assignment> },
Exclude { location: Option<ExcludeLocation> },
Expand Down
3 changes: 2 additions & 1 deletion gix-credentials/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }



Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions gix-credentials/src/program/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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()
Expand Down
18 changes: 10 additions & 8 deletions gix-credentials/tests/program/from_custom_definition.rs
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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"
);
}
Expand All @@ -36,32 +35,35 @@ 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""#)
);
}

#[test]
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""#)
);
}

#[test]
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"
);
}
Expand Down
56 changes: 47 additions & 9 deletions gix-path/src/env/git.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,67 @@
use std::path::PathBuf;
use std::{
path::Path,
process::{Command, Stdio},
};

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<Option<BString>> = 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<Option<BString>> = 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) {
exec_path.push("etc");
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)
}
Expand All @@ -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")
Expand Down
38 changes: 36 additions & 2 deletions gix-path/src/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{
path::{Path, PathBuf},
};

use crate::env::git::EXE_NAME;
use bstr::{BString, ByteSlice};

mod git;
Expand All @@ -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<Option<PathBuf>> = 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(<name>)` to learn where these bases are.
///
Expand Down Expand Up @@ -55,7 +89,7 @@ pub fn xdg_config(file: &str, env_var: &mut dyn FnMut(&str) -> Option<OsString>)
///
/// ### 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
Expand All @@ -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;
Expand Down
72 changes: 55 additions & 17 deletions gix-path/tests/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading