use crate::config_snapshot::ConfigSnapshot;
use std::ffi::{OsStr, OsString};
use std::path::Path;
pub const SWITCH_NAME: &str = "cli-git-lfs-pre-push";
pub const LFS_PROGRAM: &str = "git-lfs";
const LFS_KEY_PREFIXES: [&str; 2] = ["filter.lfs.", "lfs."];
const MACHINE_SCOPES: [&str; 2] = ["system", "global"];
pub fn uses_lfs(snapshot: &ConfigSnapshot) -> bool {
return snapshot.entries().iter().any(|entry| {
return !MACHINE_SCOPES.contains(&entry.scope.as_str())
&& LFS_KEY_PREFIXES.iter().any(|prefix| {
return entry.key.starts_with(prefix);
});
});
}
fn is_switch_key(name: &str) -> bool {
const VARIABLE: &str = ".enabled";
let Some(rest) = name.get(..5).filter(|section| {
return section.eq_ignore_ascii_case("hook.");
}).map(|_| {
return &name[5..];
}) else {
return false;
};
if rest.len() <= VARIABLE.len() {
return false;
}
if !rest.is_char_boundary(rest.len() - VARIABLE.len()) {
return false;
}
let (subsection, variable) = rest.split_at(rest.len() - VARIABLE.len());
return variable.eq_ignore_ascii_case(VARIABLE) && subsection == SWITCH_NAME;
}
fn reads_as_false(value: &str) -> bool {
return ["false", "no", "off", "0", ""].iter().any(|spelling| {
return value.eq_ignore_ascii_case(spelling);
});
}
pub fn hook_switched_off(global_prefix: &[OsString], environment: &[(OsString, OsString)]) -> bool {
let mut off: bool = false;
let count: usize = crate::child_environment::environment_value(environment, "GIT_CONFIG_COUNT")
.and_then(|value| {
return value.to_str().and_then(|text| {
return text.parse::<usize>().ok();
});
})
.unwrap_or(0);
for index in 0..count {
let name_of = |kind: &str| -> Option<OsString> {
return crate::child_environment::environment_value(environment, &format!("GIT_CONFIG_{kind}_{index}"));
};
if name_of("KEY").is_some_and(|name| {
return is_switch_key(&name.to_string_lossy());
}) {
off = name_of("VALUE").is_some_and(|value| {
return reads_as_false(&value.to_string_lossy());
});
}
}
let mut tokens = global_prefix.iter();
while let Some(token) = tokens.next() {
if token.as_os_str() != "-c" {
continue;
}
let Some(setting) = tokens.next().and_then(|value| {
return value.to_str();
}) else {
continue;
};
let (name, value) = setting.split_once('=').map_or((setting, None), |(name, value)| {
return (name, Some(value));
});
if is_switch_key(name) {
off = value.is_some_and(reads_as_false);
}
}
return off;
}
pub fn find_program(path: &OsStr) -> Option<std::path::PathBuf> {
use std::os::unix::fs::PermissionsExt;
return std::env::split_paths(path)
.map(|directory| {
let base: &Path = if directory.as_os_str().is_empty() { Path::new(".") } else { directory.as_path() };
return base.join(LFS_PROGRAM);
})
.find(|candidate| {
return std::fs::metadata(candidate).is_ok_and(|found| {
return found.is_file() && found.permissions().mode() & 0o111 != 0;
});
})
.and_then(|found| {
return std::path::absolute(found).ok();
});
}
pub fn program_on_path(path: &OsStr) -> bool {
return find_program(path).is_some();
}
pub fn skip_requested(environment: &[(std::ffi::OsString, std::ffi::OsString)]) -> bool {
return crate::child_environment::environment_value(environment, "GIT_LFS_SKIP_PUSH").is_some_and(|value| {
let text: String = value.to_string_lossy().to_ascii_lowercase();
return ["1", "t", "true", "y", "yes", "on"].contains(&text.as_str());
});
}
pub fn missing_program_message() -> String {
return format!(
"cli-git: this push was stopped before it sent anything. The Git configuration of this repository has Git LFS \
keys (`filter.lfs.*` or `lfs.*`), so the wrapper runs `{LFS_PROGRAM} push` to upload the large \
files the pushed commits point to before it publishes the pointer files, but `{LFS_PROGRAM}` was not found in \
any directory of PATH. Pushing without that upload would publish pointers whose content the LFS server does \
not have. Ways forward: install Git LFS so that `{LFS_PROGRAM}` is on PATH and push again; or, if this \
repository does not use Git LFS, remove its configuration (`git lfs uninstall`, or delete the `filter.lfs.*` \
and `lfs.*` keys); or accept pointers without objects for this push with `git push --no-verify` and upload \
the objects later with `git lfs push --all <remote>` once Git LFS is installed.\n"
);
}
pub fn plan_failed_message(why: &str) -> String {
return format!(
"cli-git: this push was stopped before it sent anything. The Git configuration of this repository has Git LFS \
keys, so the wrapper uploads the large files the pushed commits point to before it publishes the pointer \
files, but it could not work out which refs this push sends ({why}). Pushing without that upload could \
publish pointers whose content the LFS server does not have. Ways forward: push again, because the cause may \
be transient; or upload by hand with `git lfs push --all <remote>` and push with `git push --no-verify`.\n"
);
}
pub fn failed_message(status: &str) -> String {
return format!(
"cli-git: `{LFS_PROGRAM} push` {status}, so this push was not started and nothing was published. \
The lines above come from `{LFS_PROGRAM}`; they usually name the cause: the LFS server \
(`lfs.url`) is unreachable or refused the credentials, or a large file the pushed commits point to is not \
in this clone's `.git/lfs`. Ways forward: fix that cause and push again, because objects the server already \
has are not sent twice; upload by hand with `git lfs push --all <remote>` and push again; or publish the \
pointer files without their objects, which nobody can download until the objects are uploaded, with \
`git push --no-verify` (or `GIT_LFS_SKIP_PUSH=1`).\n"
);
}
#[cfg(test)]
#[path = "lfs_pre_push_tests.rs"]
mod tests;