use std::fs;
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use super::{BranchSelection, CloneHead, ClonePlan, ClonePlanError, config, plan};
use crate::fetch::{
Advertisement, FetchError, FetchFinishError, FetchLimits, FetchPlanError, FetchReady,
FetchReport, FetchRequest, FetchUpdateLimits, ReceivedFetch,
};
use crate::refs::{Expected, RefEdit, RefEditOutcome, RefName, Reflog, Target};
use crate::transport::TransportControl;
use crate::{InitKind, ObjectKind, Repository};
pub struct CloneRequest {
pub(super) destination: PathBuf,
pub(super) kind: InitKind,
url: Vec<u8>,
pub(super) selection: BranchSelection,
reflog: Reflog,
}
impl std::fmt::Debug for CloneRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CloneRequest")
.field("destination", &self.destination)
.field("kind", &self.kind)
.field("selection", &self.selection)
.finish_non_exhaustive()
}
}
impl CloneRequest {
pub fn prepare_tracking(
destination: impl AsRef<Path>,
kind: InitKind,
url: &[u8],
selection: BranchSelection,
reflog: Reflog,
) -> Result<Self, CloneFailure> {
if url.is_empty() || url.contains(&0) || url.contains(&b'\r') {
return Err(CloneFailure::Url);
}
if let BranchSelection::Branch(name) = &selection {
plan::branch(name)?;
}
let destination = destination.as_ref().to_path_buf();
absent(&destination)?;
Ok(Self {
destination,
kind,
url: url.to_vec(),
selection,
reflog,
})
}
pub fn plan(&self, advertisement: &Advertisement) -> Result<ClonePlan, ClonePlanError> {
plan::plan(&self.selection, advertisement)
}
pub fn receive_local(
self,
source: impl AsRef<Path>,
limits: FetchLimits,
control: TransportControl<'_>,
mut progress: impl FnMut(&[u8]) -> ControlFlow<()>,
) -> Result<CloneReady, CloneTransferError> {
self.receive_local_with_progress(
source,
limits,
control,
crate::fetch::LocalFetchObserver::default().with_messages(&mut progress),
)
}
pub fn receive_local_with_progress(
self,
source: impl AsRef<Path>,
limits: FetchLimits,
control: TransportControl<'_>,
observer: crate::fetch::LocalFetchObserver<'_>,
) -> Result<CloneReady, CloneTransferError> {
let mut saved = None;
let received = crate::fetch::receive_local(
source,
|ad| plan::select(self.plan(ad), &mut saved),
crate::fetch::LocalFetchRequest::new(limits),
control,
observer,
);
let (plan, received) = selected(saved, received)?;
Ok(CloneReady {
request: self,
head: plan.head,
received,
})
}
}
pub(super) fn selected<T>(
saved: Option<Result<ClonePlan, ClonePlanError>>,
transfer: Result<T, FetchError>,
) -> Result<(ClonePlan, T), CloneTransferError> {
let plan = saved.transpose()?;
let transfer = transfer?;
let plan = plan.ok_or(FetchError::Protocol("selection callback was not invoked"))?;
Ok((plan, transfer))
}
#[derive(Debug)]
pub struct CloneReady {
pub(super) request: CloneRequest,
pub(super) head: CloneHead,
pub(super) received: ReceivedFetch,
}
impl CloneReady {
pub fn head(&self) -> &CloneHead {
&self.head
}
pub fn finish(
self,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
) -> Result<CloneReport, CloneError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"clone.finish",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
let mut report = CloneReport {
destination: self.request.destination.clone(),
reserved: false,
initialized: false,
fetch: None,
configured: false,
packed_refs: None,
references: Vec::new(),
repository: None,
head: self.head.clone(),
};
let result = self.create_finish(limits, cancel, &mut report);
match result {
Ok(()) => Ok(report),
Err(source) => Err(CloneError {
report: Box::new(report),
source,
}),
}
};
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = { operation }();
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, |error| {
crate::trace::clone_finish(error, &span)
});
result
}
fn create_finish(
self,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
report: &mut CloneReport,
) -> Result<(), CloneFailure> {
cancelled(cancel)?;
absent(&self.request.destination)?;
fs::create_dir(&self.request.destination)?;
report.reserved = true;
let repo = Repository::init_reserved_clone(
&self.request.destination,
self.request.kind,
self.received.object_format(),
)?;
report.initialized = true;
self.finish_in(repo, limits, cancel, report)
}
fn finish_in(
self,
repo: Repository,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
report: &mut CloneReport,
) -> Result<(), CloneFailure> {
let has_head = self.received.advertisement().refs.iter().any(|r| {
r.kind == crate::fetch::AdvertisementKind::Tip && r.name.as_bytes() == b"HEAD"
});
let request = FetchRequest::prepare(
repo,
plan::specs(has_head),
Default::default(),
self.request.reflog.clone(),
)?;
let ready = FetchReady::for_clone(request, self.received)?;
report.fetch = Some(ready.finish(limits, cancel)?);
let repo = Repository::open(&self.request.destination)?;
verify_branches(
&repo,
report.fetch.as_ref().unwrap(),
&self.head,
limits,
cancel,
)?;
cancelled(cancel)?;
let bytes = config::contents(
self.request.kind,
repo.object_format(),
&self.request.url,
&self.head,
);
config::publish(&repo, self.request.kind, &bytes).map_err(CloneFailure::Configuration)?;
report.configured = true;
cancelled(cancel)?;
if repo.reference_backend() == crate::refs::Backend::Files {
let objects = repo.objects(limits.snapshot)?;
report.packed_refs = Some(
repo.references()?
.pack(&objects, crate::refs::PackRefsOptions::ALL)?,
);
}
cancelled(cancel)?;
report.references = repo
.references()?
.transaction(&head_edits(&self.head, &self.request.reflog))?;
report.repository = Some(Repository::open(&self.request.destination)?);
Ok(())
}
}
fn verify_branches(
repo: &Repository,
fetch: &FetchReport,
head: &CloneHead,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
) -> Result<(), CloneFailure> {
let objects = repo
.objects(limits.snapshot)
.map_err(FetchError::Destination)?;
let detached = match head {
CloneHead::Detached(id) => Some(*id),
_ => None,
};
let branches = fetch
.updates
.iter()
.filter_map(|u| u.mapping.source.as_ref())
.filter(|s| s.name.as_bytes().starts_with(b"refs/heads/"))
.map(|s| s.id);
for id in branches.chain(detached) {
cancelled(cancel)?;
let object = objects
.read(id, limits.verification.known_read)
.map_err(FetchError::from)?
.ok_or(FetchError::Missing(id))?;
if object.kind() != ObjectKind::Commit {
return Err(FetchError::Kind(id).into());
}
}
Ok(())
}
fn head_edits(head: &CloneHead, reflog: &Reflog) -> Vec<RefEdit> {
let mut edits = Vec::new();
let target = match head {
CloneHead::Branch { name, id } => {
edits.push(RefEdit {
name: name.clone(),
dereference: crate::refs::Dereference::Stored,
target: Some(Target::Direct(*id)),
expected: Expected::Absent,
reflog: reflog.clone(),
});
Target::Symbolic(name.clone())
}
CloneHead::Detached(id) => Target::Direct(*id),
CloneHead::Unborn(name) => Target::Symbolic(name.clone()),
};
let head_reflog = if matches!(target, Target::Direct(_)) {
reflog.clone()
} else {
Reflog::Preserve
};
edits.push(RefEdit {
name: RefName::HEAD,
dereference: crate::refs::Dereference::Stored,
target: Some(target),
expected: Expected::Value(Target::Symbolic(crate::repository::initial_branch())),
reflog: head_reflog,
});
edits
}
fn absent(path: &Path) -> Result<(), CloneFailure> {
match fs::symlink_metadata(path) {
Ok(_) => Err(CloneFailure::Exists(path.into())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e.into()),
}
}
fn cancelled(cancel: &AtomicBool) -> Result<(), CloneFailure> {
if cancel.load(Ordering::Relaxed) {
return Err(FetchError::Cancelled.into());
}
Ok(())
}
#[derive(Debug)]
#[non_exhaustive]
pub struct CloneReport {
pub destination: PathBuf,
pub reserved: bool,
pub initialized: bool,
pub fetch: Option<FetchReport>,
pub configured: bool,
pub packed_refs: Option<crate::refs::PackRefsReport>,
pub references: Vec<RefEditOutcome>,
pub repository: Option<Repository>,
pub head: CloneHead,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CloneTransferError {
#[error(transparent)]
Plan(#[from] ClonePlanError),
#[error(transparent)]
Transfer(#[from] FetchError),
}
#[derive(Debug, thiserror::Error)]
#[error("clone failed")]
#[non_exhaustive]
pub struct CloneError {
pub report: Box<CloneReport>,
#[source]
pub source: CloneFailure,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CloneFailure {
#[error("clone destination already exists: {0}")]
Exists(PathBuf),
#[error("invalid stored clone URL")]
Url,
#[error(transparent)]
Plan(#[from] ClonePlanError),
#[error("clone destination")]
Io(#[from] std::io::Error),
#[error(transparent)]
Initialization(#[from] crate::InitError),
#[error(transparent)]
Open(#[from] crate::OpenError),
#[error(transparent)]
FetchPlan(#[from] FetchPlanError),
#[error(transparent)]
Fetch(#[from] FetchFinishError),
#[error(transparent)]
Verification(#[from] FetchError),
#[error("clone configuration")]
Configuration(#[source] std::io::Error),
#[error(transparent)]
Reference(#[from] crate::refs::ReferenceError),
#[error(transparent)]
PackRefs(#[from] crate::refs::PackRefsError),
#[error(transparent)]
Objects(#[from] crate::ObjectReadError),
#[error(transparent)]
Publication(#[from] crate::refs::TransactionError),
}
#[cfg(test)]
#[path = "workflow_tests.rs"]
mod tests;