gix/clone/fetch/
mod.rs

1use crate::{
2    bstr::{BString, ByteSlice},
3    clone::PrepareFetch,
4};
5use gix_ref::Category;
6
7/// The error returned by [`PrepareFetch::fetch_only()`].
8#[derive(Debug, thiserror::Error)]
9#[allow(missing_docs)]
10pub enum Error {
11    #[error(transparent)]
12    Connect(#[from] crate::remote::connect::Error),
13    #[error(transparent)]
14    PrepareFetch(#[from] crate::remote::fetch::prepare::Error),
15    #[error(transparent)]
16    Fetch(#[from] crate::remote::fetch::Error),
17    #[error(transparent)]
18    RemoteInit(#[from] crate::remote::init::Error),
19    #[error("Custom configuration of remote to clone from failed")]
20    RemoteConfiguration(#[source] Box<dyn std::error::Error + Send + Sync>),
21    #[error("Custom configuration of connection to use when cloning failed")]
22    RemoteConnection(#[source] Box<dyn std::error::Error + Send + Sync>),
23    #[error(transparent)]
24    RemoteName(#[from] crate::config::remote::symbolic_name::Error),
25    #[error(transparent)]
26    ParseConfig(#[from] crate::config::overrides::Error),
27    #[error(transparent)]
28    ApplyConfig(#[from] crate::config::Error),
29    #[error("Failed to load repo-local git configuration before writing")]
30    LoadConfig(#[from] gix_config::file::init::from_paths::Error),
31    #[error("Failed to store configured remote in memory")]
32    SaveConfig(#[from] crate::remote::save::AsError),
33    #[error("Failed to write repository configuration to disk")]
34    SaveConfigIo(#[from] std::io::Error),
35    #[error("The remote HEAD points to a reference named {head_ref_name:?} which is invalid.")]
36    InvalidHeadRef {
37        source: gix_validate::reference::name::Error,
38        head_ref_name: crate::bstr::BString,
39    },
40    #[error("Failed to update HEAD with values from remote")]
41    HeadUpdate(#[from] crate::reference::edit::Error),
42    #[error("The remote didn't have any ref that matched '{}'", wanted.as_ref().as_bstr())]
43    RefNameMissing { wanted: gix_ref::PartialName },
44    #[error("The remote has {} refs for '{}', try to use a specific name: {}", candidates.len(), wanted.as_ref().as_bstr(), candidates.iter().filter_map(|n| n.to_str().ok()).collect::<Vec<_>>().join(", "))]
45    RefNameAmbiguous {
46        wanted: gix_ref::PartialName,
47        candidates: Vec<BString>,
48    },
49    #[error(transparent)]
50    CommitterOrFallback(#[from] crate::config::time::Error),
51    #[error(transparent)]
52    RefMap(#[from] crate::remote::ref_map::Error),
53    #[error(transparent)]
54    ReferenceName(#[from] gix_validate::reference::name::Error),
55}
56
57/// Modification
58impl PrepareFetch {
59    /// Fetch a pack and update local branches according to refspecs, providing `progress` and checking `should_interrupt` to stop
60    /// the operation.
61    /// On success, the persisted repository is returned, and this method must not be called again to avoid a **panic**.
62    /// On error, the method may be called again to retry as often as needed.
63    ///
64    /// If the remote repository was empty, that is newly initialized, the returned repository will also be empty and like
65    /// it was newly initialized.
66    ///
67    /// Note that all data we created will be removed once this instance drops if the operation wasn't successful.
68    ///
69    /// ### Note for users of `async`
70    ///
71    /// Even though `async` is technically supported, it will still be blocking in nature as it uses a lot of non-async writes
72    /// and computation under the hood. Thus it should be spawned into a runtime which can handle blocking futures.
73    #[gix_protocol::maybe_async::maybe_async]
74    pub async fn fetch_only<P>(
75        &mut self,
76        mut progress: P,
77        should_interrupt: &std::sync::atomic::AtomicBool,
78    ) -> Result<(crate::Repository, crate::remote::fetch::Outcome), Error>
79    where
80        P: crate::NestedProgress,
81        P::SubProgress: 'static,
82    {
83        use crate::{bstr::ByteVec, remote, remote::fetch::RefLogMessage};
84
85        let repo = self
86            .repo
87            .as_mut()
88            .expect("user error: multiple calls are allowed only until it succeeds");
89
90        repo.committer_or_set_generic_fallback()?;
91
92        if !self.config_overrides.is_empty() {
93            let mut snapshot = repo.config_snapshot_mut();
94            snapshot.append_config(&self.config_overrides, gix_config::Source::Api)?;
95        }
96
97        let remote_name = match self.remote_name.as_ref() {
98            Some(name) => name.to_owned(),
99            None => repo
100                .config
101                .resolved
102                .string(crate::config::tree::Clone::DEFAULT_REMOTE_NAME)
103                .map(|n| crate::config::tree::Clone::DEFAULT_REMOTE_NAME.try_into_symbolic_name(n))
104                .transpose()?
105                .unwrap_or_else(|| "origin".into()),
106        };
107
108        let mut remote = repo.remote_at(self.url.clone())?;
109
110        // For shallow clones without custom configuration, we'll use a single-branch refspec
111        // to match git's behavior (matching git's single-branch behavior for shallow clones).
112        let use_single_branch_for_shallow = self.shallow != remote::fetch::Shallow::NoChange
113            && remote.fetch_specs.is_empty()
114            && self.fetch_options.extra_refspecs.is_empty();
115
116        let target_ref = if use_single_branch_for_shallow {
117            // Determine target branch from user-specified ref_name or default branch
118            if let Some(ref_name) = &self.ref_name {
119                Some(Category::LocalBranch.to_full_name(ref_name.as_ref().as_bstr())?)
120            } else {
121                // For shallow clones without a specified ref, we need to determine the default branch.
122                // We'll connect to get HEAD information. For Protocol V2, we need to explicitly list refs.
123                let mut connection = remote.connect(remote::Direction::Fetch).await?;
124                if let Some(f) = self.configure_connection.as_mut() {
125                    f(&mut connection).map_err(Error::RemoteConnection)?;
126                }
127
128                // Perform handshake and try to get HEAD from it (works for Protocol V1)
129                let _ = connection.ref_map_by_ref(&mut progress, Default::default()).await?;
130
131                let target = if let Some(handshake) = &connection.handshake {
132                    // Protocol V1: refs are in handshake
133                    handshake.refs.as_ref().and_then(|refs| {
134                        refs.iter().find_map(|r| match r {
135                            gix_protocol::handshake::Ref::Symbolic {
136                                full_ref_name, target, ..
137                            } if full_ref_name == "HEAD" => gix_ref::FullName::try_from(target).ok(),
138                            _ => None,
139                        })
140                    })
141                } else {
142                    None
143                };
144
145                // For Protocol V2 or if we couldn't determine HEAD, use the configured default branch
146                let fallback_branch = target
147                    .or_else(|| {
148                        repo.config
149                            .resolved
150                            .string(crate::config::tree::Init::DEFAULT_BRANCH)
151                            .and_then(|name| Category::LocalBranch.to_full_name(name.as_bstr()).ok())
152                    })
153                    .unwrap_or_else(|| gix_ref::FullName::try_from("refs/heads/main").expect("known to be valid"));
154
155                // Drop the connection explicitly to release the borrow on remote
156                drop(connection);
157
158                Some(fallback_branch)
159            }
160        } else {
161            None
162        };
163
164        // Set up refspec based on whether we're doing a single-branch shallow clone,
165        // which requires a single ref to match Git unless it's overridden.
166        if remote.fetch_specs.is_empty() {
167            if let Some(target_ref) = &target_ref {
168                // Single-branch refspec for shallow clones
169                let short_name = target_ref.shorten();
170                remote = remote
171                    .with_refspecs(
172                        Some(format!("+{target_ref}:refs/remotes/{remote_name}/{short_name}").as_str()),
173                        remote::Direction::Fetch,
174                    )
175                    .expect("valid refspec");
176            } else {
177                // Wildcard refspec for non-shallow clones or when target couldn't be determined
178                remote = remote
179                    .with_refspecs(
180                        Some(format!("+refs/heads/*:refs/remotes/{remote_name}/*").as_str()),
181                        remote::Direction::Fetch,
182                    )
183                    .expect("valid static spec");
184            }
185        }
186
187        let mut clone_fetch_tags = None;
188        if let Some(f) = self.configure_remote.as_mut() {
189            remote = f(remote).map_err(Error::RemoteConfiguration)?;
190        } else {
191            clone_fetch_tags = remote::fetch::Tags::All.into();
192        }
193
194        let config = util::write_remote_to_local_config_file(&mut remote, remote_name.clone())?;
195
196        // Now we are free to apply remote configuration we don't want to be written to disk.
197        if let Some(fetch_tags) = clone_fetch_tags {
198            remote = remote.with_fetch_tags(fetch_tags);
199        }
200
201        // Add HEAD after the remote was written to config, we need it to know what to check out later, and assure
202        // the ref that HEAD points to is present no matter what.
203        let head_local_tracking_branch = format!("refs/remotes/{remote_name}/HEAD");
204        let head_refspec = gix_refspec::parse(
205            format!("HEAD:{head_local_tracking_branch}").as_str().into(),
206            gix_refspec::parse::Operation::Fetch,
207        )
208        .expect("valid")
209        .to_owned();
210        let pending_pack: remote::fetch::Prepare<'_, '_, _> = {
211            // For shallow clones, we already connected once, so we need to connect again
212            let mut connection = remote.connect(remote::Direction::Fetch).await?;
213            if let Some(f) = self.configure_connection.as_mut() {
214                f(&mut connection).map_err(Error::RemoteConnection)?;
215            }
216            let mut fetch_opts = {
217                let mut opts = self.fetch_options.clone();
218                if !opts.extra_refspecs.contains(&head_refspec) {
219                    opts.extra_refspecs.push(head_refspec.clone());
220                }
221                if let Some(ref_name) = &self.ref_name {
222                    opts.extra_refspecs.push(
223                        gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch)
224                            .expect("partial names are valid refspecs")
225                            .to_owned(),
226                    );
227                }
228                opts
229            };
230            match connection.prepare_fetch(&mut progress, fetch_opts.clone()).await {
231                Ok(prepare) => prepare,
232                Err(remote::fetch::prepare::Error::RefMap(remote::ref_map::Error::InitRefMap(
233                    gix_protocol::fetch::refmap::init::Error::MappingValidation(err),
234                ))) if err.issues.len() == 1
235                    && fetch_opts.extra_refspecs.contains(&head_refspec)
236                    && matches!(
237                        err.issues.first(),
238                        Some(gix_refspec::match_group::validate::Issue::Conflict {
239                            destination_full_ref_name,
240                            ..
241                        }) if *destination_full_ref_name == head_local_tracking_branch
242                    ) =>
243                {
244                    let head_refspec_idx = fetch_opts
245                        .extra_refspecs
246                        .iter()
247                        .enumerate()
248                        .find_map(|(idx, spec)| (*spec == head_refspec).then_some(idx))
249                        .expect("it's contained");
250                    // On the very special occasion that we fail as there is a remote `refs/heads/HEAD` reference that clashes
251                    // with our implicit refspec, retry without it. Maybe this tells us that we shouldn't have that implicit
252                    // refspec, as git can do this without connecting twice.
253                    let connection = remote.connect(remote::Direction::Fetch).await?;
254                    fetch_opts.extra_refspecs.remove(head_refspec_idx);
255                    connection.prepare_fetch(&mut progress, fetch_opts).await?
256                }
257                Err(err) => return Err(err.into()),
258            }
259        };
260
261        // Assure problems with custom branch names fail early, not after getting the pack or during negotiation.
262        if let Some(ref_name) = &self.ref_name {
263            util::find_custom_refname(pending_pack.ref_map(), ref_name)?;
264        }
265        if pending_pack.ref_map().object_hash != repo.object_hash() {
266            unimplemented!("configure repository to expect a different object hash as advertised by the server")
267        }
268        let reflog_message = {
269            let mut b = self.url.to_bstring();
270            b.insert_str(0, "clone: from ");
271            b
272        };
273        let outcome = pending_pack
274            .with_write_packed_refs_only(true)
275            .with_reflog_message(RefLogMessage::Override {
276                message: reflog_message.clone(),
277            })
278            .with_shallow(self.shallow.clone())
279            .receive(&mut progress, should_interrupt)
280            .await?;
281
282        util::append_config_to_repo_config(repo, config);
283        util::update_head(
284            repo,
285            &outcome.ref_map,
286            reflog_message.as_ref(),
287            remote_name.as_ref(),
288            self.ref_name.as_ref(),
289        )?;
290
291        Ok((self.repo.take().expect("still present"), outcome))
292    }
293
294    /// Similar to [`fetch_only()`][Self::fetch_only()`], but passes ownership to a utility type to configure a checkout operation.
295    #[cfg(all(feature = "worktree-mutation", feature = "blocking-network-client"))]
296    pub fn fetch_then_checkout<P>(
297        &mut self,
298        progress: P,
299        should_interrupt: &std::sync::atomic::AtomicBool,
300    ) -> Result<(crate::clone::PrepareCheckout, crate::remote::fetch::Outcome), Error>
301    where
302        P: crate::NestedProgress,
303        P::SubProgress: 'static,
304    {
305        let (repo, fetch_outcome) = self.fetch_only(progress, should_interrupt)?;
306        Ok((
307            crate::clone::PrepareCheckout {
308                repo: repo.into(),
309                ref_name: self.ref_name.clone(),
310            },
311            fetch_outcome,
312        ))
313    }
314}
315
316mod util;