gix_protocol/fetch/arguments/
mod.rs

1use std::fmt;
2
3use bstr::{BStr, BString, ByteSlice, ByteVec};
4
5/// The arguments passed to a server command.
6#[derive(Debug)]
7pub struct Arguments {
8    /// The active features/capabilities of the fetch invocation
9    #[cfg(any(feature = "async-client", feature = "blocking-client"))]
10    features: Vec<crate::command::Feature>,
11
12    args: Vec<BString>,
13    haves: Vec<BString>,
14
15    filter: bool,
16    shallow: bool,
17    deepen_since: bool,
18    deepen_not: bool,
19    deepen_relative: bool,
20    ref_in_want: bool,
21    supports_include_tag: bool,
22
23    features_for_first_want: Option<Vec<String>>,
24    #[cfg(any(feature = "async-client", feature = "blocking-client"))]
25    version: gix_transport::Protocol,
26
27    #[cfg(any(feature = "async-client", feature = "blocking-client"))]
28    trace: bool,
29}
30
31impl Arguments {
32    /// Return true if there is no argument at all.
33    ///
34    /// This can happen if callers assure that they won't add 'wants' if their 'have' is the same, i.e. if the remote has nothing
35    /// new for them.
36    pub fn is_empty(&self) -> bool {
37        self.haves.is_empty() && !self.args.iter().rev().any(|arg| arg.starts_with_str("want "))
38    }
39    /// Return true if ref filters is supported.
40    pub fn can_use_filter(&self) -> bool {
41        self.filter
42    }
43    /// Return true if shallow refs are supported.
44    ///
45    /// This is relevant for partial clones when using `--depth X`.
46    pub fn can_use_shallow(&self) -> bool {
47        self.shallow
48    }
49    /// Return true if the 'deepen' capability is supported.
50    ///
51    /// This is relevant for partial clones when using `--depth X` and retrieving additional history.
52    pub fn can_use_deepen(&self) -> bool {
53        self.shallow
54    }
55    /// Return true if the '`deepen_since`' capability is supported.
56    ///
57    /// This is relevant for partial clones when using `--depth X` and retrieving additional history
58    /// based on a date beyond which all history should be present.
59    pub fn can_use_deepen_since(&self) -> bool {
60        self.deepen_since
61    }
62    /// Return true if the '`deepen_not`' capability is supported.
63    ///
64    /// This is relevant for partial clones when using `--depth X`.
65    pub fn can_use_deepen_not(&self) -> bool {
66        self.deepen_not
67    }
68    /// Return true if the '`deepen_relative`' capability is supported.
69    ///
70    /// This is relevant for partial clones when using `--depth X`.
71    pub fn can_use_deepen_relative(&self) -> bool {
72        self.deepen_relative
73    }
74    /// Return true if the 'ref-in-want' capability is supported.
75    ///
76    /// This can be used to bypass 'ls-refs' entirely in protocol v2.
77    pub fn can_use_ref_in_want(&self) -> bool {
78        self.ref_in_want
79    }
80    /// Return true if the 'include-tag' capability is supported.
81    pub fn can_use_include_tag(&self) -> bool {
82        self.supports_include_tag
83    }
84    /// Return true if we will use a stateless mode of operation, which can be decided in conjunction with `transport_is_stateless`.
85    ///
86    /// * we are always stateless if the transport is stateless, i.e. doesn't support multiple interactions with a single connection.
87    /// * we are always stateless if the protocol version is `2`
88    /// * otherwise we may be stateful.
89    pub fn is_stateless(&self, transport_is_stateless: bool) -> bool {
90        #[cfg(any(feature = "async-client", feature = "blocking-client"))]
91        let res = transport_is_stateless || self.version == gix_transport::Protocol::V2;
92        #[cfg(not(any(feature = "async-client", feature = "blocking-client")))]
93        let res = transport_is_stateless;
94        res
95    }
96
97    /// Add the given `id` pointing to a commit to the 'want' list.
98    ///
99    /// As such it should be included in the server response as it's not present on the client.
100    pub fn want(&mut self, id: impl AsRef<gix_hash::oid>) {
101        match self.features_for_first_want.take() {
102            Some(features) => self.prefixed("want ", format!("{} {}", id.as_ref(), features.join(" "))),
103            None => self.prefixed("want ", id.as_ref()),
104        }
105    }
106    /// Add the given ref to the 'want-ref' list.
107    ///
108    /// The server should respond with a corresponding 'wanted-refs' section if it will include the
109    /// wanted ref in the packfile response.
110    pub fn want_ref(&mut self, ref_path: &BStr) {
111        let mut arg = BString::from("want-ref ");
112        arg.push_str(ref_path);
113        self.args.push(arg);
114    }
115    /// Add the given `id` pointing to a commit to the 'have' list.
116    ///
117    /// As such it should _not_ be included in the server response as it's already present on the client.
118    pub fn have(&mut self, id: impl AsRef<gix_hash::oid>) {
119        self.haves.push(format!("have {}", id.as_ref()).into());
120    }
121    /// Add the given `id` pointing to a commit to the 'shallow' list.
122    pub fn shallow(&mut self, id: impl AsRef<gix_hash::oid>) {
123        debug_assert!(self.shallow, "'shallow' feature required for 'shallow <id>'");
124        if self.shallow {
125            self.prefixed("shallow ", id.as_ref());
126        }
127    }
128    /// Deepen the commit history by `depth` amount of commits.
129    pub fn deepen(&mut self, depth: usize) {
130        debug_assert!(self.shallow, "'shallow' feature required for deepen");
131        if self.shallow {
132            self.prefixed("deepen ", depth);
133        }
134    }
135    /// Deepen the commit history to include all commits from now to (and including) `seconds` as passed since UNIX epoch.
136    pub fn deepen_since(&mut self, seconds: gix_date::SecondsSinceUnixEpoch) {
137        debug_assert!(self.deepen_since, "'deepen-since' feature required");
138        if self.deepen_since {
139            self.prefixed("deepen-since ", seconds);
140        }
141    }
142    /// Deepen the commit history in a relative instead of absolute fashion.
143    pub fn deepen_relative(&mut self) {
144        debug_assert!(self.deepen_relative, "'deepen-relative' feature required");
145        if self.deepen_relative {
146            self.args.push("deepen-relative".into());
147        }
148    }
149    /// Do not include commits reachable by the given `ref_path` when deepening the history.
150    pub fn deepen_not(&mut self, ref_path: &BStr) {
151        debug_assert!(self.deepen_not, "'deepen-not' feature required");
152        if self.deepen_not {
153            let mut line = BString::from("deepen-not ");
154            line.extend_from_slice(ref_path);
155            self.args.push(line);
156        }
157    }
158    /// Set the given filter `spec` when listing references.
159    pub fn filter(&mut self, spec: &str) {
160        debug_assert!(self.filter, "'filter' feature required");
161        if self.filter {
162            self.prefixed("filter ", spec);
163        }
164    }
165    /// Permanently allow the server to include tags that point to commits or objects it would return.
166    ///
167    /// Needs to only be called once.
168    #[cfg(any(feature = "async-client", feature = "blocking-client"))]
169    pub fn use_include_tag(&mut self) {
170        debug_assert!(self.supports_include_tag, "'include-tag' feature required");
171        if self.supports_include_tag {
172            self.add_feature("include-tag");
173        }
174    }
175
176    /// Add the given `feature`, unconditionally.
177    ///
178    /// Note that sending an unknown or unsupported feature may cause the remote to terminate
179    /// the connection. Use this method if you know what you are doing *and* there is no specialized
180    /// method for this, e.g. [`Self::use_include_tag()`].
181    #[cfg(any(feature = "async-client", feature = "blocking-client"))]
182    pub fn add_feature(&mut self, feature: &str) {
183        match self.version {
184            gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => {
185                let features = self
186                    .features_for_first_want
187                    .as_mut()
188                    .expect("call add_feature before first want()");
189                features.push(feature.into());
190            }
191            gix_transport::Protocol::V2 => {
192                self.args.push(feature.into());
193            }
194        }
195    }
196
197    fn prefixed(&mut self, prefix: &str, value: impl fmt::Display) {
198        self.args.push(format!("{prefix}{value}").into());
199    }
200    /// Create a new instance to help setting up arguments to send to the server as part of a `fetch` operation
201    /// for which `features` are the available and configured features to use.
202    /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate.
203    #[cfg(any(feature = "async-client", feature = "blocking-client"))]
204    pub fn new(version: gix_transport::Protocol, features: Vec<crate::command::Feature>, trace: bool) -> Self {
205        use crate::Command;
206        let has = |name: &str| features.iter().any(|f| f.0 == name);
207        let filter = has("filter");
208        let shallow = has("shallow");
209        let ref_in_want = has("ref-in-want");
210        let mut deepen_since = shallow;
211        let mut deepen_not = shallow;
212        let mut deepen_relative = shallow;
213        let supports_include_tag;
214        let (initial_arguments, features_for_first_want) = match version {
215            gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => {
216                deepen_since = has("deepen-since");
217                deepen_not = has("deepen-not");
218                deepen_relative = has("deepen-relative");
219                supports_include_tag = has("include-tag");
220                let baked_features = features
221                    .iter()
222                    .filter(
223                        |(f, _)| *f != "include-tag", /* not a capability in that sense, needs to be turned on by caller later */
224                    )
225                    .map(|(n, v)| match v {
226                        Some(v) => format!("{n}={v}"),
227                        None => n.to_string(),
228                    })
229                    .collect::<Vec<_>>();
230                (Vec::new(), Some(baked_features))
231            }
232            gix_transport::Protocol::V2 => {
233                supports_include_tag = true;
234                (Command::Fetch.initial_v2_arguments(&features), None)
235            }
236        };
237
238        Arguments {
239            features,
240            version,
241            args: initial_arguments,
242            haves: Vec::new(),
243            filter,
244            shallow,
245            supports_include_tag,
246            deepen_not,
247            deepen_relative,
248            ref_in_want,
249            deepen_since,
250            features_for_first_want,
251            trace,
252        }
253    }
254}
255
256#[cfg(any(feature = "blocking-client", feature = "async-client"))]
257mod shared {
258    use bstr::{BString, ByteSlice};
259    use gix_transport::{client, client::MessageKind};
260
261    use crate::fetch::Arguments;
262
263    impl Arguments {
264        pub(in crate::fetch::arguments) fn prepare_v1(
265            &mut self,
266            transport_is_stateful: bool,
267            add_done_argument: bool,
268        ) -> Result<(MessageKind, Option<Vec<BString>>), client::Error> {
269            if self.haves.is_empty() {
270                assert!(add_done_argument, "If there are no haves, is_done must be true.");
271            }
272            let on_into_read = if add_done_argument {
273                client::MessageKind::Text(&b"done"[..])
274            } else {
275                client::MessageKind::Flush
276            };
277            let retained_state = if transport_is_stateful {
278                None
279            } else {
280                Some(self.args.clone())
281            };
282
283            if let Some(first_arg_position) = self.args.iter().position(|l| l.starts_with_str("want ")) {
284                self.args.swap(first_arg_position, 0);
285            }
286            Ok((on_into_read, retained_state))
287        }
288    }
289}
290
291#[cfg(feature = "async-client")]
292mod async_io;
293
294#[cfg(feature = "blocking-client")]
295mod blocking_io;