blob: 45aefe68b23cc0f7f7c9e83d7af2229ce5524521 [file] [log] [blame]
use bstr::{BStr, BString, ByteSlice};
use crate::handshake::{refs::parse::Error, Ref};
impl From<InternalRef> for Ref {
fn from(v: InternalRef) -> Self {
match v {
InternalRef::Symbolic {
path,
target: Some(target),
tag,
object,
} => Ref::Symbolic {
full_ref_name: path,
target,
tag,
object,
},
InternalRef::Symbolic {
path,
target: None,
tag: None,
object,
} => Ref::Direct {
full_ref_name: path,
object,
},
InternalRef::Symbolic {
path,
target: None,
tag: Some(tag),
object,
} => Ref::Peeled {
full_ref_name: path,
tag,
object,
},
InternalRef::Peeled { path, tag, object } => Ref::Peeled {
full_ref_name: path,
tag,
object,
},
InternalRef::Direct { path, object } => Ref::Direct {
full_ref_name: path,
object,
},
InternalRef::SymbolicForLookup { .. } => {
unreachable!("this case should have been removed during processing")
}
}
}
}
#[cfg_attr(test, derive(PartialEq, Eq, Debug, Clone))]
pub(crate) enum InternalRef {
/// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit
Peeled {
path: BString,
tag: gix_hash::ObjectId,
object: gix_hash::ObjectId,
},
/// A ref pointing to a commit object
Direct { path: BString, object: gix_hash::ObjectId },
/// A symbolic ref pointing to `target` ref, which in turn points to an `object`
Symbolic {
path: BString,
/// It is `None` if the target is unreachable as it points to another namespace than the one is currently set
/// on the server (i.e. based on the repository at hand or the user performing the operation).
///
/// The latter is more of an edge case, please [this issue][#205] for details.
target: Option<BString>,
tag: Option<gix_hash::ObjectId>,
object: gix_hash::ObjectId,
},
/// extracted from V1 capabilities, which contain some important symbolic refs along with their targets
/// These don't contain the Id
SymbolicForLookup { path: BString, target: Option<BString> },
}
impl InternalRef {
fn unpack_direct(self) -> Option<(BString, gix_hash::ObjectId)> {
match self {
InternalRef::Direct { path, object } => Some((path, object)),
_ => None,
}
}
fn lookup_symbol_has_path(&self, predicate_path: &BStr) -> bool {
matches!(self, InternalRef::SymbolicForLookup { path, .. } if path == predicate_path)
}
}
pub(crate) fn from_capabilities<'a>(
capabilities: impl Iterator<Item = gix_transport::client::capabilities::Capability<'a>>,
) -> Result<Vec<InternalRef>, Error> {
let mut out_refs = Vec::new();
let symref_values = capabilities.filter_map(|c| {
if c.name() == b"symref".as_bstr() {
c.value().map(ToOwned::to_owned)
} else {
None
}
});
for symref in symref_values {
let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| Error::MalformedSymref {
symref: symref.to_owned(),
})?);
if left.is_empty() || right.is_empty() {
return Err(Error::MalformedSymref {
symref: symref.to_owned(),
});
}
out_refs.push(InternalRef::SymbolicForLookup {
path: left.into(),
target: match &right[1..] {
b"(null)" => None,
name => Some(name.into()),
},
})
}
Ok(out_refs)
}
pub(in crate::handshake::refs) fn parse_v1(
num_initial_out_refs: usize,
out_refs: &mut Vec<InternalRef>,
line: &BStr,
) -> Result<(), Error> {
let trimmed = line.trim_end();
let (hex_hash, path) = trimmed.split_at(
trimmed
.find(b" ")
.ok_or_else(|| Error::MalformedV1RefLine(trimmed.to_owned().into()))?,
);
let path = &path[1..];
if path.is_empty() {
return Err(Error::MalformedV1RefLine(trimmed.to_owned().into()));
}
match path.strip_suffix(b"^{}") {
Some(stripped) => {
if hex_hash.iter().all(|b| *b == b'0') && stripped == b"capabilities" {
// this is a special dummy-ref just for the sake of getting capabilities across in a repo that is empty.
return Ok(());
}
let (previous_path, tag) =
out_refs
.pop()
.and_then(InternalRef::unpack_direct)
.ok_or(Error::InvariantViolation {
message: "Expecting peeled refs to be preceded by direct refs",
})?;
if previous_path != stripped {
return Err(Error::InvariantViolation {
message: "Expecting peeled refs to have the same base path as the previous, unpeeled one",
});
}
out_refs.push(InternalRef::Peeled {
path: previous_path,
tag,
object: gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?,
});
}
None => {
let object = gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?;
match out_refs
.iter()
.take(num_initial_out_refs)
.position(|r| r.lookup_symbol_has_path(path.into()))
{
Some(position) => match out_refs.swap_remove(position) {
InternalRef::SymbolicForLookup { path: _, target } => out_refs.push(InternalRef::Symbolic {
path: path.into(),
tag: None, // TODO: figure out how annotated tags work here.
object,
target,
}),
_ => unreachable!("Bug in lookup_symbol_has_path - must return lookup symbols"),
},
None => out_refs.push(InternalRef::Direct {
object,
path: path.into(),
}),
};
}
}
Ok(())
}
pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result<Ref, Error> {
let trimmed = line.trim_end();
let mut tokens = trimmed.splitn(4, |b| *b == b' ');
match (tokens.next(), tokens.next()) {
(Some(hex_hash), Some(path)) => {
let id = if hex_hash == b"unborn" {
None
} else {
Some(gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?)
};
if path.is_empty() {
return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
}
let mut symref_target = None;
let mut peeled = None;
for attribute in tokens.by_ref().take(2) {
let mut tokens = attribute.splitn(2, |b| *b == b':');
match (tokens.next(), tokens.next()) {
(Some(attribute), Some(value)) => {
if value.is_empty() {
return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
}
match attribute {
b"peeled" => {
peeled = Some(gix_hash::ObjectId::from_hex(value.as_bytes())?);
}
b"symref-target" => {
symref_target = Some(value);
}
_ => {
return Err(Error::UnknownAttribute {
attribute: attribute.to_owned().into(),
line: trimmed.to_owned().into(),
})
}
}
}
_ => return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())),
}
}
if tokens.next().is_some() {
return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
}
Ok(match (symref_target, peeled) {
(Some(target_name), peeled) => match target_name {
b"(null)" => match peeled {
None => Ref::Direct {
full_ref_name: path.into(),
object: id.ok_or(Error::InvariantViolation {
message: "got 'unborn' while (null) was a symref target",
})?,
},
Some(peeled) => Ref::Peeled {
full_ref_name: path.into(),
object: peeled,
tag: id.ok_or(Error::InvariantViolation {
message: "got 'unborn' while (null) was a symref target",
})?,
},
},
name => match id {
Some(id) => Ref::Symbolic {
full_ref_name: path.into(),
tag: peeled.map(|_| id),
object: peeled.unwrap_or(id),
target: name.into(),
},
None => Ref::Unborn {
full_ref_name: path.into(),
target: name.into(),
},
},
},
(None, Some(peeled)) => Ref::Peeled {
full_ref_name: path.into(),
object: peeled,
tag: id.ok_or(Error::InvariantViolation {
message: "got 'unborn' as tag target",
})?,
},
(None, None) => Ref::Direct {
object: id.ok_or(Error::InvariantViolation {
message: "got 'unborn' as object name of direct reference",
})?,
full_ref_name: path.into(),
},
})
}
_ => Err(Error::MalformedV2RefLine(trimmed.to_owned().into())),
}
}