use std::io::{self, BufRead, BufWriter, Read, Write};
use std::process::ExitCode;
use clap::{CommandFactory, Parser};
use git_lfs_filter::{CleanExtension, SmudgeExtension, clean, filter_process, smudge_with_fetch};
use git_lfs_store::Store;
mod checkout;
mod clone;
mod env;
mod ext;
mod fetch;
mod fetcher;
mod fsck;
mod hooks;
mod http_client;
mod install;
mod lock;
mod lock_cache;
mod lockable;
mod locks_verify;
mod ls_files;
mod migrate;
mod pointer_cmd;
mod pre_push;
mod prune;
mod pull;
mod push;
mod status;
mod track;
mod track_cmd;
mod update;
use fetcher::LfsFetcher;
use git_lfs::args::{Cli, Command, MigrateCmd};
fn main() -> ExitCode {
let cli = Cli::parse();
if cli.version {
println!("git-lfs/{} (rust)", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS;
}
let Some(command) = cli.command else {
Cli::command().print_help().ok();
return ExitCode::FAILURE;
};
match dispatch(command) {
Ok(code) => ExitCode::from(code),
Err(e) => {
eprintln!("git-lfs: {e}");
ExitCode::FAILURE
}
}
}
const PATH_GIT_ENV_VARS: &[&str] = &[
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_COMMON_DIR",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
];
static ORIGINAL_PATH_ENVS: std::sync::OnceLock<Vec<(&'static str, std::ffi::OsString)>> =
std::sync::OnceLock::new();
pub fn original_path_env(name: &str) -> Option<std::ffi::OsString> {
ORIGINAL_PATH_ENVS
.get()?
.iter()
.find(|(k, _)| *k == name)
.map(|(_, v)| v.clone())
}
fn canonicalize_path_envs(base: &std::path::Path) {
let mut snapshot = Vec::new();
for name in PATH_GIT_ENV_VARS {
let Some(raw) = std::env::var_os(name) else {
continue;
};
snapshot.push((*name, raw.clone()));
if raw.is_empty() {
continue;
}
let p = std::path::Path::new(&raw);
if p.is_absolute() {
continue;
}
let absolute = base.join(p);
unsafe {
std::env::set_var(name, absolute);
}
}
let _ = ORIGINAL_PATH_ENVS.set(snapshot);
}
fn resolve_install_scope(
local: bool,
system: bool,
worktree: bool,
file: Option<std::path::PathBuf>,
) -> Result<install::InstallScope, &'static str> {
let count = [local, system, worktree, file.is_some()]
.iter()
.filter(|b| **b)
.count();
if count > 1 {
return Err(
"Only one of the --local, --system, --worktree, and --file options can be specified.",
);
}
Ok(if let Some(p) = file {
install::InstallScope::File(p)
} else if local {
install::InstallScope::Local
} else if system {
install::InstallScope::System
} else if worktree {
install::InstallScope::Worktree
} else {
install::InstallScope::Global
})
}
fn handle_install_config_failure(e: &install::InstallError) -> Option<u8> {
if let install::InstallError::ConfigCommandFailed { .. } = e {
println!("{e}");
Some(2)
} else {
None
}
}
fn bail_if_outside_repo(cwd: &std::path::Path) -> Option<u8> {
if git_lfs_git::git_dir(cwd).is_err() {
println!("Not in a Git repository.");
return Some(128);
}
None
}
fn skip_smudge_env() -> bool {
match std::env::var_os("GIT_LFS_SKIP_SMUDGE") {
None => false,
Some(v) => {
let s = v.to_string_lossy();
!matches!(s.as_ref(), "" | "0" | "false" | "False" | "FALSE")
}
}
}
fn handle_migrate_error(e: migrate::MigrateError) -> u8 {
match e {
migrate::MigrateError::Usage(msg) => {
eprintln!("{msg}");
2
}
other => {
eprintln!("git-lfs: {other}");
1
}
}
}
fn split_csv(values: &[String]) -> Vec<String> {
values
.iter()
.flat_map(|s| s.split(','))
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn collect_clean_extensions(cwd: &std::path::Path) -> Vec<CleanExtension> {
git_lfs_git::list_extensions(cwd)
.into_iter()
.filter_map(|ext| {
if ext.clean.trim().is_empty() {
return None;
}
let priority = u8::try_from(ext.priority).ok().filter(|&p| p <= 9)?;
Some(CleanExtension {
name: ext.name,
priority,
command: ext.clean,
})
})
.collect()
}
fn collect_smudge_extensions(cwd: &std::path::Path) -> Vec<SmudgeExtension> {
git_lfs_git::list_extensions(cwd)
.into_iter()
.filter_map(|ext| {
if ext.smudge.trim().is_empty() {
return None;
}
let priority = u8::try_from(ext.priority).ok().filter(|&p| p <= 9)?;
Some(SmudgeExtension {
name: ext.name,
priority,
command: ext.smudge,
})
})
.collect()
}
fn dispatch(cmd: Command) -> Result<u8, Box<dyn std::error::Error>> {
let cwd = std::env::current_dir()?;
canonicalize_path_envs(&cwd);
if let Ok(lfs_root) = git_lfs_git::lfs_dir(&cwd) {
Store::new(lfs_root).cleanup_tmp_objects();
}
match cmd {
Command::Clean { path } => {
let _ = install::try_install_hooks(&cwd);
let store = Store::new(git_lfs_git::lfs_dir(&cwd)?);
let stdin = io::stdin().lock();
let mut input: Box<dyn Read> = Box::new(stdin);
let mut output: Box<dyn Write> = Box::new(BufWriter::new(io::stdout().lock()));
let extensions = collect_clean_extensions(&cwd);
let path_str = path
.as_deref()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
clean(&store, &mut input, &mut output, &path_str, &extensions)?;
output.flush()?;
}
Command::Smudge { path, skip } => {
let _ = install::try_install_hooks(&cwd);
let store = Store::new(git_lfs_git::lfs_dir(&cwd)?)
.with_references(git_lfs_git::lfs_alternate_dirs(&cwd).unwrap_or_default());
let stdin = io::stdin().lock();
let mut input: Box<dyn Read> = Box::new(stdin);
let mut output: Box<dyn Write> = Box::new(BufWriter::new(io::stdout().lock()));
if skip || skip_smudge_env() {
io::copy(&mut input, &mut output)?;
} else {
let fetcher = LfsFetcher::from_repo(&cwd, &store)?;
let smudge_extensions = collect_smudge_extensions(&cwd);
let path_str = path
.as_deref()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
smudge_with_fetch(
&store,
&mut input,
&mut output,
&path_str,
&smudge_extensions,
|p| fetcher.fetch(p),
)?;
fetcher.persist_access_mode();
}
output.flush()?;
}
Command::Install {
local,
system,
worktree,
file,
force,
skip_repo,
skip_smudge,
} => {
let scope = match resolve_install_scope(local, system, worktree, file) {
Ok(s) => s,
Err(msg) => {
eprintln!("{msg}");
return Ok(2);
}
};
if scope.is_repo_scope()
&& let Some(code) = bail_if_outside_repo(&cwd)
{
return Ok(code);
}
let opts = install::InstallOptions {
scope,
force,
skip_repo,
skip_smudge,
};
let hooks_active = !opts.skip_repo
&& (opts.scope.is_repo_scope() || git_lfs_git::git_dir(&cwd).is_ok());
match install::install(&cwd, &opts) {
Ok(()) => {
if hooks_active {
println!("Updated Git hooks.");
}
println!("Git LFS initialized.");
}
Err(e @ install::InstallError::FilterAttribute { .. }) => {
println!("warning: {e}");
println!("Run `git lfs install --force` to reset Git configuration.");
return Ok(2);
}
Err(install::InstallError::HookConflict { hook, existing }) => {
install::print_hook_conflict(&hook, &existing);
return Ok(2);
}
Err(e) if let Some(code) = handle_install_config_failure(&e) => {
return Ok(code);
}
Err(e) => return Err(Box::new(e)),
}
}
Command::Uninstall {
mode,
local,
system,
worktree,
file,
skip_repo,
} => {
let scope = match resolve_install_scope(local, system, worktree, file) {
Ok(s) => s,
Err(msg) => {
eprintln!("{msg}");
return Ok(2);
}
};
let hooks_only = match mode.as_deref() {
None => false,
Some("hooks") => true,
Some(other) => {
eprintln!("git-lfs: unknown mode {other:?}");
return Ok(2);
}
};
if scope.is_repo_scope()
&& let Some(code) = bail_if_outside_repo(&cwd)
{
return Ok(code);
}
let opts = install::UninstallOptions {
scope: scope.clone(),
skip_repo,
hooks_only,
};
if let Err(e) = install::uninstall(&cwd, &opts) {
if matches!(e, install::InstallError::ConfigCommandFailed { .. }) {
println!("{e}");
return Ok(0);
}
return Err(Box::new(e));
}
if hooks_only {
} else if scope.announces_global() {
println!("Global Git LFS configuration has been removed.");
} else {
println!("Local Git LFS configuration has been removed.");
}
}
Command::Clone { args } => {
clone::run(&cwd, &args)?;
}
Command::FilterProcess { skip } => {
let _ = install::try_install_hooks(&cwd);
let store = Store::new(git_lfs_git::lfs_dir(&cwd)?)
.with_references(git_lfs_git::lfs_alternate_dirs(&cwd).unwrap_or_default());
let fetcher = LfsFetcher::from_repo(&cwd, &store)?;
let stdin = io::stdin().lock();
let stdout = io::stdout().lock();
let clean_extensions = collect_clean_extensions(&cwd);
let smudge_extensions = collect_smudge_extensions(&cwd);
let include_set = fetch::build_pattern_set(&cwd, &[], "lfs.fetchinclude")?;
let exclude_set = fetch::build_pattern_set(&cwd, &[], "lfs.fetchexclude")?;
let path_filter = move |path: &str| -> bool {
let p = std::path::Path::new(path);
fetch::path_passes_filter(Some(p), &include_set, &exclude_set)
};
filter_process(
&store,
stdin,
stdout,
|p| fetcher.fetch(p),
skip || skip_smudge_env(),
&clean_extensions,
&smudge_extensions,
&path_filter,
)?;
fetcher.persist_access_mode();
}
Command::Fetch {
args,
dry_run,
json,
all,
refetch,
stdin,
prune,
include,
exclude,
} => {
let stdin_lines: Vec<String> = if stdin {
io::stdin()
.lock()
.lines()
.map_while(Result::ok)
.map(|l| l.trim().to_owned())
.filter(|l| !l.is_empty())
.collect()
} else {
Vec::new()
};
let opts = fetch::FetchOptions {
args: &args,
stdin_lines: &stdin_lines,
dry_run,
json,
all,
refetch,
stdin,
prune,
include: &include,
exclude: &exclude,
};
match fetch::fetch(&cwd, &opts) {
Ok(outcome) => {
if !outcome.report.failed.is_empty() {
return Err("one or more objects failed to download".into());
}
}
Err(fetch::FetchCommandError::Usage(msg)) if msg == "Not in a Git repository." => {
println!("{msg}");
return Ok(128);
}
Err(e) => return Err(e.into()),
}
}
Command::Pull {
refs,
include,
exclude,
} => {
match pull::pull_with_filter(&cwd, &refs, &include, &exclude) {
Ok(()) => {}
Err(pull::PullCommandError::Fetch(fetch::FetchCommandError::Usage(msg)))
if msg == "Not in a Git repository." =>
{
println!("{msg}");
return Ok(128);
}
Err(e @ pull::PullCommandError::FetchFailures(_)) => {
eprintln!("git-lfs: {e}");
return Ok(2);
}
Err(e) => return Err(e.into()),
}
}
Command::Push {
remote,
args,
dry_run,
all,
stdin,
object_id,
} => {
let stdin_lines: Vec<String> = if stdin {
io::stdin()
.lock()
.lines()
.map_while(Result::ok)
.map(|l| l.trim().to_owned())
.filter(|l| !l.is_empty())
.collect()
} else {
Vec::new()
};
let opts = push::PushOptions {
args: &args,
stdin_lines: &stdin_lines,
dry_run,
all,
stdin,
object_id,
};
let outcome = push::push(&cwd, &remote, &opts)?;
if outcome.aborted {
return Ok(2);
}
if !outcome.report.failed.is_empty() {
return Err("one or more objects failed to upload".into());
}
}
Command::PostCheckout { args } => {
hooks::post_checkout(&cwd, &args)?;
}
Command::PostCommit { args } => {
hooks::post_commit(&cwd, &args)?;
}
Command::PostMerge { args } => {
hooks::post_merge(&cwd, &args)?;
}
Command::PrePush {
remote,
url: _,
dry_run,
} => {
let stdin = io::stdin().lock();
let outcome = pre_push::pre_push(&cwd, &remote, stdin, dry_run)?;
if outcome.aborted {
return Ok(2);
}
if !outcome.report.failed.is_empty() {
return Err("pre-push: one or more objects failed to upload".into());
}
}
Command::Track {
patterns,
lockable,
not_lockable,
dry_run,
verbose,
json,
no_excluded,
filename,
no_modify_attrs,
} => {
return track_cmd::run(track_cmd::Args {
cwd: &cwd,
patterns: &patterns,
lockable,
not_lockable,
dry_run,
verbose,
json,
no_excluded,
filename,
no_modify_attrs,
});
}
Command::Version => {
println!("git-lfs/{} (rust)", env!("CARGO_PKG_VERSION"));
}
Command::Pointer {
file,
pointer,
stdin,
check,
strict,
no_strict,
} => {
let opts = pointer_cmd::Options {
file,
pointer,
stdin,
check,
strict,
no_strict,
};
let code = pointer_cmd::run(&opts)?;
return Ok(code as u8);
}
Command::Env => {
env::run(&cwd)?;
}
Command::Ext => {
ext::run(&cwd)?;
}
Command::Update { force, manual } => match update::run(&cwd, force, manual) {
Ok(code) => return Ok(code),
Err(update::UpdateError::NotInRepo) => {
println!("Not in a Git repository.");
return Ok(128);
}
Err(e) => return Err(Box::new(e)),
},
Command::Migrate { cmd } => match cmd {
MigrateCmd::Export {
branches,
everything,
include,
exclude,
include_ref,
exclude_ref,
skip_fetch,
object_map,
verbose,
remote,
yes: _,
} => {
let opts = migrate::ExportOptions {
branches,
everything,
include: split_csv(&include),
exclude: split_csv(&exclude),
include_ref,
exclude_ref,
skip_fetch,
object_map,
verbose,
remote,
};
if let Err(e) = migrate::export(&cwd, &opts) {
return Ok(handle_migrate_error(e));
}
}
MigrateCmd::Import {
args,
everything,
include,
exclude,
include_ref,
exclude_ref,
above,
no_rewrite,
message,
yes,
fixup,
skip_fetch,
object_map,
verbose,
remote,
} => {
let above_bytes = migrate::parse_size(&above)?;
let (branches, paths) = if no_rewrite {
(Vec::new(), args)
} else {
(args, Vec::new())
};
let opts = migrate::ImportOptions {
branches,
everything,
include: split_csv(&include),
exclude: split_csv(&exclude),
include_ref,
exclude_ref,
above: above_bytes,
no_rewrite,
message,
paths,
fixup,
skip_fetch,
object_map,
verbose,
remote,
yes,
};
let _ = install::try_install_hooks(&cwd);
if let Err(e) = migrate::import(&cwd, &opts) {
return Ok(handle_migrate_error(e));
}
}
MigrateCmd::Info {
branches,
everything,
include,
exclude,
include_ref,
exclude_ref,
above,
top,
pointers,
unit,
skip_fetch: _,
remote: _,
fixup,
} => {
let pointer_mode = match pointers.as_deref() {
Some("follow") => migrate::PointerMode::Follow,
Some("no-follow") => migrate::PointerMode::NoFollow,
Some("ignore") => migrate::PointerMode::Ignore,
Some(other) => {
return Ok(handle_migrate_error(migrate::MigrateError::Usage(format!(
"Unsupported --pointers option value: {other:?}"
))));
}
None if fixup => migrate::PointerMode::Ignore,
None => migrate::PointerMode::Follow,
};
let above_bytes = migrate::parse_size(&above)?;
let unit_bytes = match unit.as_deref() {
None | Some("") => None,
Some(s) => Some(migrate::parse_size(s)?),
};
let opts = migrate::InfoOptions {
branches,
everything,
include: split_csv(&include),
exclude: split_csv(&exclude),
include_ref,
exclude_ref,
above: above_bytes,
top,
pointers: pointer_mode,
unit: unit_bytes,
fixup,
};
if let Err(e) = migrate::info(&cwd, &opts) {
return Ok(handle_migrate_error(e));
}
}
},
Command::Checkout {
paths,
to,
ours,
theirs,
base,
} => {
let opts = checkout::Options {
paths,
to,
ours,
theirs,
base,
};
match checkout::run(&cwd, &opts) {
Ok(()) => {}
Err(checkout::CheckoutError::NotInWorkTree) => {
println!("This operation must be run in a work tree.");
}
Err(checkout::CheckoutError::NotInRepo) => {
println!("Not in a Git repository.");
return Ok(128);
}
Err(checkout::CheckoutError::Usage(msg)) => {
eprintln!("{msg}");
return Ok(2);
}
Err(e) => return Err(e.into()),
}
}
Command::Prune { dry_run, verbose } => {
let opts = prune::Options { dry_run, verbose };
prune::run(&cwd, &opts)?;
}
Command::Fsck {
refspec,
objects,
pointers,
dry_run,
} => {
let _ = install::try_install_hooks(&cwd);
let mode = match (objects, pointers) {
(true, false) => fsck::Mode::Objects,
(false, true) => fsck::Mode::Pointers,
_ => fsck::Mode::Both,
};
let opts = fsck::Options { mode, dry_run };
let code = fsck::run(&cwd, refspec.as_deref(), &opts)?;
return Ok(code as u8);
}
Command::Status { porcelain, json } => {
let format = if json {
status::Format::Json
} else if porcelain {
status::Format::Porcelain
} else {
status::Format::Default
};
match status::run(&cwd, format) {
Ok(()) => {}
Err(status::StatusError::NotInRepo) => {
println!("Not in a Git repository.");
return Ok(128);
}
Err(status::StatusError::NotInWorkTree) => {
println!("This operation must be run in a work tree.");
return Ok(1);
}
Err(e) => return Err(e.into()),
}
}
Command::Lock {
paths,
remote,
refspec,
json,
} => {
let opts = lock::LockOptions {
remote,
refspec,
json,
};
let ok = lock::lock(&cwd, &paths, &opts)?;
if !ok {
return Err("one or more locks failed".into());
}
}
Command::Locks {
remote,
path,
id,
limit,
refspec,
verify,
local,
json,
} => {
let opts = lock::LocksOptions {
remote,
refspec,
path,
id,
limit,
verify,
local,
json,
};
lock::locks(&cwd, &opts)?;
}
Command::Unlock {
paths,
id,
force,
remote,
refspec,
json,
} => {
let opts = lock::UnlockOptions {
remote,
refspec,
id,
force,
json,
};
let ok = lock::unlock(&cwd, &paths, &opts)?;
if !ok {
return Err("one or more unlocks failed".into());
}
}
Command::LsFiles {
refspec,
long,
size,
name_only,
all,
debug,
json,
} => {
let format = if json {
ls_files::Format::Json
} else if debug {
ls_files::Format::Debug
} else {
ls_files::Format::Default
};
let opts = ls_files::Options {
long,
show_size: size,
name_only,
all,
format,
};
ls_files::run(&cwd, refspec.as_deref(), &opts)?;
}
Command::Untrack { patterns } => {
if patterns.is_empty() {
return Err("git lfs untrack <pattern> [pattern...]".into());
}
let work_tree = match git_lfs_git::work_tree_root(&cwd) {
Ok(p) => p,
Err(_) => {
eprintln!("fatal: not in a git repository");
return Ok(128);
}
};
let attrs_dir = if cwd.starts_with(&work_tree) {
cwd.clone()
} else {
work_tree
};
let _ = install::try_install_hooks(&cwd);
let outcome = track::untrack(&attrs_dir, &patterns)?;
for p in &outcome.removed {
println!("Untracking \"{p}\"");
}
for p in &outcome.missing {
println!("\"{p}\" was not tracked");
}
}
}
Ok(0)
}