gix_protocol/handshake/refs/
shared.rs

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