use std::{ops::DerefMut, path::PathBuf, sync::atomic::AtomicBool};
use gix_error::{ResultExt, bail, message};
use gix_odb::store::RefreshMode;
use gix_protocol::fetch::{Arguments, negotiate};
#[cfg(feature = "async-network-client")]
use gix_transport::client::async_io::Transport;
#[cfg(feature = "blocking-network-client")]
use gix_transport::client::blocking_io::Transport;
use crate::{
Result,
config::{
cache::util::ApplyLeniency,
tree::{Clone, Fetch},
},
remote::{
connection::fetch::{PrepareDetached, config},
fetch,
fetch::{Outcome, Prepare, RefLogMessage, Status, negotiate::Algorithm, outcome, refs},
},
};
impl<T> Prepare<'_, '_, T>
where
T: Transport,
{
#[gix_protocol::bisync::bisync]
pub async fn receive<P>(self, progress: P, should_interrupt: &AtomicBool) -> Result<Outcome>
where
P: gix_utils::progress::NestedProgress,
P::SubProgress: 'static,
{
let Prepare { inner, repo } = self;
inner.receive(repo, progress, should_interrupt).await
}
}
impl<T> PrepareDetached<'_, T>
where
T: Transport,
{
#[gix_protocol::bisync::bisync]
pub(crate) async fn receive<P>(
mut self,
repo: &crate::Repository,
progress: P,
should_interrupt: &AtomicBool,
) -> Result<Outcome>
where
P: gix_utils::progress::NestedProgress,
P::SubProgress: 'static,
{
let ref_map = &self.ref_map;
if ref_map.is_missing_required_mapping() {
let mut specs = ref_map.refspecs.clone();
specs.extend(ref_map.extra_refspecs.clone());
bail!(gix_error::validation(format!(
"None of the refspec(s) {} matched any of the {} refs on the remote",
specs
.iter()
.map(|spec| spec.to_ref().instruction().to_bstring().to_string())
.collect::<Vec<_>>()
.join(", "),
ref_map.remote_refs.len()
)));
}
let mut con = self.con.take().expect("receive() can only be called once");
let mut handshake = con.handshake.take().expect("receive() can only be called once");
let expected_object_hash = repo.object_hash();
if ref_map.object_hash != expected_object_hash {
bail!(
"Cannot fetch from a remote that uses {} while local repository uses {expected_object_hash} for object hashes".unsupported(),
ref_map.object_hash
);
}
let fetch_options = gix_protocol::fetch::Options {
shallow_file: repo.shallow_file()?,
shallow: &self.shallow,
tags: con.remote.fetch_tags,
reject_shallow_remote: Clone::REJECT_SHALLOW
.enrich_error(
repo.config
.resolved
.boolean_filter("clone.rejectShallow", &mut repo.filter_config_section()),
)
.or_raise(|| message("Could not obtain configuration to learn if shallow remotes should be rejected"))?
.unwrap_or(false),
};
let context = gix_protocol::fetch::Context {
handshake: &mut handshake,
transport: &mut con.transport.inner,
user_agent: repo.config.user_agent_tuple(),
trace_packetlines: con.trace,
};
let negotiator = repo
.config
.resolved
.string(Fetch::NEGOTIATION_ALGORITHM)
.map(|n| Fetch::NEGOTIATION_ALGORITHM.try_into_negotiation_algorithm(n))
.transpose()
.with_leniency(repo.config.lenient_config)?
.unwrap_or(Algorithm::Consecutive)
.into_negotiator();
let graph_repo = {
let mut r = repo.clone();
r.objects.refresh = RefreshMode::Never;
r.objects.unset_object_cache();
r
};
let cache = graph_repo.commit_graph_if_enabled().ok().flatten();
let mut graph = graph_repo.revision_graph(cache.as_ref());
let alternates = repo.objects.store_ref().alternate_db_paths()?;
let mut negotiate = Negotiate {
objects: &graph_repo.objects,
refs: &graph_repo.refs,
graph: &mut graph,
alternates,
ref_map,
shallow: &self.shallow,
tags: con.remote.fetch_tags,
negotiator,
open_options: repo.options.clone(),
};
let write_pack_options = gix_pack::bundle::write::Options {
thread_limit: config::index_threads(repo)?,
index_version: config::pack_index_version(repo)?,
iteration_mode: gix_pack::data::input::Mode::Verify,
alloc_limit_bytes: repo.config.alloc_limit_bytes,
compression: repo.config.loose_compression,
};
let mut write_pack_bundle = None;
let res = gix_protocol::fetch(
&mut negotiate,
|reader, progress, should_interrupt| -> Result<bool> {
let mut may_read_to_end = false;
write_pack_bundle = if matches!(self.dry_run, fetch::DryRun::No) {
let res = gix_pack::Bundle::write_to_directory(
reader,
Some(&repo.objects.store_ref().path().join("pack")),
progress,
should_interrupt,
Some(Box::new({
let repo = repo.clone();
repo.objects
})),
repo.object_hash(),
write_pack_options,
)
.or_raise(|| message("Failed to write the received pack"))?;
may_read_to_end = true;
Some(res)
} else {
None
};
Ok(may_read_to_end)
},
progress,
should_interrupt,
context,
fetch_options,
)
.await?;
let negotiate = res.map(|v| outcome::Negotiate {
graph: graph.detach(),
rounds: v.negotiate.rounds,
});
if matches!(handshake.server_protocol_version, gix_protocol::transport::Protocol::V2) {
gix_protocol::indicate_end_of_interaction(&mut con.transport.inner, con.trace)
.await
.ok();
}
let update_refs = refs::update(
repo,
self.reflog_message
.take()
.unwrap_or_else(|| RefLogMessage::Prefixed { action: "fetch".into() }),
&self.ref_map.mappings,
con.remote.fetch_refspecs(),
&self.ref_map.extra_refspecs,
con.remote.fetch_tags,
self.dry_run,
self.write_packed_refs,
)?;
if let Some(bundle) = write_pack_bundle.as_mut()
&& (!update_refs.edits.is_empty() || bundle.index.num_objects == 0)
&& let Some(path) = bundle.keep_path.take()
{
std::fs::remove_file(&path).or_raise(|| message!("Failed to remove .keep file at {:?}", path.display()))?;
}
let out = Outcome {
handshake,
ref_map: std::mem::take(&mut self.ref_map),
status: match write_pack_bundle {
Some(write_pack_bundle) => Status::Change {
write_pack_bundle,
update_refs,
negotiate: negotiate.expect("if we have a pack, we always negotiated it"),
},
None => Status::NoPackReceived {
dry_run: matches!(self.dry_run, fetch::DryRun::Yes),
negotiate,
update_refs,
},
},
};
Ok(out)
}
}
struct Negotiate<'a, 'b, 'c> {
objects: &'a crate::OdbHandle,
refs: &'a gix_ref::file::Store,
graph: &'a mut gix_negotiate::Graph<'b, 'c>,
alternates: Vec<PathBuf>,
ref_map: &'a gix_protocol::fetch::RefMap,
shallow: &'a gix_protocol::fetch::Shallow,
tags: gix_protocol::fetch::Tags,
negotiator: Box<dyn gix_negotiate::Negotiator>,
open_options: crate::open::Options,
}
impl gix_protocol::fetch::Negotiate for Negotiate<'_, '_, '_> {
fn mark_complete_and_common_ref(&mut self) -> Result<gix_protocol::fetch::negotiate::Action> {
negotiate::mark_complete_and_common_ref(
&self.objects,
self.refs,
{
let alternates = std::mem::take(&mut self.alternates);
let open_options = self.open_options.clone();
move || {
Ok(alternates
.into_iter()
.filter_map(move |path| {
path.ancestors()
.nth(1)
.and_then(|git_dir| crate::open_opts(git_dir, open_options.clone()).ok())
})
.map(|repo| (repo.refs, repo.objects)))
}
},
self.negotiator.deref_mut(),
&mut *self.graph,
self.ref_map,
self.shallow,
negotiate::make_refmapping_ignore_predicate(self.tags, self.ref_map),
)
}
fn add_wants(&mut self, arguments: &mut Arguments, remote_ref_target_known: &[bool]) -> bool {
negotiate::add_wants(
self.objects,
arguments,
self.ref_map,
remote_ref_target_known,
self.shallow,
negotiate::make_refmapping_ignore_predicate(self.tags, self.ref_map),
)
}
fn one_round(
&mut self,
state: &mut negotiate::one_round::State,
arguments: &mut Arguments,
previous_response: Option<&gix_protocol::fetch::Response>,
) -> Result<(negotiate::Round, bool)> {
negotiate::one_round(
self.negotiator.deref_mut(),
&mut *self.graph,
state,
arguments,
previous_response,
)
}
}