summaryrefslogtreecommitdiffstats
path: root/vendor/gix/src/remote
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gix/src/remote')
-rw-r--r--vendor/gix/src/remote/connect.rs26
-rw-r--r--vendor/gix/src/remote/connection/access.rs2
-rw-r--r--vendor/gix/src/remote/connection/fetch/error.rs2
-rw-r--r--vendor/gix/src/remote/connection/fetch/mod.rs11
-rw-r--r--vendor/gix/src/remote/connection/fetch/negotiate.rs392
-rw-r--r--vendor/gix/src/remote/connection/fetch/receive_pack.rs296
-rw-r--r--vendor/gix/src/remote/connection/fetch/update_refs/tests.rs14
-rw-r--r--vendor/gix/src/remote/connection/ref_map.rs19
-rw-r--r--vendor/gix/src/remote/fetch.rs35
9 files changed, 564 insertions, 233 deletions
diff --git a/vendor/gix/src/remote/connect.rs b/vendor/gix/src/remote/connect.rs
index 3dbd93486..63475b7c5 100644
--- a/vendor/gix/src/remote/connect.rs
+++ b/vendor/gix/src/remote/connect.rs
@@ -24,8 +24,8 @@ mod error {
Connect(#[from] gix_protocol::transport::client::connect::Error),
#[error("The {} url was missing - don't know where to establish a connection to", direction.as_str())]
MissingUrl { direction: remote::Direction },
- #[error("Protocol named {given:?} is not a valid protocol. Choose between 1 and 2")]
- UnknownProtocol { given: BString },
+ #[error("The given protocol version was invalid. Choose between 1 and 2")]
+ UnknownProtocol { source: config::key::GenericErrorWithValue },
#[error("Could not verify that \"{}\" url is a valid git directory before attempting to use it", url.to_bstring())]
FileUrl {
source: Box<gix_discover::is_git::Error>,
@@ -128,25 +128,9 @@ impl<'repo> Remote<'repo> {
Ok(url)
}
- use gix_protocol::transport::Protocol;
- let version = self
- .repo
- .config
- .resolved
- .integer("protocol", None, "version")
- .unwrap_or(Ok(2))
- .map_err(|err| Error::UnknownProtocol { given: err.input })
- .and_then(|num| {
- Ok(match num {
- 1 => Protocol::V1,
- 2 => Protocol::V2,
- num => {
- return Err(Error::UnknownProtocol {
- given: num.to_string().into(),
- })
- }
- })
- })?;
+ let version = crate::config::tree::Protocol::VERSION
+ .try_into_protocol_version(self.repo.config.resolved.integer("protocol", None, "version"))
+ .map_err(|err| Error::UnknownProtocol { source: err })?;
let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned();
if !self.repo.config.url_scheme()?.allow(&url.scheme) {
diff --git a/vendor/gix/src/remote/connection/access.rs b/vendor/gix/src/remote/connection/access.rs
index eba603da0..932fdb09d 100644
--- a/vendor/gix/src/remote/connection/access.rs
+++ b/vendor/gix/src/remote/connection/access.rs
@@ -13,7 +13,7 @@ impl<'a, 'repo, T> Connection<'a, 'repo, T> {
///
/// A custom function may also be used to prevent accessing resources with authentication.
///
- /// Use the [configured_credentials()][Connection::configured_credentials()] method to obtain the implementation
+ /// Use the [`configured_credentials()`][Connection::configured_credentials()] method to obtain the implementation
/// that would otherwise be used, which can be useful to proxy the default configuration and obtain information about the
/// URLs to authenticate with.
pub fn with_credentials(
diff --git a/vendor/gix/src/remote/connection/fetch/error.rs b/vendor/gix/src/remote/connection/fetch/error.rs
index afcacca13..5034dcb5d 100644
--- a/vendor/gix/src/remote/connection/fetch/error.rs
+++ b/vendor/gix/src/remote/connection/fetch/error.rs
@@ -43,6 +43,8 @@ pub enum Error {
RejectShallowRemoteConfig(#[from] config::boolean::Error),
#[error("Receiving objects from shallow remotes is prohibited due to the value of `clone.rejectShallow`")]
RejectShallowRemote,
+ #[error(transparent)]
+ NegotiationAlgorithmConfig(#[from] config::key::GenericErrorWithValue),
}
impl gix_protocol::transport::IsSpuriousError for Error {
diff --git a/vendor/gix/src/remote/connection/fetch/mod.rs b/vendor/gix/src/remote/connection/fetch/mod.rs
index a51ae7c54..b4fe00935 100644
--- a/vendor/gix/src/remote/connection/fetch/mod.rs
+++ b/vendor/gix/src/remote/connection/fetch/mod.rs
@@ -43,6 +43,8 @@ impl RefLogMessage {
pub enum Status {
/// Nothing changed as the remote didn't have anything new compared to our tracking branches, thus no pack was received
/// and no new object was added.
+ ///
+ /// As we could determine that nothing changed without remote interaction, there was no negotiation at all.
NoPackReceived {
/// However, depending on the refspecs, references might have been updated nonetheless to point to objects as
/// reported by the remote.
@@ -50,6 +52,8 @@ pub enum Status {
},
/// There was at least one tip with a new object which we received.
Change {
+ /// The number of rounds it took to minimize the pack to contain only the objects we don't have.
+ negotiation_rounds: usize,
/// Information collected while writing the pack and its index.
write_pack_bundle: gix_pack::bundle::write::Outcome,
/// Information collected while updating references.
@@ -58,6 +62,8 @@ pub enum Status {
/// A dry run was performed which leaves the local repository without any change
/// nor will a pack have been received.
DryRun {
+ /// The number of rounds it took to minimize the *would-be-sent*-pack to contain only the objects we don't have.
+ negotiation_rounds: usize,
/// Information about what updates to refs would have been done.
update_refs: refs::update::Outcome,
},
@@ -91,8 +97,7 @@ impl From<ProgressId> for gix_features::progress::Id {
}
}
-///
-pub mod negotiate;
+pub(crate) mod negotiate;
///
pub mod prepare {
@@ -158,7 +163,7 @@ impl<'remote, 'repo, T> Prepare<'remote, 'repo, T>
where
T: Transport,
{
- /// Return the ref_map (that includes the server handshake) which was part of listing refs prior to fetching a pack.
+ /// Return the `ref_map` (that includes the server handshake) which was part of listing refs prior to fetching a pack.
pub fn ref_map(&self) -> &RefMap {
&self.ref_map
}
diff --git a/vendor/gix/src/remote/connection/fetch/negotiate.rs b/vendor/gix/src/remote/connection/fetch/negotiate.rs
index 771c5acba..e94461bab 100644
--- a/vendor/gix/src/remote/connection/fetch/negotiate.rs
+++ b/vendor/gix/src/remote/connection/fetch/negotiate.rs
@@ -1,11 +1,12 @@
-use crate::remote::fetch;
+use std::borrow::Cow;
-/// The way the negotiation is performed
-#[derive(Copy, Clone)]
-pub(crate) enum Algorithm {
- /// Our very own implementation that probably should be replaced by one of the known algorithms soon.
- Naive,
-}
+use gix_negotiate::Flags;
+use gix_odb::HeaderExt;
+use gix_pack::Find;
+
+use crate::remote::{fetch, fetch::Shallow};
+
+type Queue = gix_revision::PriorityQueue<gix_revision::graph::CommitterTimestamp, gix_hash::ObjectId>;
/// The error returned during negotiation.
#[derive(Debug, thiserror::Error)]
@@ -13,77 +14,336 @@ pub(crate) enum Algorithm {
pub enum Error {
#[error("We were unable to figure out what objects the server should send after {rounds} round(s)")]
NegotiationFailed { rounds: usize },
+ #[error(transparent)]
+ LookupCommitInGraph(#[from] gix_revision::graph::lookup::commit::Error),
+ #[error(transparent)]
+ InitRefsIterator(#[from] crate::reference::iter::init::Error),
+ #[error(transparent)]
+ InitRefsIteratorPlatform(#[from] crate::reference::iter::Error),
+ #[error(transparent)]
+ ObtainRefDuringIteration(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
+ #[error(transparent)]
+ LoadIndex(#[from] gix_odb::store::load_index::Error),
}
-/// Negotiate one round with `algo` by looking at `ref_map` and adjust `arguments` to contain the haves and wants.
-/// If this is not the first round, the `previous_response` is set with the last recorded server response.
-/// Returns `true` if the negotiation is done from our side so the server won't keep asking.
-#[allow(clippy::too_many_arguments)]
-pub(crate) fn one_round(
- algo: Algorithm,
- round: usize,
+#[must_use]
+pub(crate) enum Action {
+ /// None of the remote refs moved compared to our last recorded state (via tracking refs), so there is nothing to do at all,
+ /// not even a ref update.
+ NoChange,
+ /// Don't negotiate, don't fetch the pack, skip right to updating the references.
+ ///
+ /// This happens if we already have all local objects even though the server seems to have changed.
+ SkipToRefUpdate,
+ /// We can't know for sure if fetching *is not* needed, so we go ahead and negotiate.
+ MustNegotiate {
+ /// Each `ref_map.mapping` has a slot here which is `true` if we have the object the remote ref points to locally.
+ remote_ref_target_known: Vec<bool>,
+ },
+}
+
+/// This function is modeled after the similarly named one in the git codebase to do the following:
+///
+/// * figure out all advertised refs on the remote *that we already have* and keep track of the oldest one as cutoff date.
+/// * mark all of our own refs as tips for a traversal.
+/// * mark all their parents, recursively, up to (and including) the cutoff date up to which we have seen the servers commit that we have.
+/// * pass all known-to-be-common-with-remote commits to the negotiator as common commits.
+///
+/// This is done so that we already find the most recent common commits, even if we are ahead, which is *potentially* better than
+/// what we would get if we would rely on tracking refs alone, particularly if one wouldn't trust the tracking refs for some reason.
+///
+/// Note that git doesn't trust its own tracking refs as the server *might* have changed completely, for instance by force-pushing, so
+/// marking our local tracking refs as known is something that's actually not proven to be correct so it's not done.
+///
+/// Additionally, it does what's done in `transport.c` and we check if a fetch is actually needed as at least one advertised ref changed.
+///
+/// Finally, we also mark tips in the `negotiator` in one go to avoid traversing all refs twice, since we naturally encounter all tips during
+/// our own walk.
+///
+/// Return whether or not we should negotiate, along with a queue for later use.
+pub(crate) fn mark_complete_and_common_ref(
repo: &crate::Repository,
- ref_map: &crate::remote::fetch::RefMap,
- fetch_tags: crate::remote::fetch::Tags,
- arguments: &mut gix_protocol::fetch::Arguments,
- _previous_response: Option<&gix_protocol::fetch::Response>,
- shallow: Option<&fetch::Shallow>,
-) -> Result<bool, Error> {
- let tag_refspec_to_ignore = fetch_tags
- .to_refspec()
- .filter(|_| matches!(fetch_tags, crate::remote::fetch::Tags::Included));
- if let Some(fetch::Shallow::Deepen(0)) = shallow {
+ negotiator: &mut dyn gix_negotiate::Negotiator,
+ graph: &mut gix_negotiate::Graph<'_>,
+ ref_map: &fetch::RefMap,
+ shallow: &fetch::Shallow,
+) -> Result<Action, Error> {
+ if let fetch::Shallow::Deepen(0) = shallow {
// Avoid deepening (relative) with zero as it seems to upset the server. Git also doesn't actually
// perform the negotiation for some reason (couldn't find it in code).
- return Ok(true);
+ return Ok(Action::NoChange);
+ }
+ if let Some(fetch::Mapping {
+ remote: fetch::Source::Ref(gix_protocol::handshake::Ref::Unborn { .. }),
+ ..
+ }) = ref_map.mappings.last().filter(|_| ref_map.mappings.len() == 1)
+ {
+ // There is only an unborn branch, as the remote has an empty repository. This means there is nothing to do except for
+ // possibly reproducing the unborn branch locally.
+ return Ok(Action::SkipToRefUpdate);
}
- match algo {
- Algorithm::Naive => {
- assert_eq!(round, 1, "Naive always finishes after the first round, it claims.");
- let mut has_missing_tracking_branch = false;
- for mapping in &ref_map.mappings {
- if tag_refspec_to_ignore.map_or(false, |tag_spec| {
- mapping
- .spec_index
- .implicit_index()
- .and_then(|idx| ref_map.extra_refspecs.get(idx))
- .map_or(false, |spec| spec.to_ref() == tag_spec)
- }) {
- continue;
- }
- let have_id = mapping.local.as_ref().and_then(|name| {
- repo.find_reference(name)
- .ok()
- .and_then(|r| r.target().try_id().map(ToOwned::to_owned))
- });
- match have_id {
- Some(have_id) => {
- if let Some(want_id) = mapping.remote.as_id() {
- if want_id != have_id {
- arguments.want(want_id);
- arguments.have(have_id);
- }
- }
- }
- None => {
- if let Some(want_id) = mapping.remote.as_id() {
- arguments.want(want_id);
- has_missing_tracking_branch = true;
- }
- }
- }
+ // Compute the cut-off date by checking which of the refs advertised (and matched in refspecs) by the remote we have,
+ // and keep the oldest one.
+ let mut cutoff_date = None::<gix_revision::graph::CommitterTimestamp>;
+ let mut num_mappings_with_change = 0;
+ let mut remote_ref_target_known: Vec<bool> = std::iter::repeat(false).take(ref_map.mappings.len()).collect();
+
+ for (mapping_idx, mapping) in ref_map.mappings.iter().enumerate() {
+ let want_id = mapping.remote.as_id();
+ let have_id = mapping.local.as_ref().and_then(|name| {
+ // this is the only time git uses the peer-id.
+ let r = repo.find_reference(name).ok()?;
+ r.target().try_id().map(ToOwned::to_owned)
+ });
+
+ // Like git, we don't let known unchanged mappings participate in the tree traversal
+ if want_id.zip(have_id).map_or(true, |(want, have)| want != have) {
+ num_mappings_with_change += 1;
+ }
+
+ if let Some(commit) = want_id
+ .and_then(|id| graph.try_lookup_or_insert_commit(id.into(), |_| {}).transpose())
+ .transpose()?
+ {
+ remote_ref_target_known[mapping_idx] = true;
+ cutoff_date = cutoff_date.unwrap_or_default().max(commit.commit_time).into();
+ } else if want_id.map_or(false, |maybe_annotated_tag| repo.objects.contains(maybe_annotated_tag)) {
+ remote_ref_target_known[mapping_idx] = true;
+ }
+ }
+
+ // If any kind of shallowing operation is desired, the server may still create a pack for us.
+ if matches!(shallow, Shallow::NoChange) {
+ if num_mappings_with_change == 0 {
+ return Ok(Action::NoChange);
+ } else if remote_ref_target_known.iter().all(|known| *known) {
+ return Ok(Action::SkipToRefUpdate);
+ }
+ }
+
+ // color our commits as complete as identified by references, unconditionally
+ // (`git` is conditional here based on `deepen`, but it doesn't make sense and it's hard to extract from history when that happened).
+ let mut queue = Queue::new();
+ mark_all_refs_in_repo(repo, graph, &mut queue, Flags::COMPLETE)?;
+ mark_alternate_complete(repo, graph, &mut queue)?;
+ // Keep track of the tips, which happen to be on our queue right, before we traverse the graph with cutoff.
+ let tips = if let Some(cutoff) = cutoff_date {
+ let tips = Cow::Owned(queue.clone());
+ // color all their parents up to the cutoff date, the oldest commit we know the server has.
+ mark_recent_complete_commits(&mut queue, graph, cutoff)?;
+ tips
+ } else {
+ Cow::Borrowed(&queue)
+ };
+
+ // mark all complete advertised refs as common refs.
+ for mapping in ref_map
+ .mappings
+ .iter()
+ .zip(remote_ref_target_known.iter().copied())
+ // We need this filter as the graph wouldn't contain annotated tags.
+ .filter_map(|(mapping, known)| (!known).then_some(mapping))
+ {
+ let want_id = mapping.remote.as_id();
+ if let Some(common_id) = want_id
+ .and_then(|id| graph.get(id).map(|c| (c, id)))
+ .filter(|(c, _)| c.data.flags.contains(Flags::COMPLETE))
+ .map(|(_, id)| id)
+ {
+ negotiator.known_common(common_id.into(), graph)?;
+ }
+ }
+
+ // As negotiators currently may rely on getting `known_common` calls first and tips after, we adhere to that which is the only
+ // reason we cached the set of tips.
+ for tip in tips.iter_unordered() {
+ negotiator.add_tip(*tip, graph)?;
+ }
+
+ Ok(Action::MustNegotiate {
+ remote_ref_target_known,
+ })
+}
+
+/// Add all `wants` to `arguments`, which is the unpeeled direct target that the advertised remote ref points to.
+pub(crate) fn add_wants(
+ repo: &crate::Repository,
+ arguments: &mut gix_protocol::fetch::Arguments,
+ ref_map: &fetch::RefMap,
+ mapping_known: &[bool],
+ shallow: &fetch::Shallow,
+ fetch_tags: fetch::Tags,
+) {
+ // With included tags, we have to keep mappings of tags to handle them later when updating refs, but we don't want to
+ // explicitly `want` them as the server will determine by itself which tags are pointing to a commit it wants to send.
+ // If we would not exclude implicit tag mappings like this, we would get too much of the graph.
+ let tag_refspec_to_ignore = matches!(fetch_tags, crate::remote::fetch::Tags::Included)
+ .then(|| fetch_tags.to_refspec())
+ .flatten();
+
+ // When using shallow, we can't exclude `wants` as the remote won't send anything then. Thus we have to resend everything
+ // we have as want instead to get exactly the same graph, but possibly deepened.
+ let is_shallow = !matches!(shallow, fetch::Shallow::NoChange);
+ let wants = ref_map
+ .mappings
+ .iter()
+ .zip(mapping_known)
+ .filter_map(|(m, known)| (is_shallow || !*known).then_some(m));
+ for want in wants {
+ // Here we ignore implicit tag mappings if needed.
+ if tag_refspec_to_ignore.map_or(false, |tag_spec| {
+ want.spec_index
+ .implicit_index()
+ .and_then(|idx| ref_map.extra_refspecs.get(idx))
+ .map_or(false, |spec| spec.to_ref() == tag_spec)
+ }) {
+ continue;
+ }
+ let id_on_remote = want.remote.as_id();
+ if !arguments.can_use_ref_in_want() || matches!(want.remote, fetch::Source::ObjectId(_)) {
+ if let Some(id) = id_on_remote {
+ arguments.want(id);
}
+ } else {
+ arguments.want_ref(
+ want.remote
+ .as_name()
+ .expect("name available if this isn't an object id"),
+ )
+ }
+ let id_is_annotated_tag_we_have = id_on_remote
+ .and_then(|id| repo.objects.header(id).ok().map(|h| (id, h)))
+ .filter(|(_, h)| h.kind() == gix_object::Kind::Tag)
+ .map(|(id, _)| id);
+ if let Some(tag_on_remote) = id_is_annotated_tag_we_have {
+ // Annotated tags are not handled at all by negotiators in the commit-graph - they only see commits and thus won't
+ // ever add `have`s for tags. To correct for that, we add these haves here to avoid getting them sent again.
+ arguments.have(tag_on_remote)
+ }
+ }
+}
- if has_missing_tracking_branch || (shallow.is_some() && arguments.is_empty()) {
- if let Ok(Some(r)) = repo.head_ref() {
- if let Some(id) = r.target().try_id() {
- arguments.have(id);
- arguments.want(id);
+/// Remove all commits that are more recent than the cut-off, which is the commit time of the oldest common commit we have with the server.
+fn mark_recent_complete_commits(
+ queue: &mut Queue,
+ graph: &mut gix_negotiate::Graph<'_>,
+ cutoff: gix_revision::graph::CommitterTimestamp,
+) -> Result<(), Error> {
+ while let Some(id) = queue
+ .peek()
+ .and_then(|(commit_time, id)| (commit_time >= &cutoff).then_some(*id))
+ {
+ queue.pop();
+ let commit = graph.get(&id).expect("definitely set when adding tips or parents");
+ for parent_id in commit.parents.clone() {
+ let mut was_complete = false;
+ if let Some(parent) = graph
+ .try_lookup_or_insert_commit(parent_id, |md| {
+ was_complete = md.flags.contains(Flags::COMPLETE);
+ md.flags |= Flags::COMPLETE
+ })?
+ .filter(|_| !was_complete)
+ {
+ queue.insert(parent.commit_time, parent_id)
+ }
+ }
+ }
+ Ok(())
+}
+
+fn mark_all_refs_in_repo(
+ repo: &crate::Repository,
+ graph: &mut gix_negotiate::Graph<'_>,
+ queue: &mut Queue,
+ mark: Flags,
+) -> Result<(), Error> {
+ for local_ref in repo.references()?.all()?.peeled() {
+ let local_ref = local_ref?;
+ let id = local_ref.id().detach();
+ let mut is_complete = false;
+ if let Some(commit) = graph
+ .try_lookup_or_insert_commit(id, |md| {
+ is_complete = md.flags.contains(Flags::COMPLETE);
+ md.flags |= mark
+ })?
+ .filter(|_| !is_complete)
+ {
+ queue.insert(commit.commit_time, id);
+ };
+ }
+ Ok(())
+}
+
+fn mark_alternate_complete(
+ repo: &crate::Repository,
+ graph: &mut gix_negotiate::Graph<'_>,
+ queue: &mut Queue,
+) -> Result<(), Error> {
+ for alternate_repo in repo
+ .objects
+ .store_ref()
+ .alternate_db_paths()?
+ .into_iter()
+ .filter_map(|path| {
+ path.ancestors()
+ .nth(1)
+ .and_then(|git_dir| crate::open_opts(git_dir, repo.options.clone()).ok())
+ })
+ {
+ mark_all_refs_in_repo(&alternate_repo, graph, queue, Flags::ALTERNATE | Flags::COMPLETE)?;
+ }
+ Ok(())
+}
+
+/// Negotiate the nth `round` with `negotiator` sending `haves_to_send` after possibly making the known common commits
+/// as sent by the remote known to `negotiator` using `previous_response` if this isn't the first round.
+/// All `haves` are added to `arguments` accordingly.
+/// Returns the amount of haves actually sent.
+pub(crate) fn one_round(
+ negotiator: &mut dyn gix_negotiate::Negotiator,
+ graph: &mut gix_negotiate::Graph<'_>,
+ haves_to_send: usize,
+ arguments: &mut gix_protocol::fetch::Arguments,
+ previous_response: Option<&gix_protocol::fetch::Response>,
+ mut common: Option<&mut Vec<gix_hash::ObjectId>>,
+) -> Result<(usize, bool), Error> {
+ let mut seen_ack = false;
+ if let Some(response) = previous_response {
+ use gix_protocol::fetch::response::Acknowledgement;
+ for ack in response.acknowledgements() {
+ match ack {
+ Acknowledgement::Common(id) => {
+ seen_ack = true;
+ negotiator.in_common_with_remote(*id, graph)?;
+ if let Some(ref mut common) = common {
+ common.push(*id);
}
}
+ Acknowledgement::Ready => {
+ // NOTE: In git, there is some logic dealing with whether to expect a DELIM or FLUSH package,
+ // but we handle this with peeking.
+ }
+ Acknowledgement::Nak => {}
}
- Ok(true)
}
}
+
+ // `common` is set only if this is a stateless transport, and we repeat previously confirmed common commits as HAVE, because
+ // we are not going to repeat them otherwise.
+ if let Some(common) = common {
+ for have_id in common {
+ arguments.have(have_id);
+ }
+ }
+
+ let mut haves_sent = 0;
+ for have_id in (0..haves_to_send).map_while(|_| negotiator.next_have(graph)) {
+ arguments.have(have_id?);
+ haves_sent += 1;
+ }
+ // Note that we are differing from the git implementation, which does an extra-round of with no new haves sent at all.
+ // For us it seems better to just say we are done when we know we are done, as potentially additional acks won't affect the
+ // queue of any of our implementation at all (so the negotiator won't come up with more haves next time either).
+ Ok((haves_sent, seen_ack))
}
diff --git a/vendor/gix/src/remote/connection/fetch/receive_pack.rs b/vendor/gix/src/remote/connection/fetch/receive_pack.rs
index 99560fbca..7837a9d3a 100644
--- a/vendor/gix/src/remote/connection/fetch/receive_pack.rs
+++ b/vendor/gix/src/remote/connection/fetch/receive_pack.rs
@@ -1,18 +1,26 @@
-use std::sync::atomic::{AtomicBool, Ordering};
+use std::{
+ ops::DerefMut,
+ sync::atomic::{AtomicBool, Ordering},
+};
-use gix_odb::FindExt;
+use gix_odb::{store::RefreshMode, FindExt};
use gix_protocol::{
fetch::Arguments,
transport::{client::Transport, packetline::read::ProgressAction},
};
use crate::{
- config::tree::Clone,
+ config::{
+ cache::util::ApplyLeniency,
+ tree::{Clone, Fetch, Key},
+ },
remote,
remote::{
connection::fetch::config,
fetch,
- fetch::{negotiate, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Shallow, Status},
+ fetch::{
+ negotiate, negotiate::Algorithm, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Shallow, Status,
+ },
},
Progress, Repository,
};
@@ -99,9 +107,6 @@ where
}
let (shallow_commits, mut shallow_lock) = add_shallow_args(&mut arguments, &self.shallow, repo)?;
- let mut previous_response = None::<gix_protocol::fetch::Response>;
- let mut round = 1;
-
if self.ref_map.object_hash != repo.object_hash() {
return Err(Error::IncompatibleObjectHash {
local: repo.object_hash(),
@@ -109,118 +114,161 @@ where
});
}
- let reader = 'negotiation: loop {
- progress.step();
- progress.set_name(format!("negotiate (round {round})"));
-
- let is_done = match negotiate::one_round(
- negotiate::Algorithm::Naive,
- round,
- repo,
- &self.ref_map,
- con.remote.fetch_tags,
- &mut arguments,
- previous_response.as_ref(),
- (self.shallow != Shallow::NoChange).then_some(&self.shallow),
- ) {
- Ok(_) if arguments.is_empty() => {
- gix_protocol::indicate_end_of_interaction(&mut con.transport).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.refspecs(remote::Direction::Fetch),
- &self.ref_map.extra_refspecs,
- con.remote.fetch_tags,
- self.dry_run,
- self.write_packed_refs,
- )?;
- return Ok(Outcome {
- ref_map: std::mem::take(&mut self.ref_map),
- status: Status::NoPackReceived { update_refs },
- });
- }
- Ok(is_done) => is_done,
- Err(err) => {
- gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
- return Err(err.into());
- }
- };
- round += 1;
- let mut reader = arguments.send(&mut con.transport, is_done).await?;
- if sideband_all {
- setup_remote_progress(progress, &mut reader, should_interrupt);
- }
- let response = gix_protocol::fetch::Response::from_line_reader(protocol_version, &mut reader).await?;
- if response.has_pack() {
- progress.step();
- progress.set_name("receiving pack");
- if !sideband_all {
- setup_remote_progress(progress, &mut reader, should_interrupt);
- }
- previous_response = Some(response);
- break 'negotiation reader;
- } else {
- previous_response = Some(response);
- }
+ let mut negotiator = repo
+ .config
+ .resolved
+ .string_by_key(Fetch::NEGOTIATION_ALGORITHM.logical_name().as_str())
+ .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();
+ // assure that checking for unknown server refs doesn't trigger ODB refreshes.
+ r.objects.refresh = RefreshMode::Never;
+ // we cache everything of importance in the graph and thus don't need an object cache.
+ r.objects.unset_object_cache();
+ r
};
- let previous_response = previous_response.expect("knowledge of a pack means a response was received");
- if !previous_response.shallow_updates().is_empty() && shallow_lock.is_none() {
- let reject_shallow_remote = repo
- .config
- .resolved
- .boolean_filter_by_key("clone.rejectShallow", &mut repo.filter_config_section())
- .map(|val| Clone::REJECT_SHALLOW.enrich_error(val))
- .transpose()?
- .unwrap_or(false);
- if reject_shallow_remote {
- return Err(Error::RejectShallowRemote);
+ let mut graph = graph_repo.commit_graph();
+ let action = negotiate::mark_complete_and_common_ref(
+ &graph_repo,
+ negotiator.deref_mut(),
+ &mut graph,
+ &self.ref_map,
+ &self.shallow,
+ )?;
+ let mut previous_response = None::<gix_protocol::fetch::Response>;
+ let mut round = 1;
+ let mut write_pack_bundle = match &action {
+ negotiate::Action::NoChange | negotiate::Action::SkipToRefUpdate => {
+ gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
+ None
}
- shallow_lock = acquire_shallow_lock(repo).map(Some)?;
- }
+ negotiate::Action::MustNegotiate {
+ remote_ref_target_known,
+ } => {
+ negotiate::add_wants(
+ repo,
+ &mut arguments,
+ &self.ref_map,
+ remote_ref_target_known,
+ &self.shallow,
+ con.remote.fetch_tags,
+ );
+ let is_stateless =
+ arguments.is_stateless(!con.transport.connection_persists_across_multiple_requests());
+ let mut haves_to_send = gix_negotiate::window_size(is_stateless, None);
+ let mut seen_ack = false;
+ let mut in_vain = 0;
+ let mut common = is_stateless.then(Vec::new);
+ let reader = 'negotiation: loop {
+ progress.step();
+ progress.set_name(format!("negotiate (round {round})"));
- let 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,
- object_hash: con.remote.repo.object_hash(),
- };
+ let is_done = match negotiate::one_round(
+ negotiator.deref_mut(),
+ &mut graph,
+ haves_to_send,
+ &mut arguments,
+ previous_response.as_ref(),
+ common.as_mut(),
+ ) {
+ Ok((haves_sent, ack_seen)) => {
+ if ack_seen {
+ in_vain = 0;
+ }
+ seen_ack |= ack_seen;
+ in_vain += haves_sent;
+ let is_done = haves_sent != haves_to_send || (seen_ack && in_vain >= 256);
+ haves_to_send = gix_negotiate::window_size(is_stateless, haves_to_send);
+ is_done
+ }
+ Err(err) => {
+ gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
+ return Err(err.into());
+ }
+ };
+ let mut reader = arguments.send(&mut con.transport, is_done).await?;
+ if sideband_all {
+ setup_remote_progress(progress, &mut reader, should_interrupt);
+ }
+ let response =
+ gix_protocol::fetch::Response::from_line_reader(protocol_version, &mut reader, is_done).await?;
+ let has_pack = response.has_pack();
+ previous_response = Some(response);
+ if has_pack {
+ progress.step();
+ progress.set_name("receiving pack");
+ if !sideband_all {
+ setup_remote_progress(progress, &mut reader, should_interrupt);
+ }
+ break 'negotiation reader;
+ } else {
+ round += 1;
+ }
+ };
+ drop(graph);
+ drop(graph_repo);
+ let previous_response = previous_response.expect("knowledge of a pack means a response was received");
+ if !previous_response.shallow_updates().is_empty() && shallow_lock.is_none() {
+ let reject_shallow_remote = repo
+ .config
+ .resolved
+ .boolean_filter_by_key("clone.rejectShallow", &mut repo.filter_config_section())
+ .map(|val| Clone::REJECT_SHALLOW.enrich_error(val))
+ .transpose()?
+ .unwrap_or(false);
+ if reject_shallow_remote {
+ return Err(Error::RejectShallowRemote);
+ }
+ shallow_lock = acquire_shallow_lock(repo).map(Some)?;
+ }
- let mut write_pack_bundle = if matches!(self.dry_run, fetch::DryRun::No) {
- Some(gix_pack::Bundle::write_to_directory(
- #[cfg(feature = "async-network-client")]
- {
- gix_protocol::futures_lite::io::BlockOn::new(reader)
- },
- #[cfg(not(feature = "async-network-client"))]
- {
- reader
- },
- Some(repo.objects.store_ref().path().join("pack")),
- progress,
- should_interrupt,
- Some(Box::new({
- let repo = repo.clone();
- move |oid, buf| repo.objects.find(oid, buf).ok()
- })),
- options,
- )?)
- } else {
- drop(reader);
- None
- };
+ let 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,
+ object_hash: con.remote.repo.object_hash(),
+ };
- if matches!(protocol_version, gix_protocol::transport::Protocol::V2) {
- gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
- }
+ let write_pack_bundle = if matches!(self.dry_run, fetch::DryRun::No) {
+ Some(gix_pack::Bundle::write_to_directory(
+ #[cfg(feature = "async-network-client")]
+ {
+ gix_protocol::futures_lite::io::BlockOn::new(reader)
+ },
+ #[cfg(not(feature = "async-network-client"))]
+ {
+ reader
+ },
+ Some(repo.objects.store_ref().path().join("pack")),
+ progress,
+ should_interrupt,
+ Some(Box::new({
+ let repo = repo.clone();
+ move |oid, buf| repo.objects.find(oid, buf).ok()
+ })),
+ options,
+ )?)
+ } else {
+ drop(reader);
+ None
+ };
+
+ if matches!(protocol_version, gix_protocol::transport::Protocol::V2) {
+ gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
+ }
- if let Some(shallow_lock) = shallow_lock {
- if !previous_response.shallow_updates().is_empty() {
- crate::shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates())?;
+ if let Some(shallow_lock) = shallow_lock {
+ if !previous_response.shallow_updates().is_empty() {
+ crate::shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates())?;
+ }
+ }
+ write_pack_bundle
}
- }
+ };
let update_refs = refs::update(
repo,
@@ -243,16 +291,26 @@ where
}
}
- Ok(Outcome {
+ let out = Outcome {
ref_map: std::mem::take(&mut self.ref_map),
- status: match write_pack_bundle {
- Some(write_pack_bundle) => Status::Change {
- write_pack_bundle,
+ status: if matches!(self.dry_run, fetch::DryRun::Yes) {
+ assert!(write_pack_bundle.is_none(), "in dry run we never read a bundle");
+ Status::DryRun {
update_refs,
- },
- None => Status::DryRun { update_refs },
+ negotiation_rounds: round,
+ }
+ } else {
+ match write_pack_bundle {
+ Some(write_pack_bundle) => Status::Change {
+ write_pack_bundle,
+ update_refs,
+ negotiation_rounds: round,
+ },
+ None => Status::NoPackReceived { update_refs },
+ }
},
- })
+ };
+ Ok(out)
}
}
diff --git a/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs b/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs
index 145990ac8..47ab5d1a5 100644
--- a/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs
+++ b/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs
@@ -140,7 +140,7 @@ mod update {
&specs,
&[],
fetch::Tags::None,
- reflog_message.map(|_| fetch::DryRun::Yes).unwrap_or(fetch::DryRun::No),
+ reflog_message.map_or(fetch::DryRun::No, |_| fetch::DryRun::Yes),
fetch::WritePackedRefs::Never,
)
.unwrap();
@@ -153,7 +153,7 @@ mod update {
}],
"{spec:?}: {detail}"
);
- assert_eq!(out.edits.len(), reflog_message.map(|_| 1).unwrap_or(0));
+ assert_eq!(out.edits.len(), reflog_message.map_or(0, |_| 1));
if let Some(reflog_message) = reflog_message {
let edit = &out.edits[0];
match &edit.change {
@@ -559,13 +559,13 @@ mod update {
.mappings
.into_iter()
.map(|m| fetch::Mapping {
- remote: m
- .item_index
- .map(|idx| fetch::Source::Ref(references[idx].clone()))
- .unwrap_or_else(|| match m.lhs {
+ remote: m.item_index.map_or_else(
+ || match m.lhs {
gix_refspec::match_group::SourceRef::ObjectId(id) => fetch::Source::ObjectId(id),
_ => unreachable!("not a ref, must be id: {:?}", m),
- }),
+ },
+ |idx| fetch::Source::Ref(references[idx].clone()),
+ ),
local: m.rhs.map(|r| r.into_owned()),
spec_index: SpecIndex::ExplicitInRemote(m.spec_index),
})
diff --git a/vendor/gix/src/remote/connection/ref_map.rs b/vendor/gix/src/remote/connection/ref_map.rs
index abf9c8e00..95ddb6214 100644
--- a/vendor/gix/src/remote/connection/ref_map.rs
+++ b/vendor/gix/src/remote/connection/ref_map.rs
@@ -148,15 +148,15 @@ where
let mappings = mappings
.into_iter()
.map(|m| fetch::Mapping {
- remote: m
- .item_index
- .map(|idx| fetch::Source::Ref(remote.refs[idx].clone()))
- .unwrap_or_else(|| {
+ remote: m.item_index.map_or_else(
+ || {
fetch::Source::ObjectId(match m.lhs {
gix_refspec::match_group::SourceRef::ObjectId(id) => id,
_ => unreachable!("no item index implies having an object id"),
})
- }),
+ },
+ |idx| fetch::Source::Ref(remote.refs[idx].clone()),
+ ),
local: m.rhs.map(|c| c.into_owned()),
spec_index: if m.spec_index < num_explicit_specs {
SpecIndex::ExplicitInRemote(m.spec_index)
@@ -191,11 +191,10 @@ where
let authenticate = match self.authenticate.as_mut() {
Some(f) => f,
None => {
- let url = self
- .remote
- .url(Direction::Fetch)
- .map(ToOwned::to_owned)
- .unwrap_or_else(|| gix_url::parse(url.as_ref()).expect("valid URL to be provided by transport"));
+ let url = self.remote.url(Direction::Fetch).map_or_else(
+ || gix_url::parse(url.as_ref()).expect("valid URL to be provided by transport"),
+ ToOwned::to_owned,
+ );
credentials_storage = self.configured_credentials(url)?;
&mut credentials_storage
}
diff --git a/vendor/gix/src/remote/fetch.rs b/vendor/gix/src/remote/fetch.rs
index 0001447cb..0947ace3f 100644
--- a/vendor/gix/src/remote/fetch.rs
+++ b/vendor/gix/src/remote/fetch.rs
@@ -1,3 +1,18 @@
+///
+pub mod negotiate {
+ pub use gix_negotiate::Algorithm;
+
+ #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
+ pub use super::super::connection::fetch::negotiate::Error;
+ #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
+ pub(crate) use super::super::connection::fetch::negotiate::{
+ add_wants, mark_complete_and_common_ref, one_round, Action,
+ };
+}
+
+#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
+pub use super::connection::fetch::{prepare, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Status};
+
/// If `Yes`, don't really make changes but do as much as possible to get an idea of what would be done.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
@@ -53,6 +68,8 @@ impl Tags {
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub enum Shallow {
/// Fetch all changes from the remote without affecting the shallow boundary at all.
+ ///
+ /// This also means that repositories that aren't shallow will remain like that.
#[default]
NoChange,
/// Receive update to `depth` commits in the history of the refs to fetch (from the viewpoint of the remote),
@@ -126,7 +143,7 @@ pub enum Source {
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
impl Source {
/// Return either the direct object id we refer to or the direct target that a reference refers to.
- /// The latter may be a direct or a symbolic reference, and we degenerate this to the peeled object id.
+ /// The latter may be a direct or a symbolic reference.
/// If unborn, `None` is returned.
pub fn as_id(&self) -> Option<&gix_hash::oid> {
match self {
@@ -135,6 +152,17 @@ impl Source {
}
}
+ /// Returns the peeled id of this instance, that is the object that can't be de-referenced anymore.
+ pub fn peeled_id(&self) -> Option<&gix_hash::oid> {
+ match self {
+ Source::ObjectId(id) => Some(id),
+ Source::Ref(r) => {
+ let (_name, target, peeled) = r.unpack();
+ peeled.or(target)
+ }
+ }
+ }
+
/// Return ourselves as the full name of the reference we represent, or `None` if this source isn't a reference but an object.
pub fn as_name(&self) -> Option<&crate::bstr::BStr> {
match self {
@@ -195,8 +223,3 @@ pub struct Mapping {
/// The index into the fetch ref-specs used to produce the mapping, allowing it to be recovered.
pub spec_index: SpecIndex,
}
-
-#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
-pub use super::connection::fetch::{
- negotiate, prepare, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Status,
-};