Skip to content

Commit

Permalink
Merge branch 'git-executable'
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Jun 24, 2024
2 parents f71b7a0 + 5bf7f89 commit f0a4431
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 43 deletions.
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;

0 comments on commit f0a4431

Please sign in to comment.