use std::path::Path;
use std::sync::atomic::AtomicBool;
use super::*;
use crate::InitKind;
use crate::clone::{BranchSelection, CloneHead, CloneReport, CloneRequest};
use crate::config::{ConfigInputs, Document};
use crate::remote::RemoteConfig;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CloneOptions {
pub kind: InitKind,
pub checkout: bool,
pub selection: BranchSelection,
pub fetch_limits: FetchLimits,
pub update_limits: FetchUpdateLimits,
pub checkout_limits: crate::checkout::Limits,
pub filter_commands: Option<crate::filter::Commands>,
pub submodules: Option<crate::submodule::RecursionOptions>,
pub filter: Option<crate::fetch::ObjectFilter>,
}
impl Default for CloneOptions {
fn default() -> Self {
Self {
kind: InitKind::Worktree,
checkout: true,
selection: BranchSelection::Default,
fetch_limits: FetchLimits::default(),
update_limits: FetchUpdateLimits::default(),
checkout_limits: crate::checkout::Limits::default(),
filter_commands: Some(crate::filter::Commands::Run),
submodules: None,
filter: None,
}
}
}
impl CloneOptions {
#[must_use]
pub fn with_kind(mut self, kind: InitKind) -> Self {
self.kind = kind;
self
}
#[must_use]
pub fn with_checkout(mut self, checkout: bool) -> Self {
self.checkout = checkout;
self
}
#[must_use]
pub fn with_filter_commands(
mut self,
filter_commands: Option<crate::filter::Commands>,
) -> Self {
self.filter_commands = filter_commands;
self
}
#[must_use]
pub fn with_submodules(
mut self,
submodules: Option<crate::submodule::RecursionOptions>,
) -> Self {
self.submodules = submodules;
self
}
#[must_use]
pub fn with_filter(mut self, filter: crate::fetch::ObjectFilter) -> Self {
self.filter = Some(filter);
self
}
#[must_use]
pub fn with_selection(mut self, selection: BranchSelection) -> Self {
self.selection = selection;
self
}
#[must_use]
pub fn with_fetch_limits(mut self, fetch_limits: FetchLimits) -> Self {
self.fetch_limits = fetch_limits;
self
}
#[must_use]
pub fn with_update_limits(mut self, update_limits: FetchUpdateLimits) -> Self {
self.update_limits = update_limits;
self
}
#[must_use]
pub fn with_checkout_limits(mut self, checkout_limits: crate::checkout::Limits) -> Self {
self.checkout_limits = checkout_limits;
self
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct CloneOutcome {
pub clone: CloneReport,
pub checkout: Option<crate::checkout::Report>,
pub submodules: Option<crate::submodule::RecursiveReport>,
pub filter_applied: bool,
pub promised: Option<super::PromisorFetch>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CloneError {
#[error("clone integrity policy")]
Integrity(#[source] Box<crate::fsck::TransferError>),
#[error("recursive clone requires a working tree with checkout enabled")]
SubmoduleLayout,
#[error("superproject clone completed but submodule population failed")]
Submodules {
report: Box<CloneOutcome>,
#[source]
source: Box<crate::submodule::RecursiveError>,
},
#[error("clone checkout requires Linux/macOS; select no-checkout or bare mode")]
UnsupportedCheckout,
#[error(transparent)]
Config(#[from] crate::config::ConfigError),
#[error(transparent)]
Resolve(#[from] Box<crate::config::ResolveError>),
#[error(transparent)]
Transfer(#[from] TransferError),
#[error(transparent)]
Prepare(#[from] crate::clone::CloneFailure),
#[error(transparent)]
Receive(#[from] crate::clone::CloneTransferError),
#[error(transparent)]
Finish(#[from] Box<crate::clone::CloneError>),
#[error("clone completed but reopening or reading the selected commit failed")]
Read {
report: Box<CloneReport>,
#[source]
source: crate::Error,
},
#[error("clone completed but checkout failed; inspect the checkout report before recovery")]
Checkout {
report: Box<CloneReport>,
#[source]
source: Box<crate::checkout::Failure>,
},
#[error("clone completed but fetching the files to check out failed")]
FetchPromised {
report: Box<CloneReport>,
#[source]
source: Box<TransferError>,
},
}
pub fn clone(url: &str, destination: impl AsRef<Path>) -> Result<CloneOutcome, CloneError> {
let environment = Environment::from_process();
let inputs = ConfigInputs::git_default(&environment)?;
clone_with(
url,
destination,
&CloneOptions::default(),
&inputs,
&environment,
&mut NoCallbacks,
)
}
pub fn clone_with(
url: &str,
destination: impl AsRef<Path>,
options: &CloneOptions,
inputs: &ConfigInputs,
environment: &Environment,
callbacks: &mut dyn TransferCallbacks,
) -> Result<CloneOutcome, CloneError> {
if options.submodules.is_some() && (!options.checkout || matches!(options.kind, InitKind::Bare))
{
return Err(CloneError::SubmoduleLayout);
}
if options.checkout
&& matches!(options.kind, InitKind::Worktree)
&& !cfg!(any(target_os = "linux", target_os = "macos"))
{
return Err(CloneError::UnsupportedCheckout);
}
let base = match environment.current_dir() {
Some(path) => path.to_path_buf(),
None => std::env::current_dir().map_err(TransferError::local)?,
};
let destination = base.join(destination);
let config = crate::Config::resolve(inputs).map_err(Box::new)?;
let mut document = Document::parse(b"").map_err(TransferError::local)?;
RemoteConfig::new(&mut document)
.add(b"origin", url.as_bytes(), &[])
.map_err(TransferError::local)?;
let remote = Remote::find(document.config(), b"origin")
.map_err(TransferError::local)?
.ok_or_else(|| TransferError::NoSuchRemote("origin".to_owned()))?;
let (endpoint, resolved) = configured_endpoint(
&config,
&remote,
"origin",
Direction::Fetch,
super::LocalPaths {
base: &base,
git_dir: None,
},
environment,
&*callbacks,
)?;
let stored_url = if resolved.bytes() != url.as_bytes() || Path::new(url).is_absolute() {
url.as_bytes().to_vec()
} else {
crate::remote::ParsedUrl::parse(url.as_bytes())
.and_then(|parsed| parsed.canonicalize_local(&base))
.map_err(TransferError::local)?
};
let request = || {
CloneRequest::prepare_tracking(
&destination,
options.kind,
&stored_url,
options.selection.clone(),
Reflog::Preserve,
)
.map(|request| match &options.filter {
Some(filter) => request.with_filter(filter.clone()),
None => request,
})
};
let prepared = request()?;
let cancel = AtomicBool::new(false);
let control = TransportControl::new(&cancel);
let ready = match endpoint {
Endpoint::Custom(custom) => {
let mut connection = custom.connect(crate::transport::Service::UploadPack)?;
let download = prepared.receive_connection(
&mut **connection.get_mut(),
custom.protocol,
options.fetch_limits,
&cancel,
|message| callbacks.remote_message(message),
);
let finished = connection.into_inner().finish();
let validated = download.and_then(|download| {
download.validate_with_progress(
&cancel,
|_| ControlFlow::Continue(()),
|event| callbacks.validation_progress(event),
)
});
if let (Err(crate::clone::CloneTransferError::Transfer(cause)), Err(error)) =
(&validated, finished)
&& super::is_stream_failure(cause)
{
return Err(TransferError::from(error).into());
}
validated
}
Endpoint::Local(path) => {
let callbacks = std::cell::RefCell::new(&mut *callbacks);
prepared.receive_local_with_progress(
path,
options.fetch_limits,
control,
crate::fetch::LocalFetchObserver::default()
.with_messages(&mut |message| {
callbacks.borrow_mut().remote_message(message);
ControlFlow::Continue(())
})
.with_construction(&mut |event| callbacks.borrow_mut().fetch_progress(event))
.with_validation(&mut |event| {
callbacks.borrow_mut().validation_progress(event)
}),
)
}
#[cfg(feature = "http")]
Endpoint::Http(http) => {
let runtime = runtime()?;
let mut download = runtime.block_on(prepared.receive_http_with_progress(
&http.remote(None)?,
options.fetch_limits,
control,
|message| callbacks.remote_message(message),
));
http.settle_proxy(match &download {
Ok(_) => Ok(()),
Err(crate::clone::CloneTransferError::Transfer(error)) => {
Err(super::status_of(error))
}
Err(_) => Err(None),
});
if matches!(
&download,
Err(crate::clone::CloneTransferError::Transfer(
crate::fetch::FetchError::Http(crate::transport::http::HttpError::Status(401))
))
) {
let session = http.credentials(callbacks)?;
download = runtime.block_on(request()?.receive_http_with_progress(
&http.remote(Some(session))?,
options.fetch_limits,
control,
|message| callbacks.remote_message(message),
));
}
download.and_then(|download| {
download.validate_with_progress(
&cancel,
|_| ControlFlow::Continue(()),
|event| callbacks.validation_progress(event),
)
})
}
#[cfg(all(feature = "ssh", any(target_os = "macos", target_os = "linux")))]
Endpoint::Ssh(ssh) => {
let callbacks = std::cell::RefCell::new(&mut *callbacks);
runtime()?
.block_on(prepared.receive_ssh_with_progress(
&ssh,
options.fetch_limits,
control,
|message| callbacks.borrow_mut().transport_message(message),
|message| callbacks.borrow_mut().remote_message(message),
))
.and_then(|download| {
download.validate_with_progress(
&cancel,
|_| ControlFlow::Continue(()),
|event| callbacks.borrow_mut().validation_progress(event),
)
})
}
}?;
let filter_applied = ready.received().filter_applied();
let integrity = ready
.received()
.enforce_fsck(&config, crate::fsck::Scope::Fetch, &base, &cancel)
.map_err(|error| CloneError::Integrity(Box::new(error)))?;
let mut report = ready
.finish(options.update_limits, &cancel)
.map_err(|error| CloneError::Finish(Box::new(error)))?;
if let Some(fetch) = &mut report.fetch {
fetch.integrity = integrity;
}
match Repository::open_with_config(&report.destination, inputs) {
Ok(repository) => report.repository = Some(repository),
Err(source) => {
return Err(CloneError::Read {
report: Box::new(report),
source: source.into(),
});
}
}
let promised = match fetch_checkout_blobs(&report, options, environment, callbacks) {
Ok(promised) => promised,
Err(PrefetchError::Read(source)) => {
return Err(CloneError::Read {
report: Box::new(report),
source,
});
}
Err(PrefetchError::Fetch(source)) => {
return Err(CloneError::FetchPromised {
report: Box::new(report),
source: Box::new(source),
});
}
};
let checkout = checkout(&report, options, environment, &cancel, callbacks);
match checkout {
Ok(checkout) => {
let mut outcome = CloneOutcome {
clone: report,
checkout,
submodules: None,
filter_applied,
promised,
};
if let Some(policy) = &options.submodules {
if outcome.checkout.is_some() {
let repository = outcome
.clone
.repository
.as_ref()
.ok_or(CloneError::SubmoduleLayout)?;
match crate::submodule::populate_recursive(
repository,
&stored_url,
policy,
callbacks,
) {
Ok(report) => outcome.submodules = Some(report),
Err(source) => {
return Err(CloneError::Submodules {
report: Box::new(outcome),
source: Box::new(source),
});
}
}
}
}
Ok(outcome)
}
Err(CheckoutError::Read(source)) => Err(CloneError::Read {
report: Box::new(report),
source,
}),
Err(CheckoutError::Checkout(source)) => Err(CloneError::Checkout {
report: Box::new(report),
source: Box::new(source),
}),
}
}
enum CheckoutError {
Read(crate::Error),
Checkout(crate::checkout::Failure),
}
enum PrefetchError {
Read(crate::Error),
Fetch(TransferError),
}
fn checkout_commit(report: &CloneReport, options: &CloneOptions) -> Option<crate::ObjectId> {
if !options.checkout || matches!(options.kind, InitKind::Bare) {
return None;
}
match report.head {
CloneHead::Branch { id, .. } | CloneHead::Detached(id) => Some(id),
_ => None,
}
}
fn fetch_checkout_blobs(
report: &CloneReport,
options: &CloneOptions,
environment: &Environment,
callbacks: &mut dyn TransferCallbacks,
) -> Result<Option<super::PromisorFetch>, PrefetchError> {
if options.filter.is_none() {
return Ok(None);
}
let Some(commit) = checkout_commit(report, options) else {
return Ok(None);
};
let repository = report
.repository
.as_ref()
.ok_or_else(|| read(std::io::Error::other("completed clone has no repository")))?;
let mut fetched = super::PromisorFetch::default();
fn read(error: impl Into<crate::Error>) -> PrefetchError {
PrefetchError::Read(error.into())
}
let root = repository
.objects()
.map_err(read)?
.read_commit(commit)
.map_err(read)?
.tree();
let mut trees = vec![root];
let mut blobs = Vec::new();
while !trees.is_empty() {
let objects = repository.objects().map_err(read)?;
let mut missing = Vec::new();
for &id in &trees {
if !objects.contains(id).map_err(read)? {
missing.push(id);
}
}
if !missing.is_empty() {
let report = super::fetch_promised(repository, &missing, environment, callbacks)
.map_err(PrefetchError::Fetch)?;
fetched.fetched.extend(report.fetched);
continue;
}
let mut next = Vec::new();
for id in trees.drain(..) {
let tree = objects.read_tree(id).map_err(read)?;
for entry in tree.entries() {
match entry.mode {
crate::EntryMode::Tree => next.push(entry.id),
crate::EntryMode::Gitlink => {}
_ => {
if !objects.contains(entry.id).map_err(read)? {
blobs.push(entry.id);
}
}
}
}
}
trees = next;
}
if !blobs.is_empty() {
let report = super::fetch_promised(repository, &blobs, environment, callbacks)
.map_err(PrefetchError::Fetch)?;
fetched.fetched.extend(report.fetched);
fetched.already_present.extend(report.already_present);
}
Ok(Some(fetched))
}
fn checkout(
report: &CloneReport,
options: &CloneOptions,
environment: &Environment,
cancel: &AtomicBool,
callbacks: &mut dyn TransferCallbacks,
) -> Result<Option<crate::checkout::Report>, CheckoutError> {
if !options.checkout || matches!(options.kind, InitKind::Bare) {
return Ok(None);
}
let id = match report.head {
CloneHead::Branch { id, .. } | CloneHead::Detached(id) => id,
_ => return Ok(None),
};
let tree = (|| -> crate::Result<_> {
let repository = report
.repository
.as_ref()
.ok_or_else(|| std::io::Error::other("completed clone has no repository"))?;
let tree = repository.objects()?.read_commit(id)?.tree();
Ok((repository, tree))
})()
.map_err(CheckoutError::Read)?;
let (repository, tree) = tree;
let Some(commands) = options.filter_commands else {
return repository
.checkout_tree_with_progress(
None,
Some(tree),
options.checkout_limits,
cancel,
|done, total| callbacks.checkout_progress(done, total),
)
.map(Some)
.map_err(CheckoutError::Checkout);
};
let pipelines = (|| -> crate::Result<_> {
let baseline = repository.filters(environment, commands)?;
let target = repository.filters_for_checkout(environment, tree, commands)?;
Ok((baseline, target))
})();
let (mut baseline, mut target) = pipelines.map_err(CheckoutError::Read)?;
repository
.checkout_filtered_with_progress(
None,
Some(tree),
&mut baseline,
&mut target,
options.checkout_limits,
cancel,
&mut |done, total| callbacks.checkout_progress(done, total),
)
.map(Some)
.map_err(CheckoutError::Checkout)
}