use std::io; use bstr::BString; use quick_error::quick_error; quick_error! { /// The error returned when parsing References/refs from the server response. #[derive(Debug)] #[allow(missing_docs)] pub enum Error { Io(err: io::Error) { display("An IO error occurred while reading refs from the server") from() source(err) } Id(err: git_hash::decode::Error) { display("Failed to hex-decode object hash") from() source(err) } MalformedSymref(symref: BString) { display("'{}' could not be parsed. A symref is expected to look like :.", symref) } MalformedV1RefLine(line: String) { display("'{}' could not be parsed. A V1 ref line should be ' '.", line) } MalformedV2RefLine(line: String) { display("'{}' could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'.", line) } UnkownAttribute(attribute: String, line: String) { display("The ref attribute '{}' is unknown. Found in line '{}'", attribute, line) } InvariantViolation(message: &'static str) { display("{}", message) } } } /// A git reference, commonly referred to as 'ref', as returned by a git server before sending a pack. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub enum Ref { /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit Peeled { /// The path at which the ref is located, like `/refs/heads/main`. path: BString, /// The hash of the tag the ref points to. tag: git_hash::ObjectId, /// The hash of the object the `tag` points to. object: git_hash::ObjectId, }, /// A ref pointing to a commit object Direct { /// The path at which the ref is located, like `/refs/heads/main`. path: BString, /// The hash of the object the ref points to. object: git_hash::ObjectId, }, /// A symbolic ref pointing to `target` ref, which in turn points to an `object` Symbolic { /// The path at which the symbolic ref is located, like `/refs/heads/main`. path: BString, /// The path of the ref the symbolic ref points to, see issue [#205] for details /// /// [#205]: https://github.com/Byron/gitoxide/issues/205 target: BString, /// The hash of the object the `target` ref points to. object: git_hash::ObjectId, }, } impl Ref { /// Provide shared fields referring to the ref itself, namely `(path, object id)`. /// In case of peeled refs, the tag object itself is returned as it is what the path refers to. pub fn unpack(&self) -> (&BString, &git_hash::ObjectId) { match self { Ref::Direct { path, object, .. } | Ref::Peeled { path, tag: object, .. } // the tag acts as reference | Ref::Symbolic { path, object, .. } => (path, object), } } } #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub(crate) mod shared { use bstr::{BString, ByteSlice}; use crate::fetch::{refs, Ref}; impl From for Ref { fn from(v: InternalRef) -> Self { match v { InternalRef::Symbolic { path, target: Some(target), object, } => Ref::Symbolic { path, target, object }, InternalRef::Symbolic { path, target: None, object, } => Ref::Direct { path, object }, InternalRef::Peeled { path, tag, object } => Ref::Peeled { path, tag, object }, InternalRef::Direct { path, object } => Ref::Direct { 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: git_hash::ObjectId, object: git_hash::ObjectId, }, /// A ref pointing to a commit object Direct { path: BString, object: git_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, object: git_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 }, } impl InternalRef { fn unpack_direct(self) -> Option<(BString, git_hash::ObjectId)> { match self { InternalRef::Direct { path, object } => Some((path, object)), _ => None, } } fn lookup_symbol_has_path(&self, predicate_path: &str) -> bool { matches!(self, InternalRef::SymbolicForLookup { path, .. } if path == predicate_path) } } pub(crate) fn from_capabilities<'a>( capabilities: impl Iterator>, ) -> Result, refs::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(|| refs::Error::MalformedSymref(symref.to_owned()))?, ); if left.is_empty() || right.is_empty() { return Err(refs::Error::MalformedSymref(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::fetch::refs) fn parse_v1( num_initial_out_refs: usize, out_refs: &mut Vec, line: &str, ) -> Result<(), refs::Error> { let trimmed = line.trim_end(); let (hex_hash, path) = trimmed.split_at( trimmed .find(' ') .ok_or_else(|| refs::Error::MalformedV1RefLine(trimmed.to_owned()))?, ); let path = &path[1..]; if path.is_empty() { return Err(refs::Error::MalformedV1RefLine(trimmed.to_owned())); } match path.strip_suffix("^{}") { Some(stripped) => { let (previous_path, tag) = out_refs .pop() .and_then(InternalRef::unpack_direct) .ok_or(refs::Error::InvariantViolation( "Expecting peeled refs to be preceded by direct refs", ))?; if previous_path != stripped { return Err(refs::Error::InvariantViolation( "Expecting peeled refs to have the same base path as the previous, unpeeled one", )); } out_refs.push(InternalRef::Peeled { path: previous_path, tag, object: git_hash::ObjectId::from_hex(hex_hash.as_bytes())?, }); } None => { let object = git_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)) { Some(position) => match out_refs.swap_remove(position) { InternalRef::SymbolicForLookup { path: _, target } => out_refs.push(InternalRef::Symbolic { path: path.into(), 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::fetch::refs) fn parse_v2(line: &str) -> Result { let trimmed = line.trim_end(); let mut tokens = trimmed.splitn(3, ' '); match (tokens.next(), tokens.next()) { (Some(hex_hash), Some(path)) => { let id = git_hash::ObjectId::from_hex(hex_hash.as_bytes())?; if path.is_empty() { return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())); } Ok(if let Some(attribute) = tokens.next() { let mut tokens = attribute.splitn(2, ':'); match (tokens.next(), tokens.next()) { (Some(attribute), Some(value)) => { if value.is_empty() { return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())); } match attribute { "peeled" => Ref::Peeled { path: path.into(), object: git_hash::ObjectId::from_hex(value.as_bytes())?, tag: id, }, "symref-target" => match value { "(null)" => Ref::Direct { path: path.into(), object: id, }, name => Ref::Symbolic { path: path.into(), object: id, target: name.into(), }, }, _ => { return Err(refs::Error::UnkownAttribute(attribute.to_owned(), trimmed.to_owned())) } } } _ => return Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())), } } else { Ref::Direct { object: id, path: path.into(), } }) } _ => Err(refs::Error::MalformedV2RefLine(trimmed.to_owned())), } } } #[cfg(feature = "async-client")] mod async_io { use futures_io::AsyncBufRead; use futures_lite::AsyncBufReadExt; use crate::fetch::{refs, Ref}; /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. pub async fn from_v2_refs(in_refs: &mut (dyn AsyncBufRead + Unpin)) -> Result, refs::Error> { let mut out_refs = Vec::new(); let mut line = String::new(); loop { line.clear(); let bytes_read = in_refs.read_line(&mut line).await?; if bytes_read == 0 { break; } out_refs.push(refs::shared::parse_v2(&line)?); } Ok(out_refs) } /// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the /// handshake. /// Together they form a complete set of refs. /// /// # Note /// /// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as /// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here. pub async fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( in_refs: &mut (dyn AsyncBufRead + Unpin), capabilities: impl Iterator>, ) -> Result, refs::Error> { let mut out_refs = refs::shared::from_capabilities(capabilities)?; let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); let mut line = String::new(); loop { line.clear(); let bytes_read = in_refs.read_line(&mut line).await?; if bytes_read == 0 { break; } refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, &line)?; } Ok(out_refs.into_iter().map(Into::into).collect()) } } #[cfg(feature = "async-client")] pub use async_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs}; #[cfg(feature = "blocking-client")] mod blocking_io { use std::io; use crate::fetch::{refs, Ref}; /// Parse refs from the given input line by line. Protocol V2 is required for this to succeed. pub fn from_v2_refs(in_refs: &mut dyn io::BufRead) -> Result, refs::Error> { let mut out_refs = Vec::new(); let mut line = String::new(); loop { line.clear(); let bytes_read = in_refs.read_line(&mut line)?; if bytes_read == 0 { break; } out_refs.push(refs::shared::parse_v2(&line)?); } Ok(out_refs) } /// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the /// handshake. /// Together they form a complete set of refs. /// /// # Note /// /// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as /// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here. pub fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>( in_refs: &mut dyn io::BufRead, capabilities: impl Iterator>, ) -> Result, refs::Error> { let mut out_refs = refs::shared::from_capabilities(capabilities)?; let number_of_possible_symbolic_refs_for_lookup = out_refs.len(); let mut line = String::new(); loop { line.clear(); let bytes_read = in_refs.read_line(&mut line)?; if bytes_read == 0 { break; } refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, &line)?; } Ok(out_refs.into_iter().map(Into::into).collect()) } } #[cfg(feature = "blocking-client")] pub use blocking_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs};