use std::{
env,
path::{Path, PathBuf},
process::{Command, Stdio},
};
use bstr::{BStr, BString, ByteSlice};
use std::sync::LazyLock;
#[cfg(windows)]
pub(super) static ALTERNATIVE_LOCATIONS: LazyLock<Vec<PathBuf>> =
LazyLock::new(|| locations_under_program_files(|key| std::env::var_os(key)));
#[cfg(not(windows))]
pub(super) static ALTERNATIVE_LOCATIONS: LazyLock<Vec<PathBuf>> = LazyLock::new(Vec::new);
#[cfg(windows)]
fn locations_under_program_files<F>(var_os_func: F) -> Vec<PathBuf>
where
F: Fn(&str) -> Option<std::ffi::OsString>,
{
let varname_64bit = "ProgramW6432";
let varname_x86 = "ProgramFiles(x86)";
let varname_current = "ProgramFiles";
let varname_user_appdata_local = "LocalAppData";
let suffixes_64 = &[r"Git\clangarm64\bin", r"Git\mingw64\bin"][..];
let suffixes_32 = &[r"Git\mingw32\bin"][..];
#[cfg(target_pointer_width = "64")]
let suffixes_current = suffixes_64;
#[cfg(target_pointer_width = "32")]
let suffixes_current = suffixes_32;
let suffixes_user = &[
r"Programs\Git\clangarm64\bin",
r"Programs\Git\mingw64\bin",
r"Programs\Git\mingw32\bin",
][..];
let rules = [
(varname_user_appdata_local, suffixes_user),
(varname_64bit, suffixes_64),
(varname_x86, suffixes_32),
(varname_current, suffixes_current),
];
let mut locations = vec![];
for (varname, suffixes) in rules {
let Some(program_files_dir) = var_os_func(varname).map(PathBuf::from).filter(|p| p.is_absolute()) else {
continue;
};
for suffix in suffixes {
let location = program_files_dir.join(suffix);
if !locations.contains(&location) {
locations.push(location);
}
}
}
locations
}
#[cfg(windows)]
pub(super) const EXE_NAME: &str = "git.exe";
#[cfg(not(windows))]
pub(super) const EXE_NAME: &str = "git";
pub(super) static GIT_HIGHEST_SCOPE_CONFIG_PATH: LazyLock<Option<BString>> = LazyLock::new(exe_info);
#[cfg(windows)]
const NULL_DEVICE: &str = "nul";
#[cfg(not(windows))]
const NULL_DEVICE: &str = "/dev/null";
fn exe_info() -> Option<BString> {
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.iter().find_map(|prefix| {
let candidate = 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)
}
fn git_cmd(executable: PathBuf) -> Command {
let mut cmd = Command::new(executable);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
let cwd = if cfg!(windows) {
env::var_os("SystemRoot")
.or_else(|| env::var_os("windir"))
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.unwrap_or_else(env::temp_dir)
} else {
"/".into()
};
cmd.args(["config", "-lz", "--show-origin", "--name-only"])
.current_dir(cwd)
.env_remove("GIT_CONFIG")
.env_remove("GIT_DISCOVERY_ACROSS_FILESYSTEM")
.env_remove("GIT_OBJECT_DIRECTORY")
.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
.env_remove("GIT_COMMON_DIR")
.env("GIT_DIR", NULL_DEVICE) .env("GIT_WORK_TREE", NULL_DEVICE) .stdin(Stdio::null())
.stderr(Stdio::null());
cmd
}
fn first_file_from_config_with_origin(source: &BStr) -> Option<&BStr> {
let file = source.strip_prefix(b"file:")?;
let end_pos = file.find_byte(b'\0')?;
file[..end_pos].as_bstr().into()
}
pub(super) fn install_config_path() -> Option<&'static BStr> {
let _span = gix_trace::detail!("gix_path::git::install_config_path()");
static PATH: LazyLock<Option<BString>> = LazyLock::new(|| {
#[cfg(windows)]
if let Some(mut exec_path) = std::env::var_os("EXEPATH").map(PathBuf::from) {
exec_path.push("etc");
exec_path.push("gitconfig");
return crate::os_string_into_bstring(exec_path.into()).ok();
}
GIT_HIGHEST_SCOPE_CONFIG_PATH.clone()
});
PATH.as_ref().map(AsRef::as_ref)
}
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")
}
#[cfg(test)]
mod tests;