Skip to main content

grit_lib/
protocol_v2.rs

1//! Pure protocol-v2 capability parsing and request-fragment building.
2//!
3//! These helpers operate purely on the textual capability/feature lines exchanged during a
4//! Git wire-protocol-v2 conversation (the lines between `version 2` and the first flush, plus
5//! the space-separated feature list inside a `fetch=` capability). They have no I/O, no
6//! environment access, and no transport-backend coupling, so they are shared by every v2 client
7//! transport (file://, git://, ssh, smart-HTTP) rather than duplicated per backend.
8//!
9//! See `Documentation/gitprotocol-v2.txt` and `serve.c` / `connect.c` / `fetch-pack.c` in Git.
10
11use std::collections::HashSet;
12
13/// True when the server's v2 capability advertisement offers the `bundle-uri` command.
14///
15/// Advertised either bare (`bundle-uri`) or with a value (`bundle-uri=...`).
16#[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/// Build the capability lines a client echoes back when issuing a follow-up v2 command
23/// (e.g. `command=bundle-uri` or `command=fetch`).
24///
25/// Mirrors the client capability handling in `connect.c`: the `agent=` line is forwarded
26/// verbatim and `object-format=<hash>` is re-emitted. Other advertised capabilities are not
27/// echoed in the per-command capability list.
28#[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/// Collect the space-separated feature tokens advertised by the server's v2 `fetch=` capability.
42///
43/// Returns an empty set when the server advertises no `fetch=` capability.
44#[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/// True when the server's v2 `fetch=` capability lists `<feature>`.
58#[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/// True when the server's `fetch=` capability advertises `sideband-all`.
67#[must_use]
68pub fn fetch_supports_sideband_all(caps: &[String]) -> bool {
69    fetch_supports_feature(caps, "sideband-all")
70}
71
72/// True when the server's v2 `fetch=` capability advertises `ref-in-want` (so the client may send
73/// `want-ref <name>` lines instead of resolving named refspecs to OIDs itself).
74#[must_use]
75pub fn fetch_supports_ref_in_want(caps: &[String]) -> bool {
76    fetch_supports_feature(caps, "ref-in-want")
77}
78
79/// True when the server's v2 `fetch=` capability advertises `filter` (so the client may send a
80/// `filter <spec>` line).
81///
82/// Mirrors `fetch-pack.c` `send_filter`, which only writes the `filter` request line when the
83/// server advertised filtering support. A promisor remote without `uploadpack.allowFilter` does
84/// not advertise it, so the client must silently drop the filter and fetch unfiltered rather than
85/// send a line the server rejects with "unexpected line".
86#[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}