use std::path::{Path, PathBuf};
use std::process::Command;
use crate::install;
use crate::pull;
#[derive(Debug, thiserror::Error)]
pub enum CloneError {
#[error("missing repository URL")]
MissingRepo,
#[error("`git clone` failed (exit {0})")]
CloneFailed(i32),
#[error("Unable to find clone dir at {0:?}")]
MissingCloneDir(String),
#[error(transparent)]
Pull(#[from] pull::PullCommandError),
#[error(transparent)]
Io(#[from] std::io::Error),
}
struct DecodedArgs {
forward: Vec<String>,
positional: Vec<String>,
no_checkout: bool,
bare: bool,
skip_repo: bool,
recurse_submodules: bool,
include: Vec<String>,
exclude: Vec<String>,
}
pub fn run(cwd: &Path, raw_args: &[String]) -> Result<(), CloneError> {
let decoded = decode_args(raw_args);
if decoded.positional.is_empty() {
return Err(CloneError::MissingRepo);
}
eprintln!("WARNING: `git lfs clone` is deprecated and will not be updated");
eprintln!(" with new flags from `git clone`");
eprintln!();
eprintln!("`git clone` has been updated in upstream Git to have comparable");
eprintln!("speeds to `git lfs clone`.");
let mut cmd = Command::new("git");
cmd.current_dir(cwd);
for kv in [
"filter.lfs.smudge=",
"filter.lfs.clean=",
"filter.lfs.process=",
"filter.lfs.required=false",
] {
cmd.args(["-c", kv]);
}
cmd.arg("clone");
for a in &decoded.forward {
cmd.arg(a);
}
let status = cmd.status()?;
if !status.success() {
return Err(CloneError::CloneFailed(status.code().unwrap_or(1)));
}
let clonedir = derive_clone_dir(cwd, &decoded.positional)?;
if !decoded.bare && !decoded.no_checkout && has_resolvable_head(&clonedir) {
let refs: Vec<String> = Vec::new();
pull::pull_with_filter(&clonedir, &refs, &decoded.include, &decoded.exclude)?;
if decoded.recurse_submodules {
let status = Command::new("git")
.current_dir(&clonedir)
.args(["submodule", "foreach", "--recursive", "git lfs pull"])
.status()?;
if !status.success() {
eprintln!(
"Error performing `git lfs pull` for submodules: exit {}",
status.code().unwrap_or(1)
);
}
}
}
if !decoded.skip_repo && !decoded.bare {
let _ = install::try_install_hooks(&clonedir);
}
Ok(())
}
fn decode_args(raw: &[String]) -> DecodedArgs {
let mut forward = Vec::with_capacity(raw.len());
let mut positional = Vec::new();
let mut no_checkout = false;
let mut bare = false;
let mut skip_repo = false;
let mut recurse_submodules = false;
let mut include: Vec<String> = Vec::new();
let mut exclude: Vec<String> = Vec::new();
let mut after_dashdash = false;
let mut i = 0;
while i < raw.len() {
let a = &raw[i];
if after_dashdash {
forward.push(a.clone());
positional.push(a.clone());
i += 1;
continue;
}
if a == "--" {
forward.push(a.clone());
after_dashdash = true;
i += 1;
continue;
}
if a == "--skip-repo" {
skip_repo = true;
i += 1;
continue;
}
if a == "-I" || a == "--include" {
if let Some(v) = raw.get(i + 1) {
include.push(v.clone());
}
i += 2;
continue;
}
if let Some(v) = a.strip_prefix("--include=") {
include.push(v.to_string());
i += 1;
continue;
}
if a == "-X" || a == "--exclude" {
if let Some(v) = raw.get(i + 1) {
exclude.push(v.clone());
}
i += 2;
continue;
}
if let Some(v) = a.strip_prefix("--exclude=") {
exclude.push(v.to_string());
i += 1;
continue;
}
if a == "--no-checkout" || a == "-n" {
no_checkout = true;
} else if a == "--bare" {
bare = true;
} else if a == "--recursive"
|| a == "--recurse-submodules"
|| a.starts_with("--recurse-submodules=")
{
recurse_submodules = true;
}
if let Some(rest) = a.strip_prefix('-')
&& !rest.starts_with('-')
&& rest.len() > 1
&& rest.contains('n')
{
no_checkout = true;
}
if a.starts_with('-') {
forward.push(a.clone());
if (value_taking_long(a) || value_taking_short(a)) && i + 1 < raw.len() {
forward.push(raw[i + 1].clone());
i += 2;
continue;
}
i += 1;
} else {
forward.push(a.clone());
positional.push(a.clone());
i += 1;
}
}
DecodedArgs {
forward,
positional,
no_checkout,
bare,
skip_repo,
recurse_submodules,
include,
exclude,
}
}
fn value_taking_long(s: &str) -> bool {
if s.contains('=') {
return false;
}
matches!(
s,
"--template"
| "--origin"
| "--branch"
| "--upload-pack"
| "--reference"
| "--reference-if-able"
| "--separate-git-dir"
| "--depth"
| "--config"
| "--shallow-since"
| "--shallow-exclude"
| "--jobs"
| "--server-option"
| "--filter"
| "--bundle-uri"
| "--revision"
)
}
fn value_taking_short(s: &str) -> bool {
matches!(s, "-o" | "-b" | "-u" | "-c" | "-j")
}
fn has_resolvable_head(cwd: &Path) -> bool {
Command::new("git")
.arg("-C")
.arg(cwd)
.args(["rev-parse", "--verify", "--quiet", "HEAD"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn derive_clone_dir(cwd: &Path, args: &[String]) -> Result<PathBuf, CloneError> {
let last = args.last().ok_or(CloneError::MissingRepo)?;
let abs_last = if Path::new(last).is_absolute() {
PathBuf::from(last)
} else {
cwd.join(last)
};
if abs_last.is_dir() {
return Ok(abs_last);
}
let base = Path::new(last)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let base = base.strip_suffix(".git").unwrap_or(&base);
let derived = cwd.join(base);
if derived.is_dir() {
return Ok(derived);
}
Err(CloneError::MissingCloneDir(derived.display().to_string()))
}