gix_protocol/handshake/refs/
shared.rs

1use 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    /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit
57    Peeled {
58        path: BString,
59        tag: gix_hash::ObjectId,
60        object: gix_hash::ObjectId,
61    },
62    /// A ref pointing to a commit object
63    Direct { path: BString, object: gix_hash::ObjectId },
64    /// A symbolic ref pointing to `target` ref, which in turn points to an `object`
65    Symbolic {
66        path: BString,
67        /// It is `None` if the target is unreachable as it points to another namespace than the one is currently set
68        /// on the server (i.e. based on the repository at hand or the user performing the operation).
69        ///
70        /// The latter is more of an edge case, please [this issue][#205] for details.
71        target: Option<BString>,
72        tag: Option<gix_hash::ObjectId>,
73        object: gix_hash::ObjectId,
74    },
75    /// extracted from V1 capabilities, which contain some important symbolic refs along with their targets
76    /// These don't contain the Id
77    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                // this is a special dummy-ref just for the sake of getting capabilities across in a repo that is empty.
143                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, // TODO: figure out how annotated tags work here.
182                        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}