use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use git_lfs_git::endpoint::{SshInfo, resolve_endpoint};
const FILTER_KEYS: &[&str] = &[
"filter.lfs.process",
"filter.lfs.smudge",
"filter.lfs.clean",
];
#[derive(Debug, thiserror::Error)]
pub enum EnvError {
#[error(transparent)]
Io(#[from] std::io::Error),
}
pub fn run(cwd: &Path) -> Result<(), EnvError> {
println!("git-lfs/{} (rust)", env!("CARGO_PKG_VERSION"));
println!("{}", git_version());
println!();
let git_dir = git_lfs_git::git_dir(cwd).ok();
let common_dir = git_lfs_git::git_common_dir(cwd).ok();
if let Some(git_dir) = git_dir.as_ref() {
emit_endpoints(cwd);
let common = common_dir.as_deref().unwrap_or(git_dir);
emit_paths_in_repo(cwd, git_dir, common);
} else {
emit_paths_outside_repo();
}
emit_static_defaults(cwd);
println!("AccessDownload=none");
println!("AccessUpload=none");
let tus = bool_config(cwd, "lfs.tustransfers");
let customs = custom_transfer_methods(cwd);
println!("DownloadTransfers={}", transfer_list(&customs, false));
println!("UploadTransfers={}", transfer_list(&customs, tus));
if git_dir.is_some() {
} else {
println!("LfsStorageDir=lfs");
}
println!();
let mut vars: Vec<(String, String)> = std::env::vars()
.filter(|(k, _)| k.starts_with("GIT_"))
.map(|(k, v)| {
let restored = crate::original_path_env(&k)
.map(|os| os.to_string_lossy().into_owned())
.unwrap_or(v);
(k, restored)
})
.collect();
vars.sort();
for (k, v) in vars {
println!("{k}={v}");
}
println!();
for key in FILTER_KEYS {
let value = git_lfs_git::config::get_effective(cwd, key)
.ok()
.flatten()
.unwrap_or_default();
println!("git config {key} = {value:?}");
}
Ok(())
}
fn emit_endpoints(cwd: &Path) {
let remotes = remotes(cwd);
let has_origin = remotes.iter().any(|r| r == "origin");
let has_default_url = std::env::var_os("GIT_LFS_URL").is_some()
|| git_lfs_git::config::get_effective(cwd, "lfs.url")
.ok()
.flatten()
.is_some();
if (has_origin || has_default_url)
&& let Ok(info) = resolve_endpoint(cwd, None)
{
let auth = access_for(cwd, &info.url);
println!("Endpoint={} (auth={auth})", info.url);
print_ssh_line(&info.ssh);
}
for r in &remotes {
if r == "origin" {
continue;
}
if let Ok(info) = resolve_endpoint(cwd, Some(r)) {
let auth = access_for(cwd, &info.url);
println!("Endpoint ({r})={} (auth={auth})", info.url);
print_ssh_line(&info.ssh);
}
}
}
fn print_ssh_line(ssh: &Option<SshInfo>) {
if let Some(info) = ssh {
println!(" SSH={}:{}", info.user_and_host, info.path);
}
}
fn emit_paths_in_repo(cwd: &Path, git_dir: &Path, common_dir: &Path) {
let lfs_dir = common_dir.join("lfs");
let media_dir = lfs_dir.join("objects");
let tmp_dir = lfs_dir.join("tmp");
let working_dir = working_dir(cwd);
if let Some(wd) = working_dir {
println!("LocalWorkingDir={}", wd.display());
} else {
println!("LocalWorkingDir=");
}
println!("LocalGitDir={}", git_dir.display());
println!("LocalGitStorageDir={}", common_dir.display());
println!("LocalMediaDir={}", media_dir.display());
println!("LocalReferenceDirs=");
println!("TempDir={}", tmp_dir.display());
println!("LfsStorageDir={}", lfs_dir.display());
}
fn emit_paths_outside_repo() {
println!("LocalWorkingDir=");
println!("LocalGitDir=");
println!("LocalGitStorageDir=");
println!(
"LocalMediaDir={}",
PathBuf::from("lfs").join("objects").display()
);
println!("LocalReferenceDirs=");
println!("TempDir={}", PathBuf::from("lfs").join("tmp").display());
}
fn emit_static_defaults(cwd: &Path) {
println!("ConcurrentTransfers={}", concurrent_transfers(cwd));
println!("TusTransfers={}", bool_config(cwd, "lfs.tustransfers"));
println!(
"BasicTransfersOnly={}",
bool_config(cwd, "lfs.basictransfersonly")
);
println!("SkipDownloadErrors={}", skip_download_errors(cwd));
println!("FetchRecentAlways=false");
println!("FetchRecentRefsDays=7");
println!("FetchRecentCommitsDays=0");
println!("FetchRecentRefsIncludeRemotes=true");
println!("PruneOffsetDays=3");
println!("PruneVerifyRemoteAlways=false");
println!("PruneVerifyUnreachableAlways=false");
println!("PruneRemoteName=origin");
}
pub(crate) fn skip_download_errors(cwd: &Path) -> bool {
bool_config(cwd, "lfs.skipdownloaderrors") || bool_env("GIT_LFS_SKIP_DOWNLOAD_ERRORS")
}
fn bool_config(cwd: &Path, key: &str) -> bool {
matches!(
git_lfs_git::config::get_effective(cwd, key)
.ok()
.flatten()
.as_deref(),
Some("true" | "1" | "yes" | "on")
)
}
fn bool_env(name: &str) -> bool {
matches!(
std::env::var(name).ok().as_deref(),
Some("true" | "1" | "yes" | "on")
)
}
fn custom_transfer_methods(cwd: &Path) -> Vec<String> {
let out = std::process::Command::new("git")
.arg("-C")
.arg(cwd)
.args([
"config",
"--name-only",
"--get-regexp",
r"^lfs\.customtransfer\..*\.path$",
])
.output();
let Ok(out) = out else { return Vec::new() };
if !out.status.success() {
return Vec::new();
}
let mut names: Vec<String> = String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|line| {
line.strip_prefix("lfs.customtransfer.")
.and_then(|rest| rest.strip_suffix(".path"))
.map(str::to_owned)
})
.collect();
names.sort();
names.dedup();
names
}
fn transfer_list(customs: &[String], with_tus: bool) -> String {
let mut parts: Vec<&str> = vec!["basic", "lfs-standalone-file", "ssh"];
for c in customs {
parts.push(c.as_str());
}
if with_tus {
parts.push("tus");
}
parts.join(",")
}
fn concurrent_transfers(cwd: &Path) -> usize {
if let Some(v) = git_lfs_git::config::get_effective(cwd, "lfs.concurrenttransfers")
.ok()
.flatten()
&& let Ok(n) = v.parse::<usize>()
&& n > 0
{
return n;
}
let n = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
(n * 3).max(8)
}
fn access_for(cwd: &Path, url: &str) -> String {
let key = format!("lfs.{url}.access");
git_lfs_git::config::get_effective(cwd, &key)
.ok()
.flatten()
.unwrap_or_else(|| "none".to_owned())
}
fn git_version() -> String {
Command::new("git")
.arg("--version")
.output()
.ok()
.and_then(|o| {
o.status
.success()
.then(|| String::from_utf8_lossy(&o.stdout).trim().to_owned())
})
.unwrap_or_else(|| "git version unknown".to_owned())
}
fn working_dir(cwd: &Path) -> Option<std::path::PathBuf> {
let out = Command::new("git")
.arg("-C")
.arg(cwd)
.args(["rev-parse", "--show-toplevel"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_owned();
if s.is_empty() {
None
} else {
Some(std::path::PathBuf::from(s))
}
}
fn remotes(cwd: &Path) -> Vec<String> {
Command::new("git")
.arg("-C")
.arg(cwd)
.arg("remote")
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.filter(|l| !l.is_empty())
.map(String::from)
.collect()
})
.unwrap_or_default()
}