use std::collections::HashSet;
use crate::ObjectId;
use crate::fetch::Advertisement;
use crate::refs::RefName;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum Direction {
Fetch,
Push,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Refspec {
raw: Vec<u8>,
source: Vec<u8>,
destination: Option<Vec<u8>>,
force: bool,
negative: bool,
wildcard: bool,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
#[error("refspec at index {index}")]
#[non_exhaustive]
pub struct RefspecsError {
pub index: usize,
#[source]
pub source: RefspecError,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum RefspecError {
#[error("invalid refspec syntax: {0}")]
Syntax(&'static str),
#[error("unsupported refspec form: {0}")]
Unsupported(&'static str),
#[error("invalid full reference name or pattern")]
Name,
}
impl Refspec {
pub fn parse(direction: Direction, bytes: &[u8]) -> Result<Self, RefspecError> {
let force = bytes.starts_with(b"+");
let body = if force { &bytes[1..] } else { bytes };
let negative = body.starts_with(b"^");
let body = if negative { &body[1..] } else { body };
if negative && (direction != Direction::Fetch || force || body.contains(&b':')) {
return Err(RefspecError::Syntax(
"negative specs are fetch-only, without + or destination",
));
}
if body.is_empty() || body == b":" || body.starts_with(b"tag ") {
return Err(RefspecError::Unsupported(
"empty/default, matching, or tag shorthand",
));
}
let mut parts = body.split(|byte| *byte == b':');
let source = parts.next().unwrap();
let explicit_destination = parts.next();
if parts.next().is_some() {
return Err(RefspecError::Syntax("more than one colon"));
}
let destination = match (direction, explicit_destination, negative) {
(_, _, true) => None,
(Direction::Fetch, None | Some(b""), false) => None,
(Direction::Push, None, false) => Some(source),
(Direction::Push, Some(b""), false) => {
return Err(RefspecError::Unsupported("empty push destination"));
}
(_, value, false) => value,
};
if source.is_empty() && direction == Direction::Fetch {
return Err(RefspecError::Unsupported("empty fetch source"));
}
let source_stars = source.iter().filter(|b| **b == b'*').count();
let destination_stars = destination.map_or(0, |s| s.iter().filter(|b| **b == b'*').count());
if source_stars > 1
|| destination_stars > 1
|| (!negative && source_stars != destination_stars)
{
return Err(RefspecError::Syntax(
"wildcards require exactly one * on both sides",
));
}
if !source.is_empty() {
validate_pattern(source, true)?;
}
if let Some(destination) = destination {
validate_pattern(destination, false)?;
}
Ok(Self {
raw: bytes.to_vec(),
source: source.to_vec(),
destination: destination.map(<[u8]>::to_vec),
force,
negative,
wildcard: source_stars == 1,
})
}
pub fn as_bytes(&self) -> &[u8] {
&self.raw
}
pub fn force(&self) -> bool {
self.force
}
pub fn is_negative(&self) -> bool {
self.negative
}
fn capture<'a>(&self, name: &'a [u8]) -> Option<&'a [u8]> {
if self.wildcard {
let star = self.source.iter().position(|b| *b == b'*').unwrap();
let prefix = &self.source[..star];
let suffix = &self.source[star + 1..];
name.strip_prefix(prefix)?.strip_suffix(suffix)
} else {
(self.source == name).then_some(b"")
}
}
fn destination(&self, capture: &[u8]) -> Result<Option<RefName>, MappingError> {
let Some(pattern) = &self.destination else {
return Ok(None);
};
let bytes = if let Some(star) = pattern.iter().position(|b| *b == b'*') {
[&pattern[..star], capture, &pattern[star + 1..]].concat()
} else {
pattern.clone()
};
let name = RefName::new(bytes).map_err(|_| MappingError::InvalidDestination)?;
Ok(Some(name))
}
pub(crate) fn source_for_destination(&self, destination: &RefName) -> Option<Vec<u8>> {
if self.negative {
return None;
}
let pattern = self.destination.as_ref()?;
let capture = if let Some(star) = pattern.iter().position(|byte| *byte == b'*') {
destination
.as_bytes()
.strip_prefix(&pattern[..star])?
.strip_suffix(&pattern[star + 1..])?
} else if pattern == destination.as_bytes() {
b""
} else {
return None;
};
if let Some(star) = self.source.iter().position(|byte| *byte == b'*') {
Some([&self.source[..star], capture, &self.source[star + 1..]].concat())
} else {
Some(self.source.clone())
}
}
}
fn validate_pattern(bytes: &[u8], source: bool) -> Result<(), RefspecError> {
if source && bytes == b"HEAD" {
return Ok(());
}
if !bytes.starts_with(b"refs/") {
return Err(RefspecError::Unsupported(
"requires full refs/ name; only exact source HEAD is allowed",
));
}
let probe: Vec<_> = bytes
.iter()
.map(|b| if *b == b'*' { b'x' } else { *b })
.collect();
RefName::new(probe).map_err(|_| RefspecError::Name)?;
Ok(())
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct RefSource {
pub name: RefName,
pub id: ObjectId,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub struct Mapping {
pub source: Option<RefSource>,
pub destination: Option<RefName>,
pub force: bool,
}
impl Mapping {
pub fn new(source: Option<RefSource>, destination: Option<RefName>, force: bool) -> Self {
Self {
source,
destination,
force,
}
}
#[must_use]
pub fn with_source(mut self, source: Option<RefSource>) -> Self {
self.source = source;
self
}
#[must_use]
pub fn with_destination(mut self, destination: Option<RefName>) -> Self {
self.destination = destination;
self
}
#[must_use]
pub fn with_force(mut self, force: bool) -> Self {
self.force = force;
self
}
}
#[derive(Debug, Clone)]
pub struct Refspecs {
direction: Direction,
specs: Vec<Refspec>,
}
#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum MappingError {
#[error("explicit refspec source not found: {0:?}")]
UnmatchedSource(Vec<u8>),
#[error("duplicate source name: {0:?}")]
DuplicateSource(RefName),
#[error("source has a zero object ID: {0:?}")]
ZeroId(RefName),
#[error("conflicting mappings for destination: {0:?}")]
Collision(RefName),
#[error("wildcard substitution produced an invalid destination")]
InvalidDestination,
#[error("advertisement selection requires fetch refspecs")]
Direction,
}
impl Refspecs {
pub fn parse<I, B>(direction: Direction, values: I) -> Result<Self, RefspecsError>
where
I: IntoIterator<Item = B>,
B: AsRef<[u8]>,
{
let specs = values
.into_iter()
.enumerate()
.map(|(index, bytes)| {
Refspec::parse(direction, bytes.as_ref())
.map_err(|source| RefspecsError { index, source })
})
.collect::<Result<_, _>>()?;
Ok(Self { direction, specs })
}
pub fn direction(&self) -> Direction {
self.direction
}
pub fn specs(&self) -> &[Refspec] {
&self.specs
}
pub(crate) fn prune_sources(&self, destination: &RefName) -> Vec<Vec<u8>> {
self.specs
.iter()
.filter_map(|spec| spec.source_for_destination(destination))
.filter(|source| {
!self
.specs
.iter()
.any(|spec| spec.negative && spec.capture(source).is_some())
})
.collect()
}
pub fn map(&self, sources: &[RefSource]) -> Result<Vec<Mapping>, MappingError> {
self.map_with_missing(sources).map(|(mappings, missing)| {
if let Some(source) = missing.into_iter().next() {
Err(MappingError::UnmatchedSource(source))
} else {
Ok(mappings)
}
})?
}
pub fn map_with_missing(
&self,
sources: &[RefSource],
) -> Result<(Vec<Mapping>, Vec<Vec<u8>>), MappingError> {
let mut names = HashSet::new();
for source in sources {
if !names.insert(&source.name) {
return Err(MappingError::DuplicateSource(source.name.clone()));
}
if source.id.is_null() {
return Err(MappingError::ZeroId(source.name.clone()));
}
}
let mut plan = Plan::default();
let mut missing = Vec::new();
for spec in self.specs.iter().filter(|spec| !spec.negative) {
if spec.source.is_empty() {
plan.add(Mapping {
source: None,
destination: spec.destination(b"")?,
force: spec.force,
})?;
continue;
}
let mut matched = false;
for source in sources {
let Some(capture) = spec.capture(source.name.as_bytes()) else {
continue;
};
matched = true;
let excluded = self.specs.iter().any(|negative| {
negative.negative && negative.capture(source.name.as_bytes()).is_some()
});
if !excluded {
plan.add(Mapping {
source: Some(source.clone()),
destination: spec.destination(capture)?,
force: spec.force,
})?;
}
}
if !matched && !spec.wildcard {
missing.push(spec.source.clone());
}
}
Ok((plan.mappings, missing))
}
pub fn map_advertisement(
&self,
advertisement: &Advertisement,
) -> Result<Vec<Mapping>, MappingError> {
if self.direction != Direction::Fetch {
return Err(MappingError::Direction);
}
let sources: Vec<_> = advertisement
.refs
.iter()
.filter(|reference| !reference.peeled)
.map(|reference| RefSource {
name: reference.name.clone(),
id: reference.id,
})
.collect();
self.map(&sources)
}
pub fn map_advertisement_with_missing(
&self,
advertisement: &Advertisement,
) -> Result<(Vec<Mapping>, Vec<Vec<u8>>), MappingError> {
if self.direction != Direction::Fetch {
return Err(MappingError::Direction);
}
let sources: Vec<_> = advertisement
.refs
.iter()
.filter(|reference| !reference.peeled)
.map(|reference| RefSource {
name: reference.name.clone(),
id: reference.id,
})
.collect();
self.map_with_missing(&sources)
}
}
#[derive(Default)]
struct Plan {
mappings: Vec<Mapping>,
seen: HashSet<Mapping>,
destinations: HashSet<RefName>,
}
impl Plan {
fn add(&mut self, mapping: Mapping) -> Result<(), MappingError> {
if self.seen.contains(&mapping) {
return Ok(());
}
if let Some(destination) = &mapping.destination {
if self.destinations.contains(destination) {
return Err(MappingError::Collision(destination.clone()));
}
self.destinations.insert(destination.clone());
}
self.seen.insert(mapping.clone());
self.mappings.push(mapping);
Ok(())
}
}
#[cfg(test)]
#[path = "refspec_tests.rs"]
mod tests;