use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use git_lfs_api::{Auth, BatchRequest, Client as ApiClient, ObjectSpec, Operation, Ref};
use git_lfs_creds::{CachingHelper, GitCredentialHelper, Helper, HelperChain};
use git_lfs_filter::FetchError;
use git_lfs_git::ConfigScope;
use git_lfs_pointer::Pointer;
use git_lfs_store::Store;
use git_lfs_transfer::{Report, Transfer, TransferConfig};
use tokio::runtime::Runtime;
pub struct LfsFetcher {
runtime: Runtime,
transfer: Result<Transfer, String>,
api: Result<ApiClient, String>,
refspec: Option<Ref>,
cwd: PathBuf,
}
impl LfsFetcher {
pub fn from_repo(cwd: &Path, store: &Store) -> std::io::Result<Self> {
Self::from_repo_with_remote(cwd, store, None)
}
pub fn from_repo_with_remote(
cwd: &Path,
store: &Store,
remote: Option<&str>,
) -> std::io::Result<Self> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let endpoint_url = git_lfs_git::endpoint_for_remote(cwd, remote);
let http = endpoint_url
.as_ref()
.map(|u| crate::http_client::build(cwd, u))
.unwrap_or_default();
let api = build_api_client_with(cwd, remote, http.clone());
let config = transfer_config_for(cwd);
let transfer = api
.clone()
.map(|c| Transfer::with_http_client(c, store.clone(), config, http));
let refspec = git_lfs_git::refs::current_refspec(cwd).map(Ref::new);
Ok(Self {
runtime,
transfer,
api,
refspec,
cwd: cwd.to_path_buf(),
})
}
pub fn persist_access_mode(&self) {
let Ok(api) = self.api.as_ref() else { return };
if !api.used_basic_auth() {
return;
}
let url = api.endpoint().as_str();
let key = format!("lfs.{url}.access");
if let Ok(Some(existing)) = git_lfs_git::config::get_effective(&self.cwd, &key)
&& existing.eq_ignore_ascii_case("basic")
{
return;
}
let _ = git_lfs_git::config::set(&self.cwd, ConfigScope::Local, &key, "basic");
}
#[must_use]
pub fn with_refspec(mut self, refspec: Option<String>) -> Self {
self.refspec = refspec.map(Ref::new);
self
}
pub fn fetch(&self, pointer: &Pointer) -> Result<(), FetchError> {
let report = self.download_many(vec![ObjectSpec {
oid: pointer.oid.to_string(),
size: pointer.size,
}])?;
if let Some((failed_oid, err)) = report.failed.into_iter().next() {
return Err(format!("download failed for {failed_oid}: {err}").into());
}
Ok(())
}
pub fn download_many(&self, specs: Vec<ObjectSpec>) -> Result<Report, FetchError> {
let transfer = self.transfer()?;
let report = self
.runtime
.block_on(transfer.download(specs, self.refspec.clone(), None))?;
Ok(report)
}
pub fn upload_many(&self, specs: Vec<ObjectSpec>) -> Result<Report, FetchError> {
let transfer = self.transfer()?;
let report = self
.runtime
.block_on(transfer.upload(specs, self.refspec.clone(), None))?;
Ok(report)
}
pub fn api_client(&self) -> Result<&ApiClient, FetchError> {
self.api
.as_ref()
.map_err(|m| -> FetchError { m.clone().into() })
}
pub fn runtime_block_on<F: std::future::Future>(&self, fut: F) -> F::Output {
self.runtime.block_on(fut)
}
pub fn preflight_verify_locks(
&self,
cwd: &Path,
remote_label: &str,
endpoint: &str,
) -> Result<crate::locks_verify::Outcome, crate::push::PushCommandError> {
let api = self
.api
.as_ref()
.map_err(|m| -> FetchError { m.clone().into() })
.map_err(crate::push::PushCommandError::Fetch)?;
crate::locks_verify::run(
&self.runtime,
api,
cwd,
remote_label,
endpoint,
self.refspec.as_ref(),
)
}
pub fn check_server_has(&self, specs: Vec<ObjectSpec>) -> Result<HashSet<String>, FetchError> {
if specs.is_empty() {
return Ok(HashSet::new());
}
let api = self
.api
.as_ref()
.map_err(|m| -> FetchError { m.clone().into() })?;
let mut req = BatchRequest::new(Operation::Upload, specs);
if let Some(r) = self.refspec.clone() {
req = req.with_ref(r);
}
let resp = self
.runtime
.block_on(api.batch(&req))
.map_err(|e| -> FetchError { e.to_string().into() })?;
Ok(resp
.objects
.into_iter()
.filter(|o| o.actions.is_none() && o.error.is_none())
.map(|o| o.oid)
.collect())
}
fn transfer(&self) -> Result<&Transfer, FetchError> {
self.transfer
.as_ref()
.map_err(|msg| -> FetchError { msg.clone().into() })
}
}
pub fn build_api_client(cwd: &Path, remote: Option<&str>) -> Result<ApiClient, String> {
let endpoint = git_lfs_git::endpoint_for_remote(cwd, remote)
.map_err(|e| format!("resolving LFS endpoint: {e}"))?;
let http = crate::http_client::build(cwd, &endpoint);
build_api_client_with(cwd, remote, http)
}
fn build_api_client_with(
cwd: &Path,
remote: Option<&str>,
http: reqwest::Client,
) -> Result<ApiClient, String> {
let endpoint = git_lfs_git::endpoint_for_remote(cwd, remote)
.map_err(|e| format!("resolving LFS endpoint: {e}"))?;
let url = url::Url::parse(&endpoint).map_err(|e| format!("invalid LFS endpoint: {e}"))?;
Ok(ApiClient::with_http_client(url, Auth::None, http)
.with_credential_helper(default_helper_chain()))
}
fn transfer_config_for(cwd: &Path) -> TransferConfig {
let mut config = TransferConfig::default();
if href_rewrite_enabled(cwd)
&& let Ok(aliases) = git_lfs_git::aliases::load_aliases(cwd)
&& !aliases.is_empty()
{
config.url_rewriter = Some(Arc::new(move |url: &str| {
git_lfs_git::aliases::apply(&aliases, url)
}));
}
if let Ok(Some(raw)) = git_lfs_git::config::get_effective(cwd, "lfs.transfer.batchSize")
&& let Ok(n) = raw.trim().parse::<usize>()
&& n > 0
{
config.batch_size = n;
}
config
}
fn href_rewrite_enabled(cwd: &Path) -> bool {
let raw = git_lfs_git::config::get_effective(cwd, "lfs.transfer.enablehrefrewrite")
.ok()
.flatten()
.unwrap_or_default();
matches!(
raw.to_ascii_lowercase().as_str(),
"true" | "1" | "yes" | "on"
)
}
fn default_helper_chain() -> Arc<dyn Helper> {
let helpers: Vec<Box<dyn Helper>> = vec![
Box::new(CachingHelper::new()),
Box::new(GitCredentialHelper::new()),
];
Arc::new(HelperChain::new(helpers))
}