use crate::ObjectId;
use crate::fetch::{Advertisement, RemoteHead, interpret_head};
use crate::refs::RefName;
use crate::remote::{Direction, Mapping, MappingError, Refspecs};
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum BranchSelection {
Default,
Branch(RefName),
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CloneHead {
Branch {
name: RefName,
id: ObjectId,
},
Detached(ObjectId),
Unborn(RefName),
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ClonePlan {
pub head: CloneHead,
pub mappings: Vec<Mapping>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ClonePlanError {
#[error(transparent)]
Mapping(#[from] MappingError),
#[error("clone requires a full refs/heads branch name")]
Branch,
#[error("selected clone branch is not advertised: {0:?}")]
MissingBranch(RefName),
#[error("remote has no advertised HEAD; select a branch explicitly")]
MissingHead,
#[error("inconsistent or unsupported symbolic HEAD advertisement")]
SymbolicHead,
}
pub(super) fn specs(has_head: bool) -> Refspecs {
let mut specs = vec![
"refs/heads/*:refs/remotes/origin/*",
"refs/tags/*:refs/tags/*",
];
if has_head {
specs.push("HEAD");
}
Refspecs::parse(Direction::Fetch, specs).expect("fixed clone refspecs")
}
pub(super) fn branch(name: &RefName) -> Result<(), ClonePlanError> {
if !name.as_bytes().starts_with(b"refs/heads/") {
return Err(ClonePlanError::Branch);
}
Ok(())
}
pub(super) fn plan(
selection: &BranchSelection,
ad: &Advertisement,
) -> Result<ClonePlan, ClonePlanError> {
let head = ad
.refs
.iter()
.find(|r| r.kind == crate::fetch::AdvertisementKind::Tip && r.name.as_bytes() == b"HEAD");
let mappings = specs(head.is_some()).map_advertisement(ad)?;
let tips: Vec<_> = ad
.refs
.iter()
.filter(|r| r.kind == crate::fetch::AdvertisementKind::Tip)
.collect();
let selected = match selection {
BranchSelection::Branch(name) => {
branch(name)?;
match tips.iter().find(|r| &r.name == name) {
Some(r) => CloneHead::Branch {
name: name.clone(),
id: r.id,
},
None if tips.is_empty() => CloneHead::Unborn(name.clone()),
None => return Err(ClonePlanError::MissingBranch(name.clone())),
}
}
BranchSelection::Default => {
match interpret_head(ad).map_err(|_| ClonePlanError::SymbolicHead)? {
RemoteHead::Symbolic { branch, id } => CloneHead::Branch { name: branch, id },
RemoteHead::Unborn { branch } if tips.is_empty() => CloneHead::Unborn(branch),
RemoteHead::Unborn { .. } => return Err(ClonePlanError::SymbolicHead),
RemoteHead::Inferred { id, .. }
| RemoteHead::Detached { id }
| RemoteHead::Ambiguous { id, .. } => CloneHead::Detached(id),
RemoteHead::Missing if tips.is_empty() => {
CloneHead::Unborn(crate::repository::initial_branch())
}
RemoteHead::Missing => return Err(ClonePlanError::MissingHead),
}
}
};
Ok(ClonePlan {
head: selected,
mappings,
})
}
pub(super) fn select(
result: Result<ClonePlan, ClonePlanError>,
saved: &mut Option<Result<ClonePlan, ClonePlanError>>,
) -> Vec<ObjectId> {
let wants = result
.as_ref()
.map(|p| {
p.mappings
.iter()
.filter_map(|m| m.source.as_ref().map(|s| s.id))
.collect()
})
.unwrap_or_default();
*saved = Some(result);
wants
}
#[cfg(test)]
mod tests;