use crate::{bstr::BString, clone::PrepareFetch, Repository}; /// The error returned by [`PrepareFetch::fetch_only()`].#[derive(Debug, thiserror::Error)]#[allow(missing_docs)]#[cfg(feature = "blocking-network-client")]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(transparent)] RemoteName(#[from] crate::config::remote::symbolic_name::Error), #[error("Failed to load repo-local git configuration before writing")] LoadConfig(#[from] git_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("The remote HEAD points to a reference named {head_ref_name:?} which is invalid.")] InvalidHeadRef { source: git_validate::refname::Error, head_ref_name: BString, }, #[error("Failed to update HEAD with values from remote")] HeadUpdate(#[from] crate::reference::edit::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. #[cfg(feature = "blocking-network-client")] pub fn fetch_only<P>( &mut self, progress: P, should_interrupt: &std::sync::atomic::AtomicBool, ) -> Result<(Repository, crate::remote::fetch::Outcome), Error> where P: crate::Progress, P::SubProgress: 'static, { use crate::{bstr::ByteVec, remote, remote::fetch::RefLogMessage}; let repo = self .repo .as_mut() .expect("user error: multiple calls are allowed only until it succeeds"); let remote_name = match self.remote_name.as_ref() { Some(name) => name.to_owned(), None => repo .config .resolved .string("clone", None, crate::config::tree::Clone::DEFAULT_REMOTE_NAME.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())? .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(|err| Error::RemoteConfiguration(err))?; } else { clone_fetch_tags = remote::fetch::Tags::All.into(); } let config = util::write_remote_to_local_config_file(&mut remote, remote_name.clone())?; // 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 checkout later, and assure // the ref that HEAD points to is present no matter what. let head_refspec = git_refspec::parse( format!("HEAD:refs/remotes/{remote_name}/HEAD").as_str().into(), git_refspec::parse::Operation::Fetch, ) .expect("valid") .to_owned(); let pending_pack: remote::fetch::Prepare<'_, '_, _, _> = remote.connect(remote::Direction::Fetch, progress)?.prepare_fetch({ let mut opts = self.fetch_options.clone(); if !opts.extra_refspecs.contains(&head_refspec) { opts.extra_refspecs.push(head_refspec) } opts })?; if pending_pack.ref_map().object_hash != repo.object_hash() { unimplemented!("configure repository to expect a different object hash as advertised by the server") } 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(), }) .receive(should_interrupt)?; util::replace_changed_local_config_file(repo, config); util::update_head( repo, &outcome.ref_map.remote_refs, reflog_message.as_ref(), remote_name.as_ref(), )?; Ok((self.repo.take().expect("still present"), outcome)) } /// Similar to [`fetch_only()`][Self::fetch_only()`], but passes ownership to a utility type to configure a checkout operation. #[cfg(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::Progress, P::SubProgress: 'static, { let (repo, fetch_outcome) = self.fetch_only(progress, should_interrupt)?; Ok((crate::clone::PrepareCheckout { repo: repo.into() }, fetch_outcome)) }} /// Builderimpl PrepareFetch { /// Set additional options to adjust parts of the fetch operation that are not affected by the git configuration. #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))] pub fn with_fetch_options(mut self, opts: crate::remote::ref_map::Options) -> Self { self.fetch_options = opts; self } /// Use `f` to apply arbitrary changes to the remote that is about to be used to fetch a pack. /// /// The passed in `remote` will be un-named and pre-configured to be a default remote as we know it from git-clone. /// It is not yet present in the configuration of the repository, /// but each change it will eventually be written to the configuration prior to performing a the fetch operation, /// _all changes done in `f()` will be persisted_. /// /// It can also be used to configure additional options, like those for fetching tags. Note that /// [with_fetch_tags()][crate::Remote::with_fetch_tags()] should be called here to configure the clone as desired. /// Otherwise a clone is configured to be complete and fetches all tags, not only those reachable from all branches. pub fn configure_remote( mut self, f: impl FnMut(crate::Remote<'_>) -> Result<crate::Remote<'_>, Box<dyn std::error::Error + Send + Sync>> + 'static, ) -> Self { self.configure_remote = Some(Box::new(f)); self } /// Set the remote's name to the given value after it was configured using the function provided via /// [`configure_remote()`][Self::configure_remote()]. /// /// If not set here, it defaults to `origin` or the value of `clone.defaultRemoteName`. pub fn with_remote_name(mut self, name: impl Into<BString>) -> Result<Self, crate::remote::name::Error> { self.remote_name = Some(crate::remote::name::validated(name)?); Ok(self) }} /// Consumptionimpl PrepareFetch { /// Persist the contained repository as is even if an error may have occurred when fetching from the remote. pub fn persist(mut self) -> Repository { self.repo.take().expect("present and consumed once") }} impl Drop for PrepareFetch { fn drop(&mut self) { if let Some(repo) = self.repo.take() { std::fs::remove_dir_all(repo.work_dir().unwrap_or_else(|| repo.path())).ok(); } }} impl From<PrepareFetch> for Repository { fn from(prep: PrepareFetch) -> Self { prep.persist() }} #[cfg(feature = "blocking-network-client")]mod util;
use crate::{bstr::BString, clone::PrepareFetch, Repository};
/// The error returned by [`PrepareFetch::fetch_only()`].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
#[cfg(feature = "blocking-network-client")]
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(transparent)]
RemoteName(#[from] crate::config::remote::symbolic_name::Error),
#[error("Failed to load repo-local git configuration before writing")]
LoadConfig(#[from] git_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("The remote HEAD points to a reference named {head_ref_name:?} which is invalid.")]
InvalidHeadRef {
source: git_validate::refname::Error,
head_ref_name: BString,
},
#[error("Failed to update HEAD with values from remote")]
HeadUpdate(#[from] crate::reference::edit::Error),
}
/// Modification
impl 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.
#[cfg(feature = "blocking-network-client")]
pub fn fetch_only<P>(
&mut self,
progress: P,
should_interrupt: &std::sync::atomic::AtomicBool,
) -> Result<(Repository, crate::remote::fetch::Outcome), Error>
where
P: crate::Progress,
P::SubProgress: 'static,
{
use crate::{bstr::ByteVec, remote, remote::fetch::RefLogMessage};
let repo = self
.repo
.as_mut()
.expect("user error: multiple calls are allowed only until it succeeds");
let remote_name = match self.remote_name.as_ref() {
Some(name) => name.to_owned(),
None => repo
.config
.resolved
.string("clone", None, crate::config::tree::Clone::DEFAULT_REMOTE_NAME.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())?
.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(|err| Error::RemoteConfiguration(err))?;
} else {
clone_fetch_tags = remote::fetch::Tags::All.into();
}
let config = util::write_remote_to_local_config_file(&mut remote, remote_name.clone())?;
// 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 checkout later, and assure
// the ref that HEAD points to is present no matter what.
let head_refspec = git_refspec::parse(
format!("HEAD:refs/remotes/{remote_name}/HEAD").as_str().into(),
git_refspec::parse::Operation::Fetch,
)
.expect("valid")
.to_owned();
let pending_pack: remote::fetch::Prepare<'_, '_, _, _> =
remote.connect(remote::Direction::Fetch, progress)?.prepare_fetch({
let mut opts = self.fetch_options.clone();
if !opts.extra_refspecs.contains(&head_refspec) {
opts.extra_refspecs.push(head_refspec)
}
opts
})?;
if pending_pack.ref_map().object_hash != repo.object_hash() {
unimplemented!("configure repository to expect a different object hash as advertised by the server")
}
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(),
})
.receive(should_interrupt)?;
util::replace_changed_local_config_file(repo, config);
util::update_head(
repo,
&outcome.ref_map.remote_refs,
reflog_message.as_ref(),
remote_name.as_ref(),
)?;
Ok((self.repo.take().expect("still present"), outcome))
}
/// Similar to [`fetch_only()`][Self::fetch_only()`], but passes ownership to a utility type to configure a checkout operation.
#[cfg(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::Progress,
P::SubProgress: 'static,
{
let (repo, fetch_outcome) = self.fetch_only(progress, should_interrupt)?;
Ok((crate::clone::PrepareCheckout { repo: repo.into() }, fetch_outcome))
}
}
/// Builder
impl PrepareFetch {
/// Set additional options to adjust parts of the fetch operation that are not affected by the git configuration.
#[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]
pub fn with_fetch_options(mut self, opts: crate::remote::ref_map::Options) -> Self {
self.fetch_options = opts;
self
}
/// Use `f` to apply arbitrary changes to the remote that is about to be used to fetch a pack.
///
/// The passed in `remote` will be un-named and pre-configured to be a default remote as we know it from git-clone.
/// It is not yet present in the configuration of the repository,
/// but each change it will eventually be written to the configuration prior to performing a the fetch operation,
/// _all changes done in `f()` will be persisted_.
///
/// It can also be used to configure additional options, like those for fetching tags. Note that
/// [with_fetch_tags()][crate::Remote::with_fetch_tags()] should be called here to configure the clone as desired.
/// Otherwise a clone is configured to be complete and fetches all tags, not only those reachable from all branches.
pub fn configure_remote(
mut self,
f: impl FnMut(crate::Remote<'_>) -> Result<crate::Remote<'_>, Box<dyn std::error::Error + Send + Sync>> + 'static,
) -> Self {
self.configure_remote = Some(Box::new(f));
self
}
/// Set the remote's name to the given value after it was configured using the function provided via
/// [`configure_remote()`][Self::configure_remote()].
///
/// If not set here, it defaults to `origin` or the value of `clone.defaultRemoteName`.
pub fn with_remote_name(mut self, name: impl Into<BString>) -> Result<Self, crate::remote::name::Error> {
self.remote_name = Some(crate::remote::name::validated(name)?);
Ok(self)
}
}
/// Consumption
impl PrepareFetch {
/// Persist the contained repository as is even if an error may have occurred when fetching from the remote.
pub fn persist(mut self) -> Repository {
self.repo.take().expect("present and consumed once")
}
}
impl Drop for PrepareFetch {
fn drop(&mut self) {
if let Some(repo) = self.repo.take() {
std::fs::remove_dir_all(repo.work_dir().unwrap_or_else(|| repo.path())).ok();
}
}
}
impl From<PrepareFetch> for Repository {
fn from(prep: PrepareFetch) -> Self {
prep.persist()
}
}
#[cfg(feature = "blocking-network-client")]
mod util;