use std::path::{Path, PathBuf};
use sley_config::GitConfig;
use sley_core::{GitError, ObjectFormat, ObjectId, Result};
use sley_formats::RepositoryLayout;
use sley_refs::{FileRefStore, RefTarget, RefUpdate};
use sley_transport::RemoteUrl;
use crate::fetch::{FetchOptions, FetchSource, fetch};
use crate::{CredentialProvider, ProgressSink};
const CLONE_UNBORN_BRANCH: &str = "__git_rs_clone_unborn__";
pub enum CloneSource {
Http(RemoteUrl),
Ssh(RemoteUrl),
Git(RemoteUrl),
Local {
git_dir: PathBuf,
common_git_dir: PathBuf,
},
}
pub struct CloneOptions<'a> {
pub origin: &'a str,
pub checkout_branch: &'a str,
pub remote_head_branch: &'a str,
pub single_branch: bool,
pub depth: Option<u32>,
pub deepen_since: Option<i64>,
pub deepen_not: Vec<String>,
pub committer: Vec<u8>,
pub detached_head: Option<ObjectId>,
pub filter: Option<sley_odb::PackObjectFilter>,
}
#[derive(Debug, Clone)]
pub struct CloneOutcome {
pub git_dir: PathBuf,
pub branch_oid: ObjectId,
}
pub struct CloneRequest<'a> {
pub destination: &'a Path,
pub format: ObjectFormat,
pub source: &'a CloneSource,
pub options: &'a CloneOptions<'a>,
}
pub struct CloneServices<'a> {
pub configure: &'a mut dyn FnMut(&Path) -> Result<GitConfig>,
pub configure_branch: &'a mut dyn FnMut(&Path, &str) -> Result<GitConfig>,
pub credentials: &'a mut dyn CredentialProvider,
pub progress: &'a mut dyn ProgressSink,
}
pub fn clone(request: CloneRequest<'_>, services: CloneServices<'_>) -> Result<CloneOutcome> {
let layout = RepositoryLayout::init_at_with_initial_branch(
request.destination,
request.format,
false,
CLONE_UNBORN_BRANCH,
)?;
let git_dir = layout.git_dir;
let config = (services.configure)(&git_dir)?;
let fetch_source = match request.source {
#[cfg(feature = "http")]
CloneSource::Http(remote) => FetchSource::Http(remote.clone()),
#[cfg(not(feature = "http"))]
CloneSource::Http(_) => {
return Err(GitError::Unsupported(
"HTTP transport is not enabled in this build".into(),
));
}
CloneSource::Ssh(remote) => FetchSource::Ssh(remote.clone()),
CloneSource::Git(remote) => FetchSource::Git(remote.clone()),
CloneSource::Local {
git_dir: remote_git_dir,
common_git_dir: remote_common_git_dir,
} => FetchSource::Local {
git_dir: remote_git_dir.clone(),
common_git_dir: remote_common_git_dir.clone(),
},
};
let fetch_options = clone_fetch_options(
request.options.depth,
request.options.deepen_since,
request.options.deepen_not.clone(),
request.options.filter,
);
fetch(
crate::fetch::FetchRequest {
git_dir: &git_dir,
format: request.format,
config: &config,
remote_name: request.options.origin,
source: &fetch_source,
refspecs: &[],
options: &fetch_options,
},
crate::fetch::FetchServices {
credentials: services.credentials,
progress: services.progress,
},
)?;
let store = FileRefStore::new(&git_dir, request.format);
if let Some(detached) = &request.options.detached_head {
sley_worktree::checkout_detached_filtered(
request.destination,
&git_dir,
request.format,
detached,
request.options.committer.clone(),
b"clone: checkout".to_vec(),
&config,
)?;
return Ok(CloneOutcome {
git_dir,
branch_oid: *detached,
});
}
let remote_branch_ref = format!(
"refs/remotes/{}/{}",
request.options.origin, request.options.checkout_branch
);
let branch_oid = match store.read_ref(&remote_branch_ref)? {
Some(RefTarget::Direct(oid)) => oid,
Some(RefTarget::Symbolic(_)) => {
return Err(GitError::Unsupported(
"clone remote-tracking branch must be direct".into(),
));
}
None => {
return Err(GitError::reference_not_found(format!(
"remote ref {remote_branch_ref}"
)));
}
};
store.create_branch(
request.options.checkout_branch,
branch_oid.clone(),
request.options.committer.clone(),
format!(
"branch: Created from {}/{}",
request.options.origin, request.options.checkout_branch
)
.into_bytes(),
)?;
let checkout_config = (services.configure_branch)(&git_dir, request.options.checkout_branch)?;
if !request.options.single_branch
|| request.options.checkout_branch == request.options.remote_head_branch
{
let mut tx = store.transaction();
tx.update(RefUpdate {
name: format!("refs/remotes/{}/HEAD", request.options.origin),
expected: None,
new: RefTarget::Symbolic(format!(
"refs/remotes/{}/{}",
request.options.origin, request.options.remote_head_branch
)),
reflog: None,
});
tx.commit()?;
}
sley_worktree::checkout_branch_filtered(
request.destination,
&git_dir,
request.format,
request.options.checkout_branch,
request.options.committer.clone(),
&checkout_config,
)?;
Ok(CloneOutcome {
git_dir,
branch_oid,
})
}
fn clone_fetch_options(
depth: Option<u32>,
deepen_since: Option<i64>,
deepen_not: Vec<String>,
filter: Option<sley_odb::PackObjectFilter>,
) -> FetchOptions {
FetchOptions {
quiet: true,
auto_follow_tags: true,
fetch_all_tags: false,
prune: false,
dry_run: false,
append: false,
write_fetch_head: true,
tag_option_explicit: false,
prune_option_explicit: false,
depth,
merge_src: None,
filter,
cloning: true,
update_shallow: false,
deepen_relative: false,
deepen_since,
deepen_not,
}
}