1use std::collections::HashSet;
12
13#[must_use]
17pub fn server_advertises_bundle_uri(caps: &[String]) -> bool {
18 caps.iter()
19 .any(|c| c == "bundle-uri" || c.starts_with("bundle-uri="))
20}
21
22#[must_use]
29pub fn cap_lines_for_command_request(caps: &[String]) -> Vec<String> {
30 let mut out = Vec::new();
31 for line in caps {
32 if line.starts_with("agent=") {
33 out.push(line.clone());
34 } else if let Some(fmt) = line.strip_prefix("object-format=") {
35 out.push(format!("object-format={fmt}"));
36 }
37 }
38 out
39}
40
41#[must_use]
45pub fn fetch_features(caps: &[String]) -> HashSet<String> {
46 let mut features = HashSet::new();
47 for line in caps {
48 if let Some(rest) = line.strip_prefix("fetch=") {
49 for feature in rest.split_whitespace() {
50 features.insert(feature.to_string());
51 }
52 }
53 }
54 features
55}
56
57#[must_use]
59pub fn fetch_supports_feature(caps: &[String], feature: &str) -> bool {
60 caps.iter().any(|c| {
61 c.strip_prefix("fetch=")
62 .is_some_and(|rest| rest.split_whitespace().any(|w| w == feature))
63 })
64}
65
66#[must_use]
68pub fn fetch_supports_sideband_all(caps: &[String]) -> bool {
69 fetch_supports_feature(caps, "sideband-all")
70}
71
72#[must_use]
75pub fn fetch_supports_ref_in_want(caps: &[String]) -> bool {
76 fetch_supports_feature(caps, "ref-in-want")
77}
78
79#[must_use]
87pub fn fetch_supports_filter(caps: &[String]) -> bool {
88 fetch_supports_feature(caps, "filter")
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn s(v: &[&str]) -> Vec<String> {
96 v.iter().map(|x| (*x).to_owned()).collect()
97 }
98
99 #[test]
100 fn bundle_uri_bare_and_valued() {
101 assert!(server_advertises_bundle_uri(&s(&[
102 "agent=git/2",
103 "bundle-uri"
104 ])));
105 assert!(server_advertises_bundle_uri(&s(&["bundle-uri=foo"])));
106 assert!(!server_advertises_bundle_uri(&s(&[
107 "agent=git/2",
108 "ls-refs"
109 ])));
110 assert!(!server_advertises_bundle_uri(&s(&[])));
111 }
112
113 #[test]
114 fn cap_lines_forwards_agent_and_object_format() {
115 let caps = s(&[
116 "version 2",
117 "agent=git/2.43",
118 "ls-refs=unborn",
119 "object-format=sha256",
120 "fetch=shallow filter",
121 ]);
122 assert_eq!(
123 cap_lines_for_command_request(&caps),
124 s(&["agent=git/2.43", "object-format=sha256"])
125 );
126 }
127
128 #[test]
129 fn cap_lines_empty_when_no_agent_or_format() {
130 assert_eq!(
131 cap_lines_for_command_request(&s(&["version 2", "ls-refs"])),
132 Vec::<String>::new()
133 );
134 }
135
136 #[test]
137 fn fetch_features_splits_on_whitespace() {
138 let caps = s(&["fetch=shallow filter ref-in-want sideband-all"]);
139 let f = fetch_features(&caps);
140 assert!(f.contains("shallow"));
141 assert!(f.contains("filter"));
142 assert!(f.contains("ref-in-want"));
143 assert!(f.contains("sideband-all"));
144 assert_eq!(f.len(), 4);
145 }
146
147 #[test]
148 fn fetch_features_empty_without_fetch_cap() {
149 assert!(fetch_features(&s(&["ls-refs", "agent=x"])).is_empty());
150 }
151
152 #[test]
153 fn per_feature_helpers() {
154 let caps = s(&["fetch=ref-in-want filter sideband-all"]);
155 assert!(fetch_supports_sideband_all(&caps));
156 assert!(fetch_supports_ref_in_want(&caps));
157 assert!(fetch_supports_filter(&caps));
158 assert!(fetch_supports_feature(&caps, "ref-in-want"));
159 assert!(!fetch_supports_feature(&caps, "shallow"));
160
161 let none = s(&["fetch=shallow", "ls-refs"]);
162 assert!(!fetch_supports_sideband_all(&none));
163 assert!(!fetch_supports_ref_in_want(&none));
164 assert!(!fetch_supports_filter(&none));
165 }
166}