Skip to main content

gix_protocol/fetch/
function.rs

1use std::{
2    path::Path,
3    sync::atomic::{AtomicBool, Ordering},
4};
5
6use gix_features::progress::DynNestedProgress;
7
8use crate::fetch::{
9    Arguments, Context, Error, Negotiate, NegotiateOutcome, Options, Outcome, ProgressId, Shallow, Tags, negotiate,
10};
11#[crate::bisync::only_async]
12use crate::transport::client::async_io::{ExtendedBufRead, HandleProgress, Transport};
13#[crate::bisync::only_sync]
14use crate::transport::client::blocking_io::{ExtendedBufRead, HandleProgress, Transport};
15
16/// Perform one fetch operation, relying on a `transport`.
17/// `negotiate` is used to run the negotiation of objects that should be contained in the pack, *if* one is to be received.
18/// `progress` and `should_interrupt` is passed to all potentially long-running parts of the operation.
19///
20/// `consume_pack(pack_read, progress, interrupt) -> bool` is always called to consume all bytes that are sent by the server, returning `true` if we should assure the pack is read to the end,
21/// or `false` to do nothing. Dropping the reader without reading to EOF (i.e. returning `false`) is an offense to the server, and
22/// `transport` won't be in the correct state to perform additional operations, or indicate the end of operation.
23/// Note that the passed reader blocking as the pack-writing is blocking as well.
24///
25/// The `Context` and `Options` further define parts of this `fetch` operation.
26///
27/// As opposed to a full `git fetch`, this operation does *not*…
28///
29/// * …update local refs
30/// * …end the interaction after the fetch
31///
32/// **Note that the interaction will never be ended**, even on error or failure, leaving it up to the caller to do that, maybe
33/// with the help of [`SendFlushOnDrop`](crate::SendFlushOnDrop) which can wrap `transport`.
34/// Generally, the `transport` is left in a state that allows for more commands to be run.
35///
36/// Return `Ok(None)` if there was nothing to do because all remote refs are at the same state as they are locally,
37/// or there was nothing wanted, or `Ok(Some(outcome))` to inform about all the changes that were made.
38#[crate::bisync::bisync]
39pub async fn fetch<P, T, E>(
40    negotiate: &mut impl Negotiate,
41    consume_pack: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result<bool, E>,
42    mut progress: P,
43    should_interrupt: &AtomicBool,
44    Context {
45        handshake,
46        transport,
47        user_agent,
48        trace_packetlines,
49    }: Context<'_, T>,
50    Options {
51        shallow_file,
52        shallow,
53        tags,
54        reject_shallow_remote,
55    }: Options<'_>,
56) -> Result<Option<Outcome>, Error>
57where
58    P: gix_features::progress::NestedProgress,
59    P::SubProgress: 'static,
60    T: Transport,
61    E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
62{
63    let _span = gix_trace::coarse!("gix_protocol::fetch()");
64    let v1_shallow_updates = handshake.v1_shallow_updates.take();
65    let protocol_version = handshake.server_protocol_version;
66
67    let fetch = crate::Command::Fetch;
68    let fetch_features = {
69        let mut f = fetch.default_features(protocol_version, &handshake.capabilities);
70        f.push(user_agent);
71        f
72    };
73
74    crate::fetch::Response::check_required_features(protocol_version, &fetch_features)?;
75    let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all");
76    let mut arguments = Arguments::new(protocol_version, fetch_features, trace_packetlines);
77    if matches!(tags, Tags::Included) {
78        if !arguments.can_use_include_tag() {
79            return Err(Error::MissingServerFeature {
80                    feature: "include-tag",
81                    description:
82                    // NOTE: if this is an issue, we could probably do what's proposed here.
83                    "To make this work we would have to implement another pass to fetch attached tags separately",
84                });
85        }
86        arguments.use_include_tag();
87    }
88    let (shallow_commits, mut shallow_lock) = add_shallow_args(&mut arguments, shallow, &shallow_file)?;
89
90    let negotiate_span = gix_trace::detail!(
91        "negotiate",
92        protocol_version = handshake.server_protocol_version as usize
93    );
94    let action = negotiate.mark_complete_and_common_ref()?;
95    let mut previous_response = None::<crate::fetch::Response>;
96    match &action {
97        negotiate::Action::NoChange | negotiate::Action::SkipToRefUpdate => Ok(None),
98        negotiate::Action::MustNegotiate {
99            remote_ref_target_known,
100        } => {
101            if !negotiate.add_wants(&mut arguments, remote_ref_target_known) {
102                return Ok(None);
103            }
104            let mut rounds = Vec::new();
105            let is_stateless = arguments.is_stateless(!transport.connection_persists_across_multiple_requests());
106            let mut state = negotiate::one_round::State::new(is_stateless);
107            let reader = 'negotiation: loop {
108                let _round = gix_trace::detail!("negotiate round", round = rounds.len() + 1);
109                progress.step();
110                progress.set_name(format!("negotiate (round {})", rounds.len() + 1));
111                if should_interrupt.load(Ordering::Relaxed) {
112                    return Err(Error::Negotiate(negotiate::Error::NegotiationFailed {
113                        rounds: rounds.len(),
114                    }));
115                }
116
117                let is_done = match negotiate.one_round(&mut state, &mut arguments, previous_response.as_ref()) {
118                    Ok((round, is_done)) => {
119                        rounds.push(round);
120                        is_done
121                    }
122                    Err(err) => {
123                        return Err(err.into());
124                    }
125                };
126                let mut reader = arguments.send(transport, is_done).await?;
127                if sideband_all {
128                    setup_remote_progress(&mut progress, &mut reader, should_interrupt);
129                }
130                let response =
131                    crate::fetch::Response::from_line_reader(protocol_version, &mut reader, is_done, !is_done).await?;
132                let has_pack = response.has_pack();
133                previous_response = Some(response);
134                if has_pack {
135                    progress.step();
136                    progress.set_name("receiving pack".into());
137                    if !sideband_all {
138                        setup_remote_progress(&mut progress, &mut reader, should_interrupt);
139                    }
140                    break 'negotiation reader;
141                }
142            };
143            // This needs drop if tracing is compiled in. We just don't know it.
144            drop(negotiate_span);
145
146            let mut previous_response = previous_response.expect("knowledge of a pack means a response was received");
147            previous_response.append_v1_shallow_updates(v1_shallow_updates);
148            if !previous_response.shallow_updates().is_empty() && shallow_lock.is_none() {
149                if reject_shallow_remote {
150                    return Err(Error::RejectShallowRemote);
151                }
152                shallow_lock = acquire_shallow_lock(&shallow_file).map(Some)?;
153            }
154
155            let (mut reader, may_read_to_end) =
156                consume_received_pack(reader, consume_pack, &mut progress, should_interrupt)?;
157
158            if may_read_to_end {
159                // Assure the final flush packet is consumed.
160                let has_read_to_end = reader.stopped_at().is_some();
161                if !has_read_to_end {
162                    read_remaining(&mut reader).await.map_err(Error::ReadRemainingBytes)?;
163                }
164            }
165            drop(reader);
166
167            if let Some(shallow_lock) = shallow_lock {
168                if !previous_response.shallow_updates().is_empty() {
169                    gix_shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates())?;
170                }
171            }
172            Ok(Some(Outcome {
173                last_response: previous_response,
174                negotiate: NegotiateOutcome { action, rounds },
175            }))
176        }
177    }
178}
179
180#[crate::bisync::only_async]
181fn consume_received_pack<R, E>(
182    reader: R,
183    consume: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result<bool, E>,
184    progress: &mut dyn DynNestedProgress,
185    should_interrupt: &AtomicBool,
186) -> Result<(R, bool), Error>
187where
188    R: crate::futures_io::AsyncBufRead + Unpin,
189    E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
190{
191    let mut reader = crate::futures_lite::io::BlockOn::new(reader);
192    let may_read_to_end =
193        consume(&mut reader, progress, should_interrupt).map_err(|err| Error::ConsumePack(err.into()))?;
194    Ok((reader.into_inner(), may_read_to_end))
195}
196
197#[crate::bisync::only_sync]
198fn consume_received_pack<R, E>(
199    mut reader: R,
200    consume: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result<bool, E>,
201    progress: &mut dyn DynNestedProgress,
202    should_interrupt: &AtomicBool,
203) -> Result<(R, bool), Error>
204where
205    R: std::io::BufRead,
206    E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
207{
208    let may_read_to_end =
209        consume(&mut reader, progress, should_interrupt).map_err(|err| Error::ConsumePack(err.into()))?;
210    Ok((reader, may_read_to_end))
211}
212
213#[crate::bisync::only_async]
214async fn read_remaining(reader: &mut (impl crate::futures_io::AsyncRead + Unpin)) -> std::io::Result<()> {
215    crate::futures_lite::io::copy(reader, &mut crate::futures_lite::io::sink())
216        .await
217        .map(|_| ())
218}
219
220#[crate::bisync::only_sync]
221fn read_remaining(reader: &mut impl std::io::Read) -> std::io::Result<()> {
222    std::io::copy(reader, &mut std::io::sink()).map(|_| ())
223}
224
225fn acquire_shallow_lock(shallow_file: &Path) -> Result<gix_lock::File, Error> {
226    gix_lock::File::acquire_to_update_resource(shallow_file, gix_lock::acquire::Fail::Immediately, None)
227        .map_err(Into::into)
228}
229
230fn add_shallow_args(
231    args: &mut Arguments,
232    shallow: &Shallow,
233    shallow_file: &std::path::Path,
234) -> Result<(Option<nonempty::NonEmpty<gix_hash::ObjectId>>, Option<gix_lock::File>), Error> {
235    let expect_change = *shallow != Shallow::NoChange;
236    let shallow_lock = expect_change.then(|| acquire_shallow_lock(shallow_file)).transpose()?;
237
238    let shallow_commits = gix_shallow::read(shallow_file)?;
239    if (shallow_commits.is_some() || expect_change) && !args.can_use_shallow() {
240        // NOTE: if this is an issue, we can always unshallow the repo ourselves.
241        return Err(Error::MissingServerFeature {
242            feature: "shallow",
243            description: "shallow clones need server support to remain shallow, otherwise bigger than expected packs are sent effectively unshallowing the repository",
244        });
245    }
246    if let Some(shallow_commits) = &shallow_commits {
247        for commit in shallow_commits.iter() {
248            args.shallow(commit);
249        }
250    }
251    match shallow {
252        Shallow::NoChange => {}
253        Shallow::DepthAtRemote(commits) => args.deepen(commits.get() as usize),
254        Shallow::Deepen(commits) => {
255            args.deepen(*commits as usize);
256            args.deepen_relative();
257        }
258        Shallow::Since { cutoff } => {
259            args.deepen_since(cutoff.seconds);
260        }
261        Shallow::Exclude {
262            remote_refs,
263            since_cutoff,
264        } => {
265            if let Some(cutoff) = since_cutoff {
266                args.deepen_since(cutoff.seconds);
267            }
268            for ref_ in remote_refs {
269                args.deepen_not(ref_.as_ref().as_bstr());
270            }
271        }
272    }
273    Ok((shallow_commits, shallow_lock))
274}
275
276fn setup_remote_progress<'a>(
277    progress: &mut dyn gix_features::progress::DynNestedProgress,
278    reader: &mut Box<dyn ExtendedBufRead<'a> + Unpin + 'a>,
279    should_interrupt: &'a AtomicBool,
280) {
281    reader.set_progress_handler(Some(Box::new({
282        let mut remote_progress = progress.add_child_with_id("remote".to_string(), ProgressId::RemoteProgress.into());
283        move |is_err: bool, data: &[u8]| {
284            crate::RemoteProgress::translate_to_progress(is_err, data, &mut remote_progress);
285            if should_interrupt.load(Ordering::Relaxed) {
286                std::ops::ControlFlow::Break(())
287            } else {
288                std::ops::ControlFlow::Continue(())
289            }
290        }
291    }) as HandleProgress<'a>));
292}