420 lines · 19.4 KB · Rust
RawDownload
use crate::{    bstr::{BString, ByteSlice},    clone::PrepareFetch,};use gix_ref::Category; /// The error returned by [`PrepareFetch::fetch_only()`].#[derive(Debug, thiserror::Error)]#[expect(missing_docs)]pub enum Error {    #[error(transparent)]    Connect(#[from] crate::remote::connect::Error),    #[error(transparent)]    PrepareFetch(#[from] crate::remote::fetch::prepare::Error),    #[error(transparent)]    Fetch(#[from] crate::remote::fetch::Error),    #[error(transparent)]    RemoteInit(#[from] crate::remote::init::Error),    #[error("Custom configuration of remote to clone from failed")]    RemoteConfiguration(#[source] Box<dyn std::error::Error + Send + Sync>),    #[error("Custom configuration of connection to use when cloning failed")]    RemoteConnection(#[source] Box<dyn std::error::Error + Send + Sync>),    #[error(transparent)]    RemoteName(#[from] crate::config::remote::symbolic_name::Error),    #[error(transparent)]    ParseConfig(#[from] crate::config::overrides::Error),    #[error(transparent)]    ApplyConfig(#[from] crate::config::Error),    #[error(transparent)]    Span(#[from] gix_config::parse::span::Error),    #[error(transparent)]    ConfigValue(#[from] gix_config::file::section::value::Error),    #[error("Failed to load repo-local git configuration before writing")]    LoadConfig(#[from] gix_config::file::init::from_paths::Error),    #[error("Failed to store configured remote in memory")]    SaveConfig(#[from] crate::remote::save::AsError),    #[error("Failed to write repository configuration to disk")]    SaveConfigIo(#[from] std::io::Error),    #[error("Failed to acquire lock to write repository configuration to disk")]    SaveConfigLockAcquire(#[from] gix_lock::acquire::Error),    #[error("Failed to commit lock after writing repository configuration to disk")]    SaveConfigLockCommit(#[from] gix_lock::commit::Error<gix_lock::File>),    #[error("The remote HEAD points to a reference named {head_ref_name:?} which is invalid.")]    InvalidHeadRef {        source: gix_validate::reference::name::Error,        head_ref_name: crate::bstr::BString,    },    #[error("Failed to update HEAD with values from remote")]    HeadUpdate(#[from] crate::reference::edit::Error),    #[error("The remote didn't have any ref that matched '{}'", wanted.as_ref().as_bstr())]    RefNameMissing { wanted: gix_ref::PartialName },    #[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(", "))]    RefNameAmbiguous {        wanted: gix_ref::PartialName,        candidates: Vec<BString>,    },    #[error(transparent)]    CommitterOrFallback(#[from] crate::config::commit_signature::Error),    #[error(transparent)]    RefMap(#[from] crate::remote::ref_map::Error),    #[error(transparent)]    ReferenceName(#[from] gix_validate::reference::name::Error),    #[error(        "Remote now uses {remote} even though the local repository was reconfigured to {local} \        to match the remote's previously advertised object format. The server is advertising \        inconsistent formats; if the correct one is known, pass `create::Options {{ \        object_hash: Some(gix_hash::Kind::{remote:?}), .. }}` to `PrepareFetch::new` to pin it."    )]    IncompatibleObjectHash {        local: gix_hash::Kind,        remote: gix_hash::Kind,    },    #[cfg(feature = "sha256")]    #[error("Failed to reopen the local repository after adopting the remote's object format")]    ReopenWithObjectHash(#[from] crate::open::Error),    #[cfg(feature = "sha256")]    #[error("Failed to transfer in-memory configuration after adopting the remote's object format")]    TransferInMemoryConfig(#[from] gix_config::file::init::Error),} /// Modificationimpl PrepareFetch {    /// Fetch a pack and update local branches according to refspecs, providing `progress` and checking `should_interrupt` to stop    /// the operation.    /// On success, the persisted repository is returned, and this method must not be called again to avoid a **panic**.    /// On error, the method may be called again to retry as often as needed.    ///    /// If the remote repository was empty, that is newly initialized, the returned repository will also be empty and like    /// it was newly initialized.    ///    /// Note that all data we created will be removed once this instance drops if the operation wasn't successful.    ///    /// ### Note for users of `async`    ///    /// Even though `async` is technically supported, it will still be blocking in nature as it uses a lot of non-async writes    /// and computation under the hood. Thus it should be spawned into a runtime which can handle blocking futures.    #[gix_protocol::bisync::bisync]    pub async fn fetch_only<P>(        &mut self,        mut progress: P,        should_interrupt: &std::sync::atomic::AtomicBool,    ) -> Result<(crate::Repository, crate::remote::fetch::Outcome), Error>    where        P: crate::NestedProgress,        P::SubProgress: 'static,    {        use crate::{bstr::ByteVec, remote, remote::fetch::RefLogMessage};         let mut repo = self            .repo            .as_ref()            .expect("user error: multiple calls are allowed only until it succeeds")            .clone();         repo.committer_or_set_generic_fallback()?;         if !self.config_overrides.is_empty() {            let mut snapshot = repo.config_snapshot_mut();            snapshot.append_config(&self.config_overrides, gix_config::Source::Api)?;        }         let remote_name = match self.remote_name.as_ref() {            Some(name) => name.to_owned(),            None => repo                .config                .resolved                .string(crate::config::tree::Clone::DEFAULT_REMOTE_NAME)                .map(|n| crate::config::tree::Clone::DEFAULT_REMOTE_NAME.try_into_symbolic_name(n))                .transpose()?                .unwrap_or_else(|| "origin".into()),        };         let mut remote = repo.remote_at(self.url.clone())?;         // For shallow clones without custom configuration, we'll use a single-branch refspec        // to match git's behavior (matching git's single-branch behavior for shallow clones).        let use_single_branch_for_shallow = self.shallow != remote::fetch::Shallow::NoChange            && remote.fetch_specs.is_empty()            && self.fetch_options.extra_refspecs.is_empty();         let target_ref = if use_single_branch_for_shallow {            // Determine target branch from user-specified ref_name or default branch            if let Some(ref_name) = &self.ref_name {                let prev_tags = std::mem::replace(&mut remote.fetch_tags, remote::fetch::Tags::None);                let mut connection = remote.connect(remote::Direction::Fetch).await?;                if let Some(f) = self.configure_connection.as_mut() {                    f(&mut connection).map_err(Error::RemoteConnection)?;                }                let (refmap, _) = connection                    .ref_map(                        &mut progress,                        remote::ref_map::Options {                            extra_refspecs: vec![                                gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch)                                    .expect("partial names are valid refspecs")                                    .to_owned(),                            ],                            ..Default::default()                        },                    )                    .await?;                let (_target, full_ref_name) = util::find_custom_refname(&refmap, ref_name)?;                remote.fetch_tags = prev_tags;                Some(full_ref_name.try_into()?)            } else {                // For shallow clones without a specified ref, we need to determine the ref to clone.                // Just fetch HEAD for that.                let prev_tags = std::mem::replace(&mut remote.fetch_tags, remote::fetch::Tags::None);                let mut connection = remote.connect(remote::Direction::Fetch).await?;                if let Some(f) = self.configure_connection.as_mut() {                    f(&mut connection).map_err(Error::RemoteConnection)?;                }                let (refmap, _) = connection                    .ref_map(                        &mut progress,                        remote::ref_map::Options {                            extra_refspecs: vec![                                gix_refspec::parse("HEAD".into(), gix_refspec::parse::Operation::Fetch)                                    .expect("valid")                                    .to_owned(),                            ],                            ..Default::default()                        },                    )                    .await?;                 // Find HEAD in the remote refs (works for both Protocol V1 and V2)                let target = refmap                    .remote_refs                    .iter()                    .find_map(|r| match r {                        gix_protocol::handshake::Ref::Symbolic {                            full_ref_name, target, ..                        }                        | gix_protocol::handshake::Ref::Unborn {                            full_ref_name, target, ..                        } if full_ref_name == "HEAD" => gix_ref::FullName::try_from(target)                            .map_err(|err| Error::InvalidHeadRef {                                head_ref_name: target.clone(),                                source: err,                            })                            .into(),                        _ => None,                    })                    .transpose()?;                 let target = target.ok_or_else(|| Error::RefNameMissing {                    wanted: "HEAD".try_into().expect("valid partial name"),                })?;                 remote.fetch_tags = prev_tags;                 Some(target)            }        } else {            None        };         // Set up refspec based on whether we're doing a single-branch shallow clone,        // which requires a single ref to match Git unless it's overridden.        if remote.fetch_specs.is_empty() {            if let Some(target_ref) = &target_ref {                // Single-branch refspec for shallow clones                let destination = match target_ref.category_and_short_name() {                    Some((Category::LocalBranch, short_name)) => {                        format!("refs/remotes/{remote_name}/{short_name}")                    }                    _ => target_ref.to_string(),                };                remote = remote                    .with_refspecs(                        Some(format!("+{target_ref}:{destination}").as_str()),                        remote::Direction::Fetch,                    )                    .expect("valid refspec");            } else {                // Wildcard refspec for non-shallow clones or when target couldn't be determined                remote = remote                    .with_refspecs(                        Some(format!("+refs/heads/*:refs/remotes/{remote_name}/*").as_str()),                        remote::Direction::Fetch,                    )                    .expect("valid static spec");            }        }         let mut clone_fetch_tags = None;        if let Some(f) = self.configure_remote.as_mut() {            remote = f(remote).map_err(Error::RemoteConfiguration)?;        } else {            clone_fetch_tags = remote::fetch::Tags::All.into();        }         // The remote section just written to `.git/config`, kept around so we can        // mirror it into the repository's in-memory config once we know which        // repo handle survives.        let config = Some(util::append_remote_to_local_config_file(            &mut remote,            remote_name.clone(),        )?);        #[cfg(feature = "sha256")]        let mut config = config;         // Now we are free to apply remote configuration we don't want to be written to disk.        if let Some(fetch_tags) = clone_fetch_tags {            remote = remote.with_fetch_tags(fetch_tags);        }         // Add HEAD after the remote was written to config, we need it to know what to check out later, and assure        // the ref that HEAD points to is present no matter what.        let head_local_tracking_branch = format!("refs/remotes/{remote_name}/HEAD");        let head_refspec = gix_refspec::parse(            format!("HEAD:{head_local_tracking_branch}").as_str().into(),            gix_refspec::parse::Operation::Fetch,        )        .expect("valid")        .to_owned();        let pending_pack = {            // For shallow clones, we already connected once, so we need to connect again            let mut connection = remote.connect(remote::Direction::Fetch).await?;            if let Some(f) = self.configure_connection.as_mut() {                f(&mut connection).map_err(Error::RemoteConnection)?;            }            let connection = connection.into_detached();            let mut fetch_opts = {                let mut opts = self.fetch_options.clone();                if !opts.extra_refspecs.contains(&head_refspec) {                    opts.extra_refspecs.push(head_refspec.clone());                }                if let Some(ref_name) = &self.ref_name {                    opts.extra_refspecs.push(                        gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch)                            .expect("partial names are valid refspecs")                            .to_owned(),                    );                }                opts            };            match connection.prepare_fetch(&repo, &mut progress, fetch_opts.clone()).await {                Ok(prepare) => prepare,                Err(remote::fetch::prepare::Error::RefMap(remote::ref_map::Error::InitRefMap(                    gix_protocol::fetch::refmap::init::Error::MappingValidation(err),                ))) if err.issues.len() == 1                    && fetch_opts.extra_refspecs.contains(&head_refspec)                    && matches!(                        err.issues.first(),                        Some(gix_refspec::match_group::validate::Issue::Conflict {                            destination_full_ref_name,                            ..                        }) if *destination_full_ref_name == head_local_tracking_branch                    ) =>                {                    let head_refspec_idx = fetch_opts                        .extra_refspecs                        .iter()                        .enumerate()                        .find_map(|(idx, spec)| (*spec == head_refspec).then_some(idx))                        .expect("it's contained");                    // On the very special occasion that we fail as there is a remote `refs/heads/HEAD` reference that clashes                    // with our implicit refspec, retry without it. Maybe this tells us that we shouldn't have that implicit                    // refspec, as git can do this without connecting twice.                    let connection = remote.connect(remote::Direction::Fetch).await?;                    let connection = connection.into_detached();                    fetch_opts.extra_refspecs.remove(head_refspec_idx);                    connection.prepare_fetch(&repo, &mut progress, fetch_opts).await?                }                Err(err) => return Err(err.into()),            }        };        drop(remote);         // Assure problems with custom branch names fail early, not after getting the pack or during negotiation.        if let Some(ref_name) = &self.ref_name {            util::find_custom_refname(pending_pack.ref_map(), ref_name)?;        }        // On an object-format mismatch: adopt the remote's format before receiving the pack.        // Only reachable with sha256, otherwise `gix_hash::Kind` has a single variant, so        // local and remote hashes can never differ.        #[cfg(feature = "sha256")]        {            let remote_object_hash = pending_pack.ref_map().object_hash;            if remote_object_hash != repo.object_hash() {                let mut in_memory_config = Vec::new();                repo.config.resolved.write_to_filter(&mut in_memory_config, |section| {                    section.meta().source == gix_config::Source::Api                })?;                // Reopen the still-empty repo with the remote's format; on error the original is kept for a retry.                repo = util::reinitialize_with_object_hash(&repo, remote_object_hash)?;                let mut resolved_config = repo.config.resolved.as_ref().clone();                // The reopened repo has the rewritten local config. Reapply the                // old API-only layer and then the remote config written during                // clone setup, matching the normal in-memory config order.                // TODO: make this much easier - we go from parsed-to-buffer-to-parsed.                //       Maybe make API changes available as overlay, just as utility over                //       Api sections.                resolved_config.append(gix_config::File::from_bytes_owned(                    &mut in_memory_config,                    gix_config::file::Metadata::api(),                    Default::default(),                )?)?;                repo.config                    .reread_values_and_clear_caches_replacing_config(resolved_config.into())?;                config = None;            }        }        let reflog_message = {            let mut b = self.url.to_bstring();            b.insert_str(0, "clone: from ");            b        };        let outcome = pending_pack            .with_write_packed_refs_only(true)            .with_reflog_message(RefLogMessage::Override {                message: reflog_message.clone(),            })            .with_shallow(self.shallow.clone())            .receive(&repo, &mut progress, should_interrupt)            .await?;         // Before finalisation, the current repo handle still needs to        // learn about the remote config written after it was opened.        if let Some(config) = config {            util::append_config_to_repo_config(&mut repo, config)?;        }        util::update_head(            &mut repo,            &outcome.ref_map,            reflog_message.as_ref(),            remote_name.as_ref(),            self.ref_name.as_ref(),        )?;         drop(self.repo.take().expect("still present"));        Ok((repo, outcome))    }     /// Similar to [`fetch_only()`][Self::fetch_only()`], but passes ownership to a utility type to configure a checkout operation.    #[cfg(all(feature = "worktree-mutation", feature = "blocking-network-client"))]    pub fn fetch_then_checkout<P>(        &mut self,        progress: P,        should_interrupt: &std::sync::atomic::AtomicBool,    ) -> Result<(crate::clone::PrepareCheckout, crate::remote::fetch::Outcome), Error>    where        P: crate::NestedProgress,        P::SubProgress: 'static,    {        let (repo, fetch_outcome) = self.fetch_only(progress, should_interrupt)?;        Ok((            crate::clone::PrepareCheckout {                repo: repo.into(),                ref_name: self.ref_name.clone(),                remove_worktree_on_drop: self.remove_worktree_on_drop,            },            fetch_outcome,        ))    }} mod util;