1use std::collections::HashSet;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum FetchResponseSection {
20 Acknowledgments,
22 ShallowInfo,
24 WantedRefs,
26 PackfileUris,
28 Packfile,
30}
31
32impl FetchResponseSection {
33 #[must_use]
35 pub const fn header(self) -> &'static str {
36 match self {
37 Self::Acknowledgments => "acknowledgments",
38 Self::ShallowInfo => "shallow-info",
39 Self::WantedRefs => "wanted-refs",
40 Self::PackfileUris => "packfile-uris",
41 Self::Packfile => "packfile",
42 }
43 }
44}
45
46#[must_use]
51pub fn fetch_prelude_shallow_before_wanted_refs(section_headers: &[&str]) -> bool {
52 let shallow = section_headers
53 .iter()
54 .position(|h| *h == FetchResponseSection::ShallowInfo.header());
55 let wanted = section_headers
56 .iter()
57 .position(|h| *h == FetchResponseSection::WantedRefs.header());
58 match (shallow, wanted) {
59 (Some(s), Some(w)) => s < w,
60 _ => true,
61 }
62}
63
64pub const FETCH_RESPONSE_SECTION_ORDER: &[FetchResponseSection] = &[
66 FetchResponseSection::Acknowledgments,
67 FetchResponseSection::ShallowInfo,
68 FetchResponseSection::WantedRefs,
69 FetchResponseSection::PackfileUris,
70 FetchResponseSection::Packfile,
71];
72
73#[must_use]
77pub fn server_advertises_bundle_uri(caps: &[String]) -> bool {
78 caps.iter()
79 .any(|c| c == "bundle-uri" || c.starts_with("bundle-uri="))
80}
81
82#[must_use]
87pub(crate) fn server_advertises_ls_refs_unborn(caps: &[String]) -> bool {
88 caps.iter().any(|c| {
89 c == "ls-refs=unborn"
90 || c.strip_prefix("ls-refs=")
91 .is_some_and(|rest| rest.split_whitespace().any(|tok| tok == "unborn"))
92 })
93}
94
95#[must_use]
102pub fn cap_lines_for_command_request(caps: &[String]) -> Vec<String> {
103 let mut out = Vec::new();
104 for line in caps {
105 if line.starts_with("agent=") {
106 out.push(line.clone());
107 } else if let Some(fmt) = line.strip_prefix("object-format=") {
108 out.push(format!("object-format={fmt}"));
109 }
110 }
111 out
112}
113
114#[must_use]
118pub fn fetch_features(caps: &[String]) -> HashSet<String> {
119 let mut features = HashSet::new();
120 for line in caps {
121 if let Some(rest) = line.strip_prefix("fetch=") {
122 for feature in rest.split_whitespace() {
123 features.insert(feature.to_string());
124 }
125 }
126 }
127 features
128}
129
130#[must_use]
132pub fn fetch_supports_feature(caps: &[String], feature: &str) -> bool {
133 caps.iter().any(|c| {
134 c.strip_prefix("fetch=")
135 .is_some_and(|rest| rest.split_whitespace().any(|w| w == feature))
136 })
137}
138
139#[must_use]
141pub fn fetch_supports_sideband_all(caps: &[String]) -> bool {
142 fetch_supports_feature(caps, "sideband-all")
143}
144
145#[must_use]
148pub fn fetch_supports_ref_in_want(caps: &[String]) -> bool {
149 fetch_supports_feature(caps, "ref-in-want")
150}
151
152#[must_use]
160pub fn fetch_supports_filter(caps: &[String]) -> bool {
161 fetch_supports_feature(caps, "filter")
162}
163
164pub const PROMISOR_REMOTE_CAPABILITY: &str = "promisor-remote";
168
169#[must_use]
179pub fn server_promisor_remote_info(caps: &[String]) -> Option<&str> {
180 caps.iter().find_map(|c| {
181 c.strip_prefix(PROMISOR_REMOTE_CAPABILITY)
182 .and_then(|rest| rest.strip_prefix('='))
183 })
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn s(v: &[&str]) -> Vec<String> {
191 v.iter().map(|x| (*x).to_owned()).collect()
192 }
193
194 #[test]
195 fn fetch_prelude_order_helper() {
196 assert!(fetch_prelude_shallow_before_wanted_refs(&[
197 "shallow-info",
198 "wanted-refs",
199 "packfile"
200 ]));
201 assert!(!fetch_prelude_shallow_before_wanted_refs(&[
202 "wanted-refs",
203 "shallow-info"
204 ]));
205 }
206
207 #[test]
208 fn fetch_response_section_order_matches_protocol_v2() {
209 assert_eq!(
210 FETCH_RESPONSE_SECTION_ORDER
211 .iter()
212 .map(|s| s.header())
213 .collect::<Vec<_>>(),
214 [
215 "acknowledgments",
216 "shallow-info",
217 "wanted-refs",
218 "packfile-uris",
219 "packfile"
220 ]
221 );
222 }
223
224 #[test]
225 fn ls_refs_unborn_capability() {
226 assert!(server_advertises_ls_refs_unborn(&s(&["ls-refs=unborn"])));
227 assert!(!server_advertises_ls_refs_unborn(&s(&["ls-refs"])));
228 }
229
230 #[test]
231 fn bundle_uri_bare_and_valued() {
232 assert!(server_advertises_bundle_uri(&s(&[
233 "agent=git/2",
234 "bundle-uri"
235 ])));
236 assert!(server_advertises_bundle_uri(&s(&["bundle-uri=foo"])));
237 assert!(!server_advertises_bundle_uri(&s(&[
238 "agent=git/2",
239 "ls-refs"
240 ])));
241 assert!(!server_advertises_bundle_uri(&s(&[])));
242 }
243
244 #[test]
245 fn cap_lines_forwards_agent_and_object_format() {
246 let caps = s(&[
247 "version 2",
248 "agent=git/2.43",
249 "ls-refs=unborn",
250 "object-format=sha256",
251 "fetch=shallow filter",
252 ]);
253 assert_eq!(
254 cap_lines_for_command_request(&caps),
255 s(&["agent=git/2.43", "object-format=sha256"])
256 );
257 }
258
259 #[test]
260 fn cap_lines_empty_when_no_agent_or_format() {
261 assert_eq!(
262 cap_lines_for_command_request(&s(&["version 2", "ls-refs"])),
263 Vec::<String>::new()
264 );
265 }
266
267 #[test]
268 fn fetch_features_splits_on_whitespace() {
269 let caps = s(&["fetch=shallow filter ref-in-want sideband-all"]);
270 let f = fetch_features(&caps);
271 assert!(f.contains("shallow"));
272 assert!(f.contains("filter"));
273 assert!(f.contains("ref-in-want"));
274 assert!(f.contains("sideband-all"));
275 assert_eq!(f.len(), 4);
276 }
277
278 #[test]
279 fn fetch_features_empty_without_fetch_cap() {
280 assert!(fetch_features(&s(&["ls-refs", "agent=x"])).is_empty());
281 }
282
283 #[test]
284 fn promisor_remote_info_value() {
285 let caps = s(&["agent=x", "promisor-remote=name=lop,url=file:///lop"]);
286 assert_eq!(
287 server_promisor_remote_info(&caps),
288 Some("name=lop,url=file:///lop")
289 );
290 assert_eq!(server_promisor_remote_info(&s(&["promisor-remote"])), None);
291 assert_eq!(server_promisor_remote_info(&s(&["agent=x"])), None);
292 }
293
294 #[test]
295 fn per_feature_helpers() {
296 let caps = s(&["fetch=ref-in-want filter sideband-all"]);
297 assert!(fetch_supports_sideband_all(&caps));
298 assert!(fetch_supports_ref_in_want(&caps));
299 assert!(fetch_supports_filter(&caps));
300 assert!(fetch_supports_feature(&caps, "ref-in-want"));
301 assert!(!fetch_supports_feature(&caps, "shallow"));
302
303 let none = s(&["fetch=shallow", "ls-refs"]);
304 assert!(!fetch_supports_sideband_all(&none));
305 assert!(!fetch_supports_ref_in_want(&none));
306 assert!(!fetch_supports_filter(&none));
307 }
308}