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(&["agent=git/2", "bundle-uri"])));
102 assert!(server_advertises_bundle_uri(&s(&["bundle-uri=foo"])));
103 assert!(!server_advertises_bundle_uri(&s(&["agent=git/2", "ls-refs"])));
104 assert!(!server_advertises_bundle_uri(&s(&[])));
105 }
106
107 #[test]
108 fn cap_lines_forwards_agent_and_object_format() {
109 let caps = s(&[
110 "version 2",
111 "agent=git/2.43",
112 "ls-refs=unborn",
113 "object-format=sha256",
114 "fetch=shallow filter",
115 ]);
116 assert_eq!(
117 cap_lines_for_command_request(&caps),
118 s(&["agent=git/2.43", "object-format=sha256"])
119 );
120 }
121
122 #[test]
123 fn cap_lines_empty_when_no_agent_or_format() {
124 assert_eq!(
125 cap_lines_for_command_request(&s(&["version 2", "ls-refs"])),
126 Vec::<String>::new()
127 );
128 }
129
130 #[test]
131 fn fetch_features_splits_on_whitespace() {
132 let caps = s(&["fetch=shallow filter ref-in-want sideband-all"]);
133 let f = fetch_features(&caps);
134 assert!(f.contains("shallow"));
135 assert!(f.contains("filter"));
136 assert!(f.contains("ref-in-want"));
137 assert!(f.contains("sideband-all"));
138 assert_eq!(f.len(), 4);
139 }
140
141 #[test]
142 fn fetch_features_empty_without_fetch_cap() {
143 assert!(fetch_features(&s(&["ls-refs", "agent=x"])).is_empty());
144 }
145
146 #[test]
147 fn per_feature_helpers() {
148 let caps = s(&["fetch=ref-in-want filter sideband-all"]);
149 assert!(fetch_supports_sideband_all(&caps));
150 assert!(fetch_supports_ref_in_want(&caps));
151 assert!(fetch_supports_filter(&caps));
152 assert!(fetch_supports_feature(&caps, "ref-in-want"));
153 assert!(!fetch_supports_feature(&caps, "shallow"));
154
155 let none = s(&["fetch=shallow", "ls-refs"]);
156 assert!(!fetch_supports_sideband_all(&none));
157 assert!(!fetch_supports_ref_in_want(&none));
158 assert!(!fetch_supports_filter(&none));
159 }
160}