use std::collections::HashSet;
use std::path::Path;
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_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>,
}
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 api = build_api_client(cwd, remote);
let transfer = api
.clone()
.map(|c| Transfer::new(c, store.clone(), TransferConfig::default()));
let refspec = git_lfs_git::refs::current_refspec(cwd).map(Ref::new);
Ok(Self {
runtime,
transfer,
api,
refspec,
})
}
#[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(|e| crate::push::PushCommandError::Fetch(e))?;
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 url = url::Url::parse(&endpoint).map_err(|e| format!("invalid LFS endpoint: {e}"))?;
Ok(ApiClient::new(url, Auth::None).with_credential_helper(default_helper_chain()))
}
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))
}