use std::path::{Path, PathBuf};
use sley_config::GitConfig;
use sley_core::{GitError, ObjectFormat, ObjectId, Result};
use sley_formats::{InitOptions, RefStorageFormat, RepositoryBootstrap};
use sley_object::{Commit, ObjectType, Tree};
use sley_odb::{FileObjectDatabase, ObjectReader};
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 {
remote: RemoteUrl,
protocol_v2: bool,
},
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 checkout: bool,
pub filter: Option<sley_odb::PackObjectFilter>,
pub branch_explicit: bool,
pub ref_storage: RefStorageFormat,
pub ssh_options: Option<crate::ssh::SshTransportOptions>,
}
#[derive(Debug, Clone)]
pub struct CloneOutcome {
pub git_dir: PathBuf,
pub branch_oid: Option<ObjectId>,
pub empty: bool,
}
pub struct CloneRequest<'a> {
pub destination: &'a Path,
pub git_dir_override: Option<&'a Path>,
pub core_worktree: Option<&'a str>,
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 = RepositoryBootstrap::init(InitOptions {
git_dir_override: request.git_dir_override.map(Path::to_path_buf),
core_worktree: request.core_worktree.map(str::to_string),
worktree: request.destination.to_path_buf(),
object_format: request.format,
object_format_explicit: false,
bare: false,
initial_branch: CLONE_UNBORN_BRANCH.into(),
template_dir: None,
copy_template_config: false,
separate_git_dir: None,
shared_repository: None,
ref_storage: request.options.ref_storage,
ref_storage_explicit: request.options.ref_storage != RefStorageFormat::Files,
})?;
let git_dir = layout.git_dir;
let config = (services.configure)(&git_dir)?;
crate::protocol::check_transport_allowed(
scheme_for_clone_source(request.source),
Some(&config),
None,
)
.map_err(crate::protocol::transport_policy_git_error)?;
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,
protocol_v2,
} => FetchSource::Git {
remote: remote.clone(),
protocol_v2: *protocol_v2,
},
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.clone(),
!request.options.checkout,
request.options.ssh_options,
);
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 {
if request.options.checkout {
sley_worktree::checkout_detached_filtered(
request.destination,
&git_dir,
request.format,
detached,
request.options.committer.clone(),
b"clone: checkout".to_vec(),
&config,
)?;
} else {
let mut tx = store.transaction();
tx.update(RefUpdate {
name: "HEAD".to_string(),
expected: None,
new: RefTarget::Direct(*detached),
reflog: None,
});
tx.commit()?;
}
return Ok(CloneOutcome {
git_dir,
branch_oid: Some(*detached),
empty: false,
});
}
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 => {
if request.options.branch_explicit {
return Err(GitError::reference_not_found(format!(
"remote ref {remote_branch_ref}"
)));
}
let unborn = format!("refs/heads/{}", request.options.checkout_branch);
let mut tx = store.transaction();
tx.update(RefUpdate {
name: "HEAD".to_string(),
expected: None,
new: RefTarget::Symbolic(unborn),
reflog: None,
});
tx.commit()?;
(services.configure_branch)(&git_dir, request.options.checkout_branch)?;
return Ok(CloneOutcome {
git_dir,
branch_oid: None,
empty: true,
});
}
};
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.checkout {
fetch_local_partial_clone_checkout_blobs(&request, &git_dir, branch_oid)?;
} else {
let mut tx = store.transaction();
tx.update(RefUpdate {
name: "HEAD".to_string(),
expected: None,
new: RefTarget::Symbolic(format!("refs/heads/{}", request.options.checkout_branch)),
reflog: None,
});
tx.commit()?;
}
if !request.options.remote_head_branch.is_empty()
&& (!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()?;
}
if request.options.checkout {
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: Some(branch_oid),
empty: false,
})
}
fn scheme_for_clone_source(source: &CloneSource) -> &'static str {
match source {
CloneSource::Http(remote) => crate::protocol::transport_scheme_for_remote(remote),
CloneSource::Ssh(remote) => crate::protocol::transport_scheme_for_remote(remote),
CloneSource::Git { remote, .. } => crate::protocol::transport_scheme_for_remote(remote),
CloneSource::Local { .. } => "file",
}
}
fn fetch_local_partial_clone_checkout_blobs(
request: &CloneRequest<'_>,
git_dir: &Path,
commit_oid: ObjectId,
) -> Result<()> {
if request.options.filter.is_none() {
return Ok(());
}
let CloneSource::Local {
git_dir: remote_git_dir,
common_git_dir: remote_common_git_dir,
} = request.source
else {
return Ok(());
};
let remote_db = FileObjectDatabase::from_git_dir(remote_common_git_dir, request.format);
let mut wants = Vec::new();
collect_checkout_blob_wants(&remote_db, request.format, commit_oid, &mut wants)?;
crate::local::install_fetch_pack_via_local_upload_pack(
git_dir,
remote_git_dir,
request.format,
wants,
None,
true,
false,
Some(sley_odb::PackObjectFilter::BlobNone),
false,
None,
)?;
Ok(())
}
fn collect_checkout_blob_wants(
db: &FileObjectDatabase,
format: ObjectFormat,
commit_oid: ObjectId,
wants: &mut Vec<ObjectId>,
) -> Result<()> {
let commit_object = db.read_object(&commit_oid)?;
if commit_object.object_type != ObjectType::Commit {
return Err(GitError::InvalidObject(format!(
"expected commit {commit_oid}, found {}",
commit_object.object_type.as_str()
)));
}
let commit = Commit::parse_ref(format, &commit_object.body)?;
collect_tree_blob_wants(db, format, commit.tree, wants)
}
fn collect_tree_blob_wants(
db: &FileObjectDatabase,
format: ObjectFormat,
tree_oid: ObjectId,
wants: &mut Vec<ObjectId>,
) -> Result<()> {
let tree_object = db.read_object(&tree_oid)?;
if tree_object.object_type != ObjectType::Tree {
return Err(GitError::InvalidObject(format!(
"expected tree {tree_oid}, found {}",
tree_object.object_type.as_str()
)));
}
for entry in Tree::parse(format, &tree_object.body)?.entries {
if entry.is_tree() {
collect_tree_blob_wants(db, format, entry.oid, wants)?;
} else if !entry.is_gitlink() {
wants.push(entry.oid);
}
}
Ok(())
}
fn clone_fetch_options(
depth: Option<u32>,
deepen_since: Option<i64>,
deepen_not: Vec<String>,
filter: Option<sley_odb::PackObjectFilter>,
record_promisor_refs: bool,
ssh_options: Option<crate::ssh::SshTransportOptions>,
) -> FetchOptions {
FetchOptions {
quiet: true,
auto_follow_tags: true,
fetch_all_tags: false,
prune: false,
prune_tags: false,
dry_run: false,
append: false,
write_fetch_head: true,
tag_option_explicit: false,
prune_option_explicit: false,
prune_tags_option_explicit: false,
refmap: None,
depth,
merge_srcs: Vec::new(),
filter,
refetch: false,
cloning: true,
record_promisor_refs,
update_shallow: false,
deepen_relative: false,
update_head_ok: false,
deepen_since,
deepen_not,
ssh_options,
}
}