use crate::child_environment::environment_value;
use crate::real_git_candidate::{CandidateKind, classify_candidate};
use crate::real_git_record::service_unit_file;
pub use crate::real_git_resolution::{RecordedGit, ResolvedGit, resolve_real_git};
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
pub const DEFAULT_WINDOWS_PATH_EXTENSIONS: &str = ".COM;.EXE;.BAT;.CMD";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Platform {
Unix,
MacOs,
Windows,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolutionInputs {
pub platform: Platform,
pub path: OsString,
pub path_extensions: OsString,
pub current_directory: PathBuf,
pub common_paths: Vec<PathBuf>,
pub own_executable: PathBuf,
pub service_unit: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SearchedCandidate {
pub path: PathBuf,
pub kind: CandidateKind,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RealGitNotFound {
pub candidate_count: usize,
pub skipped_wrapper_count: usize,
pub searched: Vec<SearchedCandidate>,
pub path: OsString,
pub recorded: Option<RecordedGit>,
}
fn verdict_words(kind: CandidateKind) -> &'static str {
match kind {
CandidateKind::Wrapper => {
return "skipped, it leads back to cli-git (this executable, a link or copy of it, its toolchain launcher, or a script that starts it)";
}
CandidateKind::Unusable => return "skipped, it is missing, not an executable file, or unreadable",
CandidateKind::RealGit => return "usable",
}
}
impl std::fmt::Display for RealGitNotFound {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"Could not find a real Git executable after examining {} PATH candidates and \
skipping {} cli-git wrappers, so no Git command was run. PATH is {}.",
self.candidate_count,
self.skipped_wrapper_count,
if self.path.is_empty() { String::from("empty or unset") } else { self.path.to_string_lossy().into_owned() }
)?;
for candidate in &self.searched {
write!(formatter, "\n {}: {}", candidate.path.display(), verdict_words(candidate.kind))?;
}
if let Some(recorded) = &self.recorded {
write!(
formatter,
"\n {}: recorded by the commit service unit {} as the Git it starts, {}",
recorded.candidate.path.display(),
recorded.unit_file.display(),
verdict_words(recorded.candidate.kind)
)?;
}
return write!(
formatter,
"\ncli-git never starts itself as Git, so a candidate that leads back to cli-git is passed over on purpose. \
To fix it, install Git; or add the directory that holds the real Git executable (for example /usr/bin) to \
the PATH of the program that ran this command; or, when an entry of that PATH should not be used, remove it."
);
}
}
impl std::error::Error for RealGitNotFound {}
pub fn executable_names(platform: Platform, path_extensions: &OsStr) -> Vec<OsString> {
if platform != Platform::Windows {
return vec![OsString::from("git")];
}
let mut names: Vec<OsString> = Vec::<OsString>::new();
for piece in path_extensions.as_encoded_bytes().split(is_semicolon) {
if piece.is_empty() {
continue;
}
let Ok(extension) = std::str::from_utf8(piece) else {
continue;
};
if extension.starts_with('.') {
names.push(OsString::from(format!("git{extension}")));
} else {
names.push(OsString::from(format!("git.{extension}")));
}
}
return names;
}
fn is_semicolon(byte: &u8) -> bool {
return *byte == b';';
}
pub fn common_git_paths(platform: Platform, environment: &[(OsString, OsString)]) -> Vec<PathBuf> {
if platform == Platform::Unix {
return vec![
PathBuf::from("/usr/bin/git"),
PathBuf::from("/usr/local/bin/git"),
];
}
if platform == Platform::MacOs {
return vec![
PathBuf::from("/usr/bin/git"),
PathBuf::from("/usr/local/bin/git"),
PathBuf::from("/opt/homebrew/bin/git"),
PathBuf::from("/opt/local/bin/git"),
];
}
let mut roots: Vec<OsString> = Vec::<OsString>::new();
for name in ["ProgramFiles", "ProgramW6432", "ProgramFiles(x86)"] {
if let Some(root) = environment_value(environment, name) {
roots.push(root);
}
}
roots.push(OsString::from("C:\\Program Files"));
if let Some(local) = environment_value(environment, "LOCALAPPDATA") {
let mut programs: OsString = trimmed_root(local.as_os_str());
programs.push("\\Programs");
roots.push(programs);
}
let mut paths: Vec<PathBuf> = Vec::<PathBuf>::new();
for root in &roots {
for tail in ["\\Git\\cmd\\git.exe", "\\Git\\bin\\git.exe"] {
let mut path: OsString = trimmed_root(root.as_os_str());
path.push(tail);
let candidate: PathBuf = PathBuf::from(path);
if !paths.contains(&candidate) {
paths.push(candidate);
}
}
}
return paths;
}
fn trimmed_root(root: &OsStr) -> OsString {
let bytes: &[u8] = root.as_encoded_bytes();
let mut end: usize = bytes.len();
while end > 0 && (bytes[end - 1] == b'\\' || bytes[end - 1] == b'/') {
end -= 1;
}
match std::str::from_utf8(&bytes[..end]) {
Ok(text) => return OsString::from(text),
Err(_) => return root.to_os_string(),
}
}
fn candidate_identity(path: &Path, platform: Platform) -> OsString {
if platform == Platform::Windows {
return path.as_os_str().to_ascii_lowercase();
}
return path.as_os_str().to_os_string();
}
fn lexically_tidied(path: &Path) -> PathBuf {
let mut result: PathBuf = PathBuf::new();
for component in path.components() {
result.push(component.as_os_str());
}
return result;
}
pub fn candidate_sequence(inputs: &ResolutionInputs) -> Vec<PathBuf> {
let names: Vec<OsString> =
executable_names(inputs.platform, inputs.path_extensions.as_os_str());
let mut path_candidates: Vec<PathBuf> = Vec::<PathBuf>::new();
for directory in std::env::split_paths(inputs.path.as_os_str()) {
for name in &names {
let candidate: PathBuf = inputs.current_directory.join(&directory).join(name);
path_candidates.push(lexically_tidied(candidate.as_path()));
}
}
let mut sequence: Vec<PathBuf> = Vec::<PathBuf>::new();
for common in &inputs.common_paths {
let wanted: OsString = candidate_identity(common.as_path(), inputs.platform);
for candidate in &path_candidates {
if candidate_identity(candidate.as_path(), inputs.platform) == wanted
&& !sequence.contains(candidate)
{
sequence.push(candidate.clone());
}
}
}
for candidate in path_candidates {
if !sequence.contains(&candidate) {
sequence.push(candidate);
}
}
return sequence;
}
pub fn search_path_for_real_git(inputs: &ResolutionInputs) -> Result<PathBuf, RealGitNotFound> {
let candidates: Vec<PathBuf> = candidate_sequence(inputs);
let mut skipped_wrapper_count: usize = 0;
let mut searched: Vec<SearchedCandidate> = Vec::<SearchedCandidate>::new();
for candidate in &candidates {
let kind: CandidateKind =
classify_candidate(candidate.as_path(), inputs.own_executable.as_path());
if kind == CandidateKind::RealGit {
return Ok(candidate.clone());
}
if kind == CandidateKind::Wrapper {
skipped_wrapper_count += 1;
}
searched.push(SearchedCandidate { path: candidate.clone(), kind });
}
return Err(RealGitNotFound {
candidate_count: candidates.len(),
skipped_wrapper_count,
searched,
path: inputs.path.clone(),
recorded: None,
});
}
pub fn host_platform() -> Platform {
if cfg!(windows) {
return Platform::Windows;
}
if cfg!(target_os = "macos") {
return Platform::MacOs;
}
return Platform::Unix;
}
fn service_unit_below(runtime: OsString) -> Option<PathBuf> {
let directory: PathBuf = PathBuf::from(runtime);
if !directory.is_absolute() {
return None;
}
return Some(service_unit_file(directory.as_path()));
}
pub fn process_resolution_inputs(
environment: &[(OsString, OsString)],
) -> std::io::Result<ResolutionInputs> {
let platform: Platform = host_platform();
#[allow(
clippy::disallowed_methods,
reason = "identifies the wrapper so the search for the real Git skips it; nothing is started from the path"
)]
let own_executable: PathBuf = std::env::current_exe()?;
crate::service_route::remember_own_executable(own_executable.as_path());
let current_directory: PathBuf = std::env::current_dir()?;
let path: OsString = environment_value(environment, "PATH").unwrap_or_default();
let path_extensions: OsString = environment_value(environment, "PATHEXT")
.unwrap_or(OsString::from(DEFAULT_WINDOWS_PATH_EXTENSIONS));
let service_unit: Option<PathBuf> =
environment_value(environment, "XDG_RUNTIME_DIR").and_then(service_unit_below);
return Ok(ResolutionInputs {
platform,
path,
path_extensions,
current_directory,
common_paths: common_git_paths(platform, environment),
own_executable,
service_unit,
});
}
#[cfg(test)]
#[path = "real_git_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "real_git_fallback_tests.rs"]
mod fallback_tests;