use crate::git_version_cache::{binary_identity, cache_directory, read_version_text, store_version_text};
use crate::transaction_git::{GitContext, GitOutput, GitRequest, run_git};
use std::ffi::OsString;
use std::io::Write;
use std::path::Path;
use std::sync::OnceLock;
pub const MINIMUM_GIT_VERSION: (u64, u64, u64) = (2, 56, 0);
pub type GitRelease = (u64, u64, u64);
static CHECKED: OnceLock<Result<GitRelease, String>> = OnceLock::new();
pub fn parse_git_version(output: &[u8]) -> Option<(u64, u64, u64)> {
let text: &str = std::str::from_utf8(output).ok()?;
let release: &str = text.strip_prefix("git version ")?.split_whitespace().next()?;
let mut parts: std::str::Split<'_, char> = release.split('.');
let major: u64 = parts.next()?.parse::<u64>().ok()?;
let minor: u64 = parts.next()?.parse::<u64>().ok()?;
let patch_text: &str = parts.next()?;
let digits: &str = &patch_text[..patch_text.find(not_digit).unwrap_or(patch_text.len())];
let patch: u64 = digits.parse::<u64>().ok()?;
return Some((major, minor, patch));
}
fn not_digit(character: char) -> bool {
return !character.is_ascii_digit();
}
pub fn unsupported_message(context: &GitContext, found: &str) -> String {
return format!(
"cli-git: real Git at {} reports {found}, so its release cannot be read; commits, hooks and the \
locks that coordinate with them depend on it. Nothing was run. Point the wrapper at a working \
Git, then run the command again.",
context.real_git.display()
);
}
pub fn older_git_warning(context: &GitContext, release: GitRelease) -> Option<String> {
if release >= MINIMUM_GIT_VERSION {
return None;
}
return Some(format!(
"cli-git: warning: real Git at {} is version {}.{}.{}, older than {}.{}.{}, the release this wrapper was tested \
with; the command continues with it.\n",
context.real_git.display(),
release.0,
release.1,
release.2,
MINIMUM_GIT_VERSION.0,
MINIMUM_GIT_VERSION.1,
MINIMUM_GIT_VERSION.2
));
}
fn judge_version_text(context: &GitContext, stdout: &[u8]) -> Result<GitRelease, String> {
let Some(version) = parse_git_version(stdout) else {
let shown: String = String::from_utf8_lossy(stdout).trim().to_string();
return Err(unsupported_message(context, format!("an unreadable version {shown:?}").as_str()));
};
return Ok(version);
}
fn run_git_version(context: &GitContext) -> Result<GitOutput, String> {
let mut request: GitRequest = GitRequest::new(std::path::Path::new("."), &["version"]);
request.without_prefix = true;
return match run_git(context, &request) {
Ok(finished) => Ok(finished),
Err(error) => Err(unsupported_message(context, format!("nothing ({error})").as_str())),
};
}
pub fn ask_git_version(context: &GitContext) -> Result<(u64, u64, u64), String> {
let output: GitOutput = run_git_version(context)?;
return judge_version_text(context, output.stdout.as_slice());
}
pub fn ask_git_version_cached(context: &GitContext, cache_directory: Option<&Path>) -> Result<GitRelease, String> {
let Some(directory) = cache_directory else {
return ask_git_version(context);
};
let Some(identity) = binary_identity(context.real_git.as_path()) else {
return ask_git_version(context);
};
if let Some(text) = read_version_text(directory, &identity)
&& parse_git_version(text.as_slice()).is_some()
{
return judge_version_text(context, text.as_slice());
}
let output: GitOutput = run_git_version(context)?;
let judged: Result<GitRelease, String> = judge_version_text(context, output.stdout.as_slice());
let unchanged: bool = binary_identity(context.real_git.as_path()).as_ref() == Some(&identity);
if judged.is_ok() && output.succeeded() && unchanged {
store_version_text(directory, &identity, output.stdout.as_slice());
}
return judged;
}
fn ask_git_version_with_process_cache(context: &GitContext) -> Result<GitRelease, String> {
let answer: Result<GitRelease, String> = if cfg!(test) {
ask_git_version(context)
} else {
let environment: Vec<(OsString, OsString)> = std::env::vars_os().collect();
ask_git_version_cached(context, cache_directory(environment.as_slice()).as_deref())
};
if let Ok(release) = &answer
&& let Some(warning) = older_git_warning(context, *release)
{
let mut stream = std::io::stderr();
let _ = stream.write_all(warning.as_bytes());
let _ = stream.flush();
}
return answer;
}
pub fn check_git_version(context: &GitContext) -> Result<(u64, u64, u64), String> {
return check_once(&CHECKED, context, ask_git_version_with_process_cache);
}
pub fn check_once(
cell: &OnceLock<Result<GitRelease, String>>,
context: &GitContext,
ask: fn(&GitContext) -> Result<GitRelease, String>,
) -> Result<GitRelease, String> {
if let Some(known) = cell.get() {
return known.clone();
}
let answer: Result<(u64, u64, u64), String> = ask(context);
let _ = cell.set(answer.clone());
return answer;
}
#[cfg(test)]
#[path = "git_version_tests.rs"]
mod tests;