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>,
}
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),
}
}
}
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_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>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CloneError {
#[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>,
},
}
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.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,
&base,
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,
)
};
let prepared = request()?;
let cancel = AtomicBool::new(false);
let control = TransportControl::new(&cancel);
let ready = match endpoint {
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),
));
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 mut report = ready
.finish(options.update_limits, &cancel)
.map_err(|error| CloneError::Finish(Box::new(error)))?;
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 checkout = checkout(&report, options, environment, &cancel, callbacks);
match checkout {
Ok(checkout) => Ok(CloneOutcome {
clone: report,
checkout,
}),
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),
}
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)
}