gix_protocol/handshake/refs/
shared.rs1use crate::fetch::response::ShallowUpdate;
2use crate::handshake::{refs::parse::Error, Ref};
3use bstr::{BStr, BString, ByteSlice};
4
5impl From<InternalRef> for Ref {
6 fn from(v: InternalRef) -> Self {
7 match v {
8 InternalRef::Symbolic {
9 path,
10 target: Some(target),
11 tag,
12 object,
13 } => Ref::Symbolic {
14 full_ref_name: path,
15 target,
16 tag,
17 object,
18 },
19 InternalRef::Symbolic {
20 path,
21 target: None,
22 tag: None,
23 object,
24 } => Ref::Direct {
25 full_ref_name: path,
26 object,
27 },
28 InternalRef::Symbolic {
29 path,
30 target: None,
31 tag: Some(tag),
32 object,
33 } => Ref::Peeled {
34 full_ref_name: path,
35 tag,
36 object,
37 },
38 InternalRef::Peeled { path, tag, object } => Ref::Peeled {
39 full_ref_name: path,
40 tag,
41 object,
42 },
43 InternalRef::Direct { path, object } => Ref::Direct {
44 full_ref_name: path,
45 object,
46 },
47 InternalRef::SymbolicForLookup { .. } => {
48 unreachable!("this case should have been removed during processing")
49 }
50 }
51 }
52}
53
54#[cfg_attr(test, derive(PartialEq, Eq, Debug, Clone))]
55pub(crate) enum InternalRef {
56 Peeled {
58 path: BString,
59 tag: gix_hash::ObjectId,
60 object: gix_hash::ObjectId,
61 },
62 Direct { path: BString, object: gix_hash::ObjectId },
64 Symbolic {
66 path: BString,
67 target: Option<BString>,
72 tag: Option<gix_hash::ObjectId>,
73 object: gix_hash::ObjectId,
74 },
75 SymbolicForLookup { path: BString, target: Option<BString> },
78}
79
80impl InternalRef {
81 fn unpack_direct(self) -> Option<(BString, gix_hash::ObjectId)> {
82 match self {
83 InternalRef::Direct { path, object } => Some((path, object)),
84 _ => None,
85 }
86 }
87 fn lookup_symbol_has_path(&self, predicate_path: &BStr) -> bool {
88 matches!(self, InternalRef::SymbolicForLookup { path, .. } if path == predicate_path)
89 }
90}
91
92pub(crate) fn from_capabilities<'a>(
93 capabilities: impl Iterator<Item = gix_transport::client::capabilities::Capability<'a>>,
94) -> Result<Vec<InternalRef>, Error> {
95 let mut out_refs = Vec::new();
96 let symref_values = capabilities.filter_map(|c| {
97 if c.name() == b"symref".as_bstr() {
98 c.value().map(ToOwned::to_owned)
99 } else {
100 None
101 }
102 });
103 for symref in symref_values {
104 let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| Error::MalformedSymref {
105 symref: symref.to_owned(),
106 })?);
107 if left.is_empty() || right.is_empty() {
108 return Err(Error::MalformedSymref {
109 symref: symref.to_owned(),
110 });
111 }
112 out_refs.push(InternalRef::SymbolicForLookup {
113 path: left.into(),
114 target: match &right[1..] {
115 b"(null)" => None,
116 name => Some(name.into()),
117 },
118 });
119 }
120 Ok(out_refs)
121}
122
123pub(in crate::handshake::refs) fn parse_v1(
124 num_initial_out_refs: usize,
125 out_refs: &mut Vec<InternalRef>,
126 out_shallow: &mut Vec<ShallowUpdate>,
127 line: &BStr,
128) -> Result<(), Error> {
129 let trimmed = line.trim_end();
130 let (hex_hash, path) = trimmed.split_at(
131 trimmed
132 .find(b" ")
133 .ok_or_else(|| Error::MalformedV1RefLine(trimmed.to_owned().into()))?,
134 );
135 let path = &path[1..];
136 if path.is_empty() {
137 return Err(Error::MalformedV1RefLine(trimmed.to_owned().into()));
138 }
139 match path.strip_suffix(b"^{}") {
140 Some(stripped) => {
141 if hex_hash.iter().all(|b| *b == b'0') && stripped == b"capabilities" {
142 return Ok(());
144 }
145 let (previous_path, tag) =
146 out_refs
147 .pop()
148 .and_then(InternalRef::unpack_direct)
149 .ok_or(Error::InvariantViolation {
150 message: "Expecting peeled refs to be preceded by direct refs",
151 })?;
152 if previous_path != stripped {
153 return Err(Error::InvariantViolation {
154 message: "Expecting peeled refs to have the same base path as the previous, unpeeled one",
155 });
156 }
157 out_refs.push(InternalRef::Peeled {
158 path: previous_path,
159 tag,
160 object: gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?,
161 });
162 }
163 None => {
164 let object = match gix_hash::ObjectId::from_hex(hex_hash.as_bytes()) {
165 Ok(id) => id,
166 Err(_) if hex_hash.as_bstr() == "shallow" => {
167 let id = gix_hash::ObjectId::from_hex(path)?;
168 out_shallow.push(ShallowUpdate::Shallow(id));
169 return Ok(());
170 }
171 Err(err) => return Err(err.into()),
172 };
173 match out_refs
174 .iter()
175 .take(num_initial_out_refs)
176 .position(|r| r.lookup_symbol_has_path(path.into()))
177 {
178 Some(position) => match out_refs.swap_remove(position) {
179 InternalRef::SymbolicForLookup { path: _, target } => out_refs.push(InternalRef::Symbolic {
180 path: path.into(),
181 tag: None, object,
183 target,
184 }),
185 _ => unreachable!("Bug in lookup_symbol_has_path - must return lookup symbols"),
186 },
187 None => out_refs.push(InternalRef::Direct {
188 object,
189 path: path.into(),
190 }),
191 };
192 }
193 }
194 Ok(())
195}
196
197pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result<Ref, Error> {
198 let trimmed = line.trim_end();
199 let mut tokens = trimmed.splitn(4, |b| *b == b' ');
200 match (tokens.next(), tokens.next()) {
201 (Some(hex_hash), Some(path)) => {
202 let id = if hex_hash == b"unborn" {
203 None
204 } else {
205 Some(gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?)
206 };
207 if path.is_empty() {
208 return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
209 }
210 let mut symref_target = None;
211 let mut peeled = None;
212 for attribute in tokens.by_ref().take(2) {
213 let mut tokens = attribute.splitn(2, |b| *b == b':');
214 match (tokens.next(), tokens.next()) {
215 (Some(attribute), Some(value)) => {
216 if value.is_empty() {
217 return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
218 }
219 match attribute {
220 b"peeled" => {
221 peeled = Some(gix_hash::ObjectId::from_hex(value.as_bytes())?);
222 }
223 b"symref-target" => {
224 symref_target = Some(value);
225 }
226 _ => {
227 return Err(Error::UnknownAttribute {
228 attribute: attribute.to_owned().into(),
229 line: trimmed.to_owned().into(),
230 })
231 }
232 }
233 }
234 _ => return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())),
235 }
236 }
237 if tokens.next().is_some() {
238 return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
239 }
240 Ok(match (symref_target, peeled) {
241 (Some(target_name), peeled) => match target_name {
242 b"(null)" => match peeled {
243 None => Ref::Direct {
244 full_ref_name: path.into(),
245 object: id.ok_or(Error::InvariantViolation {
246 message: "got 'unborn' while (null) was a symref target",
247 })?,
248 },
249 Some(peeled) => Ref::Peeled {
250 full_ref_name: path.into(),
251 object: peeled,
252 tag: id.ok_or(Error::InvariantViolation {
253 message: "got 'unborn' while (null) was a symref target",
254 })?,
255 },
256 },
257 name => match id {
258 Some(id) => Ref::Symbolic {
259 full_ref_name: path.into(),
260 tag: peeled.map(|_| id),
261 object: peeled.unwrap_or(id),
262 target: name.into(),
263 },
264 None => Ref::Unborn {
265 full_ref_name: path.into(),
266 target: name.into(),
267 },
268 },
269 },
270 (None, Some(peeled)) => Ref::Peeled {
271 full_ref_name: path.into(),
272 object: peeled,
273 tag: id.ok_or(Error::InvariantViolation {
274 message: "got 'unborn' as tag target",
275 })?,
276 },
277 (None, None) => Ref::Direct {
278 object: id.ok_or(Error::InvariantViolation {
279 message: "got 'unborn' as object name of direct reference",
280 })?,
281 full_ref_name: path.into(),
282 },
283 })
284 }
285 _ => Err(Error::MalformedV2RefLine(trimmed.to_owned().into())),
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use gix_transport::client;
292
293 use crate::handshake::{refs, refs::shared::InternalRef};
294
295 #[test]
296 fn extract_symbolic_references_from_capabilities() -> Result<(), client::Error> {
297 let caps = client::Capabilities::from_bytes(
298 b"\0unrelated symref=HEAD:refs/heads/main symref=ANOTHER:refs/heads/foo symref=MISSING_NAMESPACE_TARGET:(null) agent=git/2.28.0",
299 )?
300 .0;
301 let out = refs::shared::from_capabilities(caps.iter()).expect("a working example");
302
303 assert_eq!(
304 out,
305 vec![
306 InternalRef::SymbolicForLookup {
307 path: "HEAD".into(),
308 target: Some("refs/heads/main".into())
309 },
310 InternalRef::SymbolicForLookup {
311 path: "ANOTHER".into(),
312 target: Some("refs/heads/foo".into())
313 },
314 InternalRef::SymbolicForLookup {
315 path: "MISSING_NAMESPACE_TARGET".into(),
316 target: None
317 }
318 ]
319 );
320 Ok(())
321 }
322}