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/build.rs23
-rw-r--r--vendor/gix/src/remote/connect.rs6
-rw-r--r--vendor/gix/src/remote/connection/fetch/mod.rs60
-rw-r--r--vendor/gix/src/remote/connection/fetch/negotiate.rs141
-rw-r--r--vendor/gix/src/remote/connection/fetch/receive_pack.rs129
-rw-r--r--vendor/gix/src/remote/connection/fetch/update_refs/mod.rs368
-rw-r--r--vendor/gix/src/remote/connection/fetch/update_refs/tests.rs427
-rw-r--r--vendor/gix/src/remote/connection/fetch/update_refs/update.rs45
-rw-r--r--vendor/gix/src/remote/connection/ref_map.rs6
-rw-r--r--vendor/gix/src/remote/errors.rs23
-rw-r--r--vendor/gix/src/remote/fetch.rs19
-rw-r--r--vendor/gix/src/remote/init.rs13
-rw-r--r--vendor/gix/src/remote/save.rs5
13 files changed, 996 insertions, 269 deletions
diff --git a/vendor/gix/src/remote/build.rs b/vendor/gix/src/remote/build.rs
index 10c216537..452da66a0 100644
--- a/vendor/gix/src/remote/build.rs
+++ b/vendor/gix/src/remote/build.rs
@@ -10,7 +10,10 @@ impl Remote<'_> {
Url: TryInto<gix_url::Url, Error = E>,
gix_url::parse::Error: From<E>,
{
- self.push_url_inner(url, true)
+ self.push_url_inner(
+ url.try_into().map_err(|err| remote::init::Error::Url(err.into()))?,
+ true,
+ )
}
/// Set the `url` to be used when pushing data to a remote, without applying rewrite rules in case these could be faulty,
@@ -20,7 +23,10 @@ impl Remote<'_> {
Url: TryInto<gix_url::Url, Error = E>,
gix_url::parse::Error: From<E>,
{
- self.push_url_inner(url, false)
+ self.push_url_inner(
+ url.try_into().map_err(|err| remote::init::Error::Url(err.into()))?,
+ false,
+ )
}
/// Configure how tags should be handled when fetching from the remote.
@@ -29,14 +35,11 @@ impl Remote<'_> {
self
}
- fn push_url_inner<Url, E>(mut self, push_url: Url, should_rewrite_urls: bool) -> Result<Self, remote::init::Error>
- where
- Url: TryInto<gix_url::Url, Error = E>,
- gix_url::parse::Error: From<E>,
- {
- let push_url = push_url
- .try_into()
- .map_err(|err| remote::init::Error::Url(err.into()))?;
+ fn push_url_inner(
+ mut self,
+ push_url: gix_url::Url,
+ should_rewrite_urls: bool,
+ ) -> Result<Self, remote::init::Error> {
self.push_url = push_url.into();
let (_, push_url_alias) = should_rewrite_urls
diff --git a/vendor/gix/src/remote/connect.rs b/vendor/gix/src/remote/connect.rs
index 63475b7c5..6acc9f67f 100644
--- a/vendor/gix/src/remote/connect.rs
+++ b/vendor/gix/src/remote/connect.rs
@@ -1,5 +1,7 @@
#![allow(clippy::result_large_err)]
+
use gix_protocol::transport::client::Transport;
+use std::borrow::Cow;
use crate::{remote::Connection, Remote};
@@ -104,7 +106,7 @@ impl<'repo> Remote<'repo> {
) -> Result<(gix_url::Url, gix_protocol::transport::Protocol), Error> {
fn sanitize(mut url: gix_url::Url) -> Result<gix_url::Url, Error> {
if url.scheme == gix_url::Scheme::File {
- let mut dir = gix_path::to_native_path_on_windows(url.path.as_ref());
+ let mut dir = gix_path::to_native_path_on_windows(Cow::Borrowed(url.path.as_ref()));
let kind = gix_discover::is_git(dir.as_ref())
.or_else(|_| {
dir.to_mut().push(gix_discover::DOT_GIT_DIR);
@@ -117,7 +119,7 @@ impl<'repo> Remote<'repo> {
let (git_dir, _work_dir) = gix_discover::repository::Path::from_dot_git_dir(
dir.clone().into_owned(),
kind,
- std::env::current_dir()?,
+ &std::env::current_dir()?,
)
.ok_or_else(|| Error::InvalidRemoteRepositoryPath {
directory: dir.into_owned(),
diff --git a/vendor/gix/src/remote/connection/fetch/mod.rs b/vendor/gix/src/remote/connection/fetch/mod.rs
index b4fe00935..8327d5abc 100644
--- a/vendor/gix/src/remote/connection/fetch/mod.rs
+++ b/vendor/gix/src/remote/connection/fetch/mod.rs
@@ -46,27 +46,25 @@ pub enum Status {
///
/// As we could determine that nothing changed without remote interaction, there was no negotiation at all.
NoPackReceived {
+ /// If `true`, we didn't receive a pack due to dry-run mode being enabled.
+ dry_run: bool,
+ /// Information about the pack negotiation phase if negotiation happened at all.
+ ///
+ /// It's possible that negotiation didn't have to happen as no reference of interest changed on the server.
+ negotiate: Option<outcome::Negotiate>,
/// However, depending on the refspecs, references might have been updated nonetheless to point to objects as
/// reported by the remote.
update_refs: refs::update::Outcome,
},
/// 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 about the pack negotiation phase.
+ negotiate: outcome::Negotiate,
/// Information collected while writing the pack and its index.
write_pack_bundle: gix_pack::bundle::write::Outcome,
/// Information collected while updating references.
update_refs: refs::update::Outcome,
},
- /// 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,
- },
}
/// The outcome of receiving a pack via [`Prepare::receive()`].
@@ -78,6 +76,46 @@ pub struct Outcome {
pub status: Status,
}
+/// Additional types related to the outcome of a fetch operation.
+pub mod outcome {
+ /// Information about the negotiation phase of a fetch.
+ ///
+ /// Note that negotiation can happen even if no pack is ultimately produced.
+ #[derive(Default, Debug, Clone)]
+ pub struct Negotiate {
+ /// The negotiation graph indicating what kind of information 'the algorithm' collected in the end.
+ pub graph: gix_negotiate::IdMap,
+ /// Additional information for each round of negotiation.
+ pub rounds: Vec<negotiate::Round>,
+ }
+
+ ///
+ pub mod negotiate {
+ /// Key information about each round in the pack-negotiation.
+ #[derive(Debug, Clone)]
+ pub struct Round {
+ /// The amount of `HAVE` lines sent this round.
+ ///
+ /// Each `HAVE` is an object that we tell the server about which would acknowledge each one it has as well.
+ pub haves_sent: usize,
+ /// A total counter, over all previous rounds, indicating how many `HAVE`s we sent without seeing a single acknowledgement,
+ /// i.e. the indication of a common object.
+ ///
+ /// This number maybe zero or be lower compared to the previous round if we have received at least one acknowledgement.
+ pub in_vain: usize,
+ /// The amount of haves we should send in this round.
+ ///
+ /// If the value is lower than `haves_sent` (the `HAVE` lines actually sent), the negotiation algorithm has run out of options
+ /// which typically indicates the end of the negotiation phase.
+ pub haves_to_send: usize,
+ /// If `true`, the server reported, as response to our previous `HAVE`s, that at least one of them is in common by acknowledging it.
+ ///
+ /// This may also lead to the server responding with a pack.
+ pub previous_response_had_at_least_one_in_common: bool,
+ }
+ }
+}
+
/// The progress ids used in during various steps of the fetch operation.
///
/// Note that tagged progress isn't very widely available yet, but support can be improved as needed.
@@ -129,7 +167,7 @@ where
/// Note that at this point, the `transport` should already be configured using the [`transport_mut()`][Self::transport_mut()]
/// method, as it will be consumed here.
///
- /// From there additional properties of the fetch can be adjusted to override the defaults that are configured via gix-config.
+ /// From there additional properties of the fetch can be adjusted to override the defaults that are configured via git-config.
///
/// # Async Experimental
///
diff --git a/vendor/gix/src/remote/connection/fetch/negotiate.rs b/vendor/gix/src/remote/connection/fetch/negotiate.rs
index e94461bab..92a141f6f 100644
--- a/vendor/gix/src/remote/connection/fetch/negotiate.rs
+++ b/vendor/gix/src/remote/connection/fetch/negotiate.rs
@@ -1,12 +1,13 @@
use std::borrow::Cow;
+use gix_date::SecondsSinceUnixEpoch;
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>;
+type Queue = gix_revwalk::PriorityQueue<SecondsSinceUnixEpoch, gix_hash::ObjectId>;
/// The error returned during negotiation.
#[derive(Debug, thiserror::Error)]
@@ -15,7 +16,7 @@ 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),
+ LookupCommitInGraph(#[from] gix_revwalk::graph::lookup::commit::Error),
#[error(transparent)]
InitRefsIterator(#[from] crate::reference::iter::init::Error),
#[error(transparent)]
@@ -67,7 +68,9 @@ pub(crate) fn mark_complete_and_common_ref(
graph: &mut gix_negotiate::Graph<'_>,
ref_map: &fetch::RefMap,
shallow: &fetch::Shallow,
+ mapping_is_ignored: impl Fn(&fetch::Mapping) -> bool,
) -> Result<Action, Error> {
+ let _span = gix_trace::detail!("mark_complete_and_common_ref", mappings = ref_map.mappings.len());
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).
@@ -85,9 +88,10 @@ pub(crate) fn mark_complete_and_common_ref(
// 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 cutoff_date = None::<SecondsSinceUnixEpoch>;
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();
+ let mut remote_ref_included: 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();
@@ -97,9 +101,13 @@ pub(crate) fn mark_complete_and_common_ref(
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;
+ // Even for ignored mappings we want to know if the `want` is already present locally, so skip nothing else.
+ if !mapping_is_ignored(mapping) {
+ remote_ref_included[mapping_idx] = true;
+ // 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
@@ -113,11 +121,15 @@ pub(crate) fn mark_complete_and_common_ref(
}
}
- // 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) {
+ } else if remote_ref_target_known
+ .iter()
+ .zip(remote_ref_included)
+ .filter_map(|(known, included)| included.then_some(known))
+ .all(|known| *known)
+ {
return Ok(Action::SkipToRefUpdate);
}
}
@@ -137,51 +149,75 @@ pub(crate) fn mark_complete_and_common_ref(
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)
+ gix_trace::detail!("mark known_common").into_scope(|| -> Result<_, Error> {
+ // 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))
{
- negotiator.known_common(common_id.into(), graph)?;
+ 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)?;
+ }
}
- }
+ Ok(())
+ })?;
// 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)?;
- }
+ gix_trace::detail!("mark tips", num_tips = tips.len()).into_scope(|| -> Result<_, Error> {
+ for tip in tips.iter_unordered() {
+ negotiator.add_tip(*tip, graph)?;
+ }
+ Ok(())
+ })?;
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,
+/// Create a predicate that checks if a refspec mapping should be ignored.
+///
+/// We want to ignore mappings during negotiation if they would be handled implicitly by the server, which is the case
+/// when tags would be sent implicitly due to `Tags::Included`.
+pub(crate) fn make_refmapping_ignore_predicate(
fetch_tags: fetch::Tags,
-) {
+ ref_map: &fetch::RefMap,
+) -> impl Fn(&fetch::Mapping) -> bool + '_ {
// 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();
+ move |mapping| {
+ 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)
+ })
+ }
+}
+/// 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,
+ mapping_is_ignored: impl Fn(&fetch::Mapping) -> bool,
+) {
// 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);
@@ -189,17 +225,9 @@ pub(crate) fn add_wants(
.mappings
.iter()
.zip(mapping_known)
- .filter_map(|(m, known)| (is_shallow || !*known).then_some(m));
+ .filter_map(|(m, known)| (is_shallow || !*known).then_some(m))
+ .filter(|m| !mapping_is_ignored(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 {
@@ -228,13 +256,14 @@ pub(crate) fn add_wants(
fn mark_recent_complete_commits(
queue: &mut Queue,
graph: &mut gix_negotiate::Graph<'_>,
- cutoff: gix_revision::graph::CommitterTimestamp,
+ cutoff: SecondsSinceUnixEpoch,
) -> Result<(), Error> {
+ let _span = gix_trace::detail!("mark_recent_complete", queue_len = queue.len());
while let Some(id) = queue
.peek()
.and_then(|(commit_time, id)| (commit_time >= &cutoff).then_some(*id))
{
- queue.pop();
+ queue.pop_value();
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;
@@ -258,6 +287,7 @@ fn mark_all_refs_in_repo(
queue: &mut Queue,
mark: Flags,
) -> Result<(), Error> {
+ let _span = gix_trace::detail!("mark_all_refs");
for local_ref in repo.references()?.all()?.peeled() {
let local_ref = local_ref?;
let id = local_ref.id().detach();
@@ -280,17 +310,14 @@ fn mark_alternate_complete(
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())
- })
- {
+ let alternates = repo.objects.store_ref().alternate_db_paths()?;
+ let _span = gix_trace::detail!("mark_alternate_refs", num_odb = alternates.len());
+
+ for alternate_repo in alternates.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(())
diff --git a/vendor/gix/src/remote/connection/fetch/receive_pack.rs b/vendor/gix/src/remote/connection/fetch/receive_pack.rs
index 7837a9d3a..18e5ac159 100644
--- a/vendor/gix/src/remote/connection/fetch/receive_pack.rs
+++ b/vendor/gix/src/remote/connection/fetch/receive_pack.rs
@@ -19,17 +19,18 @@ use crate::{
connection::fetch::config,
fetch,
fetch::{
- negotiate, negotiate::Algorithm, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Shallow, Status,
+ negotiate, negotiate::Algorithm, outcome, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage,
+ Shallow, Status,
},
},
- Progress, Repository,
+ Repository,
};
impl<'remote, 'repo, T> Prepare<'remote, 'repo, T>
where
T: Transport,
{
- /// Receive the pack and perform the operation as configured by git via `gix-config` or overridden by various builder methods.
+ /// Receive the pack and perform the operation as configured by git via `git-config` or overridden by various builder methods.
/// Return `Ok(None)` if there was nothing to do because all remote refs are at the same state as they are locally, or `Ok(Some(outcome))`
/// to inform about all the changes that were made.
///
@@ -72,18 +73,28 @@ where
/// - `gitoxide.userAgent` is read to obtain the application user agent for git servers and for HTTP servers as well.
///
#[gix_protocol::maybe_async::maybe_async]
- pub async fn receive<P>(mut self, mut progress: P, should_interrupt: &AtomicBool) -> Result<Outcome, Error>
+ pub async fn receive<P>(self, mut progress: P, should_interrupt: &AtomicBool) -> Result<Outcome, Error>
where
- P: Progress,
+ P: gix_features::progress::NestedProgress,
P::SubProgress: 'static,
{
+ self.receive_inner(&mut progress, should_interrupt).await
+ }
+
+ #[gix_protocol::maybe_async::maybe_async]
+ #[allow(clippy::drop_non_drop)]
+ pub(crate) async fn receive_inner(
+ mut self,
+ progress: &mut dyn crate::DynNestedProgress,
+ should_interrupt: &AtomicBool,
+ ) -> Result<Outcome, Error> {
+ let _span = gix_trace::coarse!("fetch::Prepare::receive()");
let mut con = self.con.take().expect("receive() can only be called once");
let handshake = &self.ref_map.handshake;
let protocol_version = handshake.server_protocol_version;
let fetch = gix_protocol::Command::Fetch;
- let progress = &mut progress;
let repo = con.remote.repo;
let fetch_features = {
let mut f = fetch.default_features(protocol_version, &handshake.capabilities);
@@ -114,6 +125,7 @@ where
});
}
+ let negotiate_span = gix_trace::detail!("negotiate");
let mut negotiator = repo
.config
.resolved
@@ -131,20 +143,20 @@ where
r.objects.unset_object_cache();
r
};
- let mut graph = graph_repo.commit_graph();
+ let mut graph = graph_repo.revision_graph();
let action = negotiate::mark_complete_and_common_ref(
&graph_repo,
negotiator.deref_mut(),
&mut graph,
&self.ref_map,
&self.shallow,
+ negotiate::make_refmapping_ignore_predicate(con.remote.fetch_tags, &self.ref_map),
)?;
let mut previous_response = None::<gix_protocol::fetch::Response>;
- let mut round = 1;
- let mut write_pack_bundle = match &action {
+ let (mut write_pack_bundle, negotiate) = match &action {
negotiate::Action::NoChange | negotiate::Action::SkipToRefUpdate => {
gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
- None
+ (None, None)
}
negotiate::Action::MustNegotiate {
remote_ref_target_known,
@@ -155,17 +167,19 @@ where
&self.ref_map,
remote_ref_target_known,
&self.shallow,
- con.remote.fetch_tags,
+ negotiate::make_refmapping_ignore_predicate(con.remote.fetch_tags, &self.ref_map),
);
+ let mut rounds = Vec::new();
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 {
+ let mut reader = 'negotiation: loop {
+ let _round = gix_trace::detail!("negotiate round", round = rounds.len() + 1);
progress.step();
- progress.set_name(format!("negotiate (round {round})"));
+ progress.set_name(format!("negotiate (round {})", rounds.len() + 1));
let is_done = match negotiate::one_round(
negotiator.deref_mut(),
@@ -181,8 +195,14 @@ where
}
seen_ack |= ack_seen;
in_vain += haves_sent;
+ rounds.push(outcome::negotiate::Round {
+ haves_sent,
+ in_vain,
+ haves_to_send,
+ previous_response_had_at_least_one_in_common: ack_seen,
+ });
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);
+ haves_to_send = gix_negotiate::window_size(is_stateless, Some(haves_to_send));
is_done
}
Err(err) => {
@@ -200,17 +220,17 @@ where
previous_response = Some(response);
if has_pack {
progress.step();
- progress.set_name("receiving pack");
+ progress.set_name("receiving pack".into());
if !sideband_all {
setup_remote_progress(progress, &mut reader, should_interrupt);
}
break 'negotiation reader;
- } else {
- round += 1;
}
};
- drop(graph);
+ let graph = graph.detach();
drop(graph_repo);
+ drop(negotiate_span);
+
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
@@ -234,28 +254,34 @@ where
};
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")),
+ #[cfg(not(feature = "async-network-client"))]
+ let mut rd = reader;
+ #[cfg(feature = "async-network-client")]
+ let mut rd = gix_protocol::futures_lite::io::BlockOn::new(reader);
+ let res = gix_pack::Bundle::write_to_directory(
+ &mut rd,
+ 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()
+ move |oid, buf| repo.objects.find(&oid, buf).ok()
})),
options,
- )?)
+ )?;
+ #[cfg(feature = "async-network-client")]
+ {
+ reader = rd.into_inner();
+ }
+ #[cfg(not(feature = "async-network-client"))]
+ {
+ reader = rd;
+ }
+ Some(res)
} else {
- drop(reader);
None
};
+ drop(reader);
if matches!(protocol_version, gix_protocol::transport::Protocol::V2) {
gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
@@ -266,7 +292,7 @@ where
crate::shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates())?;
}
}
- write_pack_bundle
+ (write_pack_bundle, Some(outcome::Negotiate { graph, rounds }))
}
};
@@ -293,21 +319,17 @@ where
let out = Outcome {
ref_map: std::mem::take(&mut self.ref_map),
- 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 {
+ status: match write_pack_bundle {
+ Some(write_pack_bundle) => Status::Change {
+ write_pack_bundle,
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 },
- }
+ 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)
@@ -348,14 +370,14 @@ fn add_shallow_args(
args.deepen_relative();
}
Shallow::Since { cutoff } => {
- args.deepen_since(cutoff.seconds_since_unix_epoch as usize);
+ args.deepen_since(cutoff.seconds);
}
Shallow::Exclude {
remote_refs,
since_cutoff,
} => {
if let Some(cutoff) = since_cutoff {
- args.deepen_since(cutoff.seconds_since_unix_epoch as usize);
+ args.deepen_since(cutoff.seconds);
}
for ref_ in remote_refs {
args.deepen_not(ref_.as_ref().as_bstr());
@@ -365,17 +387,14 @@ fn add_shallow_args(
Ok((shallow_commits, shallow_lock))
}
-fn setup_remote_progress<P>(
- progress: &mut P,
+fn setup_remote_progress(
+ progress: &mut dyn crate::DynNestedProgress,
reader: &mut Box<dyn gix_protocol::transport::client::ExtendedBufRead + Unpin + '_>,
should_interrupt: &AtomicBool,
-) where
- P: Progress,
- P::SubProgress: 'static,
-{
+) {
use gix_protocol::transport::client::ExtendedBufRead;
reader.set_progress_handler(Some(Box::new({
- let mut remote_progress = progress.add_child_with_id("remote", ProgressId::RemoteProgress.into());
+ let mut remote_progress = progress.add_child_with_id("remote".to_string(), ProgressId::RemoteProgress.into());
// SAFETY: Ugh, so, with current Rust I can't declare lifetimes in the involved traits the way they need to
// be and I also can't use scoped threads to pump from local scopes to an Arc version that could be
// used here due to the this being called from sync AND async code (and the async version doesn't work
diff --git a/vendor/gix/src/remote/connection/fetch/update_refs/mod.rs b/vendor/gix/src/remote/connection/fetch/update_refs/mod.rs
index 953490672..3d6fb18bd 100644
--- a/vendor/gix/src/remote/connection/fetch/update_refs/mod.rs
+++ b/vendor/gix/src/remote/connection/fetch/update_refs/mod.rs
@@ -11,7 +11,10 @@ use crate::{
ext::ObjectIdExt,
remote::{
fetch,
- fetch::{refs::update::Mode, RefLogMessage, Source},
+ fetch::{
+ refs::update::{Mode, TypeChange},
+ RefLogMessage, Source,
+ },
},
Repository,
};
@@ -23,14 +26,20 @@ pub mod update;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Update {
/// The way the update was performed.
- pub mode: update::Mode,
+ pub mode: Mode,
+ /// If not `None`, the update also affects the type of the reference. This also implies that `edit_index` is not None.
+ pub type_change: Option<TypeChange>,
/// The index to the edit that was created from the corresponding mapping, or `None` if there was no local ref.
pub edit_index: Option<usize>,
}
-impl From<update::Mode> for Update {
+impl From<Mode> for Update {
fn from(mode: Mode) -> Self {
- Update { mode, edit_index: None }
+ Update {
+ mode,
+ type_change: None,
+ edit_index: None,
+ }
}
}
@@ -42,6 +51,14 @@ impl From<update::Mode> for Update {
/// `action` is the prefix used for reflog entries, and is typically "fetch".
///
/// It can be used to produce typical information that one is used to from `git fetch`.
+///
+/// We will reject updates only if…
+///
+/// * …fast-forward rules are violated
+/// * …the local ref is currently checked out
+/// * …existing refs would not become 'unborn', i.e. point to a reference that doesn't exist and won't be created due to ref-specs
+///
+/// With these safeguards in place, one can handle each naturally and implement mirrors or bare repos easily.
#[allow(clippy::too_many_arguments)]
pub(crate) fn update(
repo: &Repository,
@@ -53,8 +70,10 @@ pub(crate) fn update(
dry_run: fetch::DryRun,
write_packed_refs: fetch::WritePackedRefs,
) -> Result<update::Outcome, update::Error> {
+ let _span = gix_trace::detail!("update_refs()", mappings = mappings.len());
let mut edits = Vec::new();
let mut updates = Vec::new();
+ let mut edit_indices_to_validate = Vec::new();
let implicit_tag_refspec = fetch_tags
.to_refspec()
@@ -75,46 +94,56 @@ pub(crate) fn update(
})
},
) {
- let remote_id = match remote.as_id() {
- Some(id) => id,
- None => continue,
- };
- if dry_run == fetch::DryRun::No && !repo.objects.contains(remote_id) {
- let update = if is_implicit_tag {
- update::Mode::ImplicitTagNotSentByRemote.into()
- } else {
- update::Mode::RejectedSourceObjectNotFound { id: remote_id.into() }.into()
- };
- updates.push(update);
- continue;
+ // `None` only if unborn.
+ let remote_id = remote.as_id();
+ if matches!(dry_run, fetch::DryRun::No) && !remote_id.map_or(true, |id| repo.objects.contains(id)) {
+ if let Some(remote_id) = remote_id.filter(|id| !repo.objects.contains(id)) {
+ let update = if is_implicit_tag {
+ Mode::ImplicitTagNotSentByRemote.into()
+ } else {
+ Mode::RejectedSourceObjectNotFound { id: remote_id.into() }.into()
+ };
+ updates.push(update);
+ continue;
+ }
}
- let checked_out_branches = worktree_branches(repo)?;
- let (mode, edit_index) = match local {
+ let mut checked_out_branches = worktree_branches(repo)?;
+ let (mode, edit_index, type_change) = match local {
Some(name) => {
let (mode, reflog_message, name, previous_value) = match repo.try_find_reference(name)? {
Some(existing) => {
- if let Some(wt_dir) = checked_out_branches.get(existing.name()) {
- let mode = update::Mode::RejectedCurrentlyCheckedOut {
- worktree_dir: wt_dir.to_owned(),
+ if let Some(wt_dirs) = checked_out_branches.get_mut(existing.name()) {
+ wt_dirs.sort();
+ wt_dirs.dedup();
+ let mode = Mode::RejectedCurrentlyCheckedOut {
+ worktree_dirs: wt_dirs.to_owned(),
};
updates.push(mode.into());
continue;
}
- match existing.target() {
- TargetRef::Symbolic(_) => {
- updates.push(update::Mode::RejectedSymbolic.into());
- continue;
- }
- TargetRef::Peeled(local_id) => {
- let previous_value =
- PreviousValue::MustExistAndMatch(Target::Peeled(local_id.to_owned()));
+
+ match existing
+ .try_id()
+ .map_or_else(|| existing.clone().peel_to_id_in_place(), Ok)
+ .map(crate::Id::detach)
+ {
+ Ok(local_id) => {
+ let remote_id = match remote_id {
+ Some(id) => id,
+ None => {
+ // we don't allow to go back to unborn state if there is a local reference already present.
+ // Note that we will be changing it to a symbolic reference just fine.
+ updates.push(Mode::RejectedToReplaceWithUnborn.into());
+ continue;
+ }
+ };
let (mode, reflog_message) = if local_id == remote_id {
- (update::Mode::NoChangeNeeded, "no update will be performed")
+ (Mode::NoChangeNeeded, "no update will be performed")
} else if let Some(gix_ref::Category::Tag) = existing.name().category() {
if spec.allow_non_fast_forward() {
- (update::Mode::Forced, "updating tag")
+ (Mode::Forced, "updating tag")
} else {
- updates.push(update::Mode::RejectedTagUpdate.into());
+ updates.push(Mode::RejectedTagUpdate.into());
continue;
}
} else {
@@ -126,21 +155,21 @@ pub(crate) fn update(
.try_into_commit()
.map_err(|_| ())
.and_then(|c| {
- c.committer().map(|a| a.time.seconds_since_unix_epoch).map_err(|_| ())
+ c.committer().map(|a| a.time.seconds).map_err(|_| ())
}).and_then(|local_commit_time|
- remote_id
- .to_owned()
- .ancestors(|id, buf| repo.objects.find_commit_iter(id, buf))
- .sorting(
- gix_traverse::commit::Sorting::ByCommitTimeNewestFirstCutoffOlderThan {
- time_in_seconds_since_epoch: local_commit_time
- },
- )
- .map_err(|_| ())
- );
+ remote_id
+ .to_owned()
+ .ancestors(|id, buf| repo.objects.find_commit_iter(id, buf))
+ .sorting(
+ gix_traverse::commit::Sorting::ByCommitTimeNewestFirstCutoffOlderThan {
+ seconds: local_commit_time
+ },
+ )
+ .map_err(|_| ())
+ );
match ancestors {
Ok(mut ancestors) => {
- ancestors.any(|cid| cid.map_or(false, |cid| cid == local_id))
+ ancestors.any(|cid| cid.map_or(false, |c| c.id == local_id))
}
Err(_) => {
force = true;
@@ -152,20 +181,41 @@ pub(crate) fn update(
};
if is_fast_forward {
(
- update::Mode::FastForward,
+ Mode::FastForward,
matches!(dry_run, fetch::DryRun::Yes)
.then(|| "fast-forward (guessed in dry-run)")
.unwrap_or("fast-forward"),
)
} else if force {
- (update::Mode::Forced, "forced-update")
+ (Mode::Forced, "forced-update")
} else {
- updates.push(update::Mode::RejectedNonFastForward.into());
+ updates.push(Mode::RejectedNonFastForward.into());
continue;
}
};
- (mode, reflog_message, existing.name().to_owned(), previous_value)
+ (
+ mode,
+ reflog_message,
+ existing.name().to_owned(),
+ PreviousValue::MustExistAndMatch(existing.target().into_owned()),
+ )
}
+ Err(crate::reference::peel::Error::ToId(gix_ref::peel::to_id::Error::Follow(_))) => {
+ // An unborn reference, always allow it to be changed to whatever the remote wants.
+ (
+ if existing.target().try_name().map(gix_ref::FullNameRef::as_bstr)
+ == remote.as_target()
+ {
+ Mode::NoChangeNeeded
+ } else {
+ Mode::Forced
+ },
+ "change unborn ref",
+ existing.name().to_owned(),
+ PreviousValue::MustExistAndMatch(existing.target().into_owned()),
+ )
+ }
+ Err(err) => return Err(err.into()),
}
}
None => {
@@ -176,13 +226,37 @@ pub(crate) fn update(
_ => "storing ref",
};
(
- update::Mode::New,
+ Mode::New,
reflog_msg,
name,
- PreviousValue::ExistingMustMatch(Target::Peeled(remote_id.to_owned())),
+ PreviousValue::ExistingMustMatch(new_value_by_remote(repo, remote, mappings)?),
)
}
};
+
+ let new = new_value_by_remote(repo, remote, mappings)?;
+ let type_change = match (&previous_value, &new) {
+ (
+ PreviousValue::ExistingMustMatch(Target::Peeled(_))
+ | PreviousValue::MustExistAndMatch(Target::Peeled(_)),
+ Target::Symbolic(_),
+ ) => Some(TypeChange::DirectToSymbolic),
+ (
+ PreviousValue::ExistingMustMatch(Target::Symbolic(_))
+ | PreviousValue::MustExistAndMatch(Target::Symbolic(_)),
+ Target::Peeled(_),
+ ) => Some(TypeChange::SymbolicToDirect),
+ _ => None,
+ };
+ // We are here because this edit should work and fast-forward rules are respected.
+ // But for setting a symref-target, we have to be sure that the target already exists
+ // or will exists. To be sure all rules are respected, we delay the check to when the
+ // edit-list has been built.
+ let edit_index = edits.len();
+ if matches!(new, Target::Symbolic(_)) {
+ let anticipated_update_index = updates.len();
+ edit_indices_to_validate.push((anticipated_update_index, edit_index));
+ }
let edit = RefEdit {
change: Change::Update {
log: LogChange {
@@ -191,42 +265,57 @@ pub(crate) fn update(
message: message.compose(reflog_message),
},
expected: previous_value,
- new: if let Source::Ref(gix_protocol::handshake::Ref::Symbolic { target, .. }) = &remote {
- match mappings.iter().find_map(|m| {
- m.remote.as_name().and_then(|name| {
- (name == target)
- .then(|| m.local.as_ref().and_then(|local| local.try_into().ok()))
- .flatten()
- })
- }) {
- Some(local_branch) => {
- // This is always safe because…
- // - the reference may exist already
- // - if it doesn't exist it will be created - we are here because it's in the list of mappings after all
- // - if it exists and is updated, and the update is rejected due to non-fastforward for instance, the
- // target reference still exists and we can point to it.
- Target::Symbolic(local_branch)
- }
- None => Target::Peeled(remote_id.into()),
- }
- } else {
- Target::Peeled(remote_id.into())
- },
+ new,
},
name,
+ // We must not deref symrefs or we will overwrite their destination, which might be checked out
+ // and we don't check for that case.
deref: false,
};
- let edit_index = edits.len();
edits.push(edit);
- (mode, Some(edit_index))
+ (mode, Some(edit_index), type_change)
}
- None => (update::Mode::NoChangeNeeded, None),
+ None => (Mode::NoChangeNeeded, None, None),
};
- updates.push(Update { mode, edit_index })
+ updates.push(Update {
+ mode,
+ type_change,
+ edit_index,
+ })
+ }
+
+ for (update_index, edit_index) in edit_indices_to_validate {
+ let edit = &edits[edit_index];
+ if update_needs_adjustment_as_edits_symbolic_target_is_missing(edit, repo, &edits) {
+ let edit = &mut edits[edit_index];
+ let update = &mut updates[update_index];
+
+ update.mode = Mode::RejectedToReplaceWithUnborn;
+ update.type_change = None;
+
+ match edit.change {
+ Change::Update {
+ ref expected,
+ ref mut new,
+ ref mut log,
+ ..
+ } => match expected {
+ PreviousValue::MustExistAndMatch(existing) => {
+ *new = existing.clone();
+ log.message = "no-op".into();
+ }
+ _ => unreachable!("at this point it can only be one variant"),
+ },
+ Change::Delete { .. } => {
+ unreachable!("we don't do that here")
+ }
+ };
+ }
}
let edits = match dry_run {
fetch::DryRun::No => {
+ let _span = gix_trace::detail!("apply", edits = edits.len());
let (file_lock_fail, packed_refs_lock_fail) = repo
.config
.lock_timeout()
@@ -238,9 +327,8 @@ pub(crate) fn update(
fetch::WritePackedRefs::Only => {
gix_ref::file::transaction::PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(Box::new(|oid, buf| {
repo.objects
- .try_find(oid, buf)
+ .try_find(&oid, buf)
.map(|obj| obj.map(|obj| obj.kind))
- .map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)
}))},
fetch::WritePackedRefs::Never => gix_ref::file::transaction::PackedRefs::DeletionsOnly
}
@@ -256,16 +344,128 @@ pub(crate) fn update(
Ok(update::Outcome { edits, updates })
}
-fn worktree_branches(repo: &Repository) -> Result<BTreeMap<gix_ref::FullName, PathBuf>, update::Error> {
- let mut map = BTreeMap::new();
- if let Some((wt_dir, head_ref)) = repo.work_dir().zip(repo.head_ref().ok().flatten()) {
- map.insert(head_ref.inner.name, wt_dir.to_owned());
+/// Figure out if target of `edit` points to a reference that doesn't exist in `repo` and won't exist as it's not in any of `edits`.
+/// If so, return true.
+fn update_needs_adjustment_as_edits_symbolic_target_is_missing(
+ edit: &RefEdit,
+ repo: &Repository,
+ edits: &[RefEdit],
+) -> bool {
+ match edit.change.new_value().expect("here we need a symlink") {
+ TargetRef::Peeled(_) => unreachable!("BUG: we already know it's symbolic"),
+ TargetRef::Symbolic(new_target_ref) => {
+ match &edit.change {
+ Change::Update { expected, .. } => match expected {
+ PreviousValue::MustExistAndMatch(current_target) => {
+ if let Target::Symbolic(current_target_name) = current_target {
+ if current_target_name.as_ref() == new_target_ref {
+ return false; // no-op are always fine
+ }
+ let current_is_unborn = repo.refs.try_find(current_target_name).ok().flatten().is_none();
+ if current_is_unborn {
+ return false;
+ }
+ }
+ }
+ PreviousValue::ExistingMustMatch(_) => return false, // this means the ref doesn't exist locally, so we can create unborn refs anyway
+ _ => {
+ unreachable!("BUG: we don't do that here")
+ }
+ },
+ Change::Delete { .. } => {
+ unreachable!("we don't ever delete here")
+ }
+ };
+ let target_ref_exists_locally = repo.refs.try_find(new_target_ref).ok().flatten().is_some();
+ if target_ref_exists_locally {
+ return false;
+ }
+
+ let target_ref_will_be_created = edits.iter().any(|edit| edit.name.as_ref() == new_target_ref);
+ !target_ref_will_be_created
+ }
}
+}
+
+fn new_value_by_remote(
+ repo: &Repository,
+ remote: &Source,
+ mappings: &[fetch::Mapping],
+) -> Result<Target, update::Error> {
+ let remote_id = remote.as_id();
+ Ok(
+ if let Source::Ref(
+ gix_protocol::handshake::Ref::Symbolic { target, .. } | gix_protocol::handshake::Ref::Unborn { target, .. },
+ ) = &remote
+ {
+ match mappings.iter().find_map(|m| {
+ m.remote.as_name().and_then(|name| {
+ (name == target)
+ .then(|| m.local.as_ref().and_then(|local| local.try_into().ok()))
+ .flatten()
+ })
+ }) {
+ // Map the target on the remote to the local branch name, which should be covered by refspecs.
+ Some(local_branch) => {
+ // This is always safe because…
+ // - the reference may exist already
+ // - if it doesn't exist it will be created - we are here because it's in the list of mappings after all
+ // - if it exists and is updated, and the update is rejected due to non-fastforward for instance, the
+ // target reference still exists and we can point to it.
+ Target::Symbolic(local_branch)
+ }
+ None => {
+ // If we can't map it, it's usually a an unborn branch causing this, or a the target isn't covered
+ // by any refspec so we don't officially pull it in.
+ match remote_id {
+ Some(desired_id) => {
+ if repo.try_find_reference(target)?.is_some() {
+ // We are allowed to change a direct reference to a symbolic one, which may point to other objects
+ // than the remote. The idea is that we are fine as long as the resulting refs are valid.
+ Target::Symbolic(target.try_into()?)
+ } else {
+ // born branches that we don't have in our refspecs we create peeled. That way they can be used.
+ Target::Peeled(desired_id.to_owned())
+ }
+ }
+ // Unborn branches we create as such, with the location they point to on the remote which helps mirroring.
+ None => Target::Symbolic(target.try_into()?),
+ }
+ }
+ }
+ } else {
+ Target::Peeled(remote_id.expect("unborn case handled earlier").to_owned())
+ },
+ )
+}
+
+fn insert_head(
+ head: Option<crate::Head<'_>>,
+ out: &mut BTreeMap<gix_ref::FullName, Vec<PathBuf>>,
+) -> Result<(), update::Error> {
+ if let Some((head, wd)) = head.and_then(|head| head.repo.work_dir().map(|wd| (head, wd))) {
+ out.entry("HEAD".try_into().expect("valid"))
+ .or_default()
+ .push(wd.to_owned());
+ let mut ref_chain = Vec::new();
+ let mut cursor = head.try_into_referent();
+ while let Some(ref_) = cursor {
+ ref_chain.push(ref_.name().to_owned());
+ cursor = ref_.follow().transpose()?;
+ }
+ for name in ref_chain {
+ out.entry(name).or_default().push(wd.to_owned());
+ }
+ }
+ Ok(())
+}
+
+fn worktree_branches(repo: &Repository) -> Result<BTreeMap<gix_ref::FullName, Vec<PathBuf>>, update::Error> {
+ let mut map = BTreeMap::new();
+ insert_head(repo.head().ok(), &mut map)?;
for proxy in repo.worktrees()? {
let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?;
- if let Some((wt_dir, head_ref)) = repo.work_dir().zip(repo.head_ref().ok().flatten()) {
- map.insert(head_ref.inner.name, wt_dir.to_owned());
- }
+ insert_head(repo.head().ok(), &mut map)?;
}
Ok(map)
}
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 47ab5d1a5..0b29f14f4 100644
--- a/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs
+++ b/vendor/gix/src/remote/connection/fetch/update_refs/tests.rs
@@ -31,6 +31,10 @@ mod update {
gix_testtools::scripted_fixture_read_only_with_args("make_fetch_repos.sh", [base_repo_path()]).unwrap();
gix::open_opts(dir.join(name), restricted()).unwrap()
}
+ fn named_repo(name: &str) -> gix::Repository {
+ let dir = gix_testtools::scripted_fixture_read_only("make_remote_repos.sh").unwrap();
+ gix::open_opts(dir.join(name), restricted()).unwrap()
+ }
fn repo_rw(name: &str) -> (gix::Repository, gix_testtools::tempfile::TempDir) {
let dir = gix_testtools::scripted_fixture_writable_with_args(
"make_fetch_repos.sh",
@@ -41,13 +45,19 @@ mod update {
let repo = gix::open_opts(dir.path().join(name), restricted()).unwrap();
(repo, dir)
}
- use gix_ref::{transaction::Change, TargetRef};
+ use gix_ref::{
+ transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},
+ Target, TargetRef,
+ };
use crate::{
bstr::BString,
remote::{
fetch,
- fetch::{refs::tests::restricted, Mapping, RefLogMessage, Source, SpecIndex},
+ fetch::{
+ refs::{tests::restricted, update::TypeChange},
+ Mapping, RefLogMessage, Source, SpecIndex,
+ },
},
};
@@ -112,7 +122,7 @@ mod update {
(
"+refs/remotes/origin/g:refs/heads/main",
fetch::refs::update::Mode::RejectedCurrentlyCheckedOut {
- worktree_dir: repo.work_dir().expect("present").to_owned(),
+ worktree_dirs: vec![repo.work_dir().expect("present").to_owned()],
},
None,
"checked out branches cannot be written, as it requires a merge of sorts which isn't done here",
@@ -148,6 +158,7 @@ mod update {
assert_eq!(
out.updates,
vec![fetch::refs::Update {
+ type_change: None,
mode: expected_mode.clone(),
edit_index: reflog_message.map(|_| 0),
}],
@@ -180,7 +191,7 @@ mod update {
#[test]
fn checked_out_branches_in_worktrees_are_rejected_with_additional_information() -> Result {
- let root = gix_path::realpath(gix_testtools::scripted_fixture_read_only_with_args(
+ let root = gix_path::realpath(&gix_testtools::scripted_fixture_read_only_with_args(
"make_fetch_repos.sh",
[base_repo_path()],
)?)?;
@@ -211,8 +222,9 @@ mod update {
out.updates,
vec![fetch::refs::Update {
mode: fetch::refs::update::Mode::RejectedCurrentlyCheckedOut {
- worktree_dir: root.join(path_from_root),
+ worktree_dirs: vec![root.join(path_from_root)],
},
+ type_change: None,
edit_index: None,
}],
"{spec}: checked-out checks are done before checking if a change would actually be required (here it isn't)"
@@ -223,10 +235,350 @@ mod update {
}
#[test]
- fn local_symbolic_refs_are_never_written() {
+ fn unborn_remote_branches_can_be_created_locally_if_they_are_new() -> Result {
+ let repo = named_repo("unborn");
+ let (mappings, specs) = mapping_from_spec("HEAD:refs/remotes/origin/HEAD", &repo);
+ assert_eq!(mappings.len(), 1);
+ let out = fetch::refs::update(
+ &repo,
+ prefixed("action"),
+ &mappings,
+ &specs,
+ &[],
+ fetch::Tags::None,
+ fetch::DryRun::Yes,
+ fetch::WritePackedRefs::Never,
+ )?;
+ assert_eq!(
+ out.updates,
+ vec![fetch::refs::Update {
+ mode: fetch::refs::update::Mode::New,
+ type_change: None,
+ edit_index: Some(0)
+ }]
+ );
+ assert_eq!(out.edits.len(), 1, "we are OK with creating unborn refs");
+ Ok(())
+ }
+
+ #[test]
+ fn unborn_remote_branches_can_update_local_unborn_branches() -> Result {
+ let repo = named_repo("unborn");
+ let (mappings, specs) = mapping_from_spec("HEAD:refs/heads/existing-unborn-symbolic", &repo);
+ assert_eq!(mappings.len(), 1);
+ let out = fetch::refs::update(
+ &repo,
+ prefixed("action"),
+ &mappings,
+ &specs,
+ &[],
+ fetch::Tags::None,
+ fetch::DryRun::Yes,
+ fetch::WritePackedRefs::Never,
+ )?;
+ assert_eq!(
+ out.updates,
+ vec![fetch::refs::Update {
+ mode: fetch::refs::update::Mode::NoChangeNeeded,
+ type_change: None,
+ edit_index: Some(0)
+ }]
+ );
+ assert_eq!(out.edits.len(), 1, "we are OK with updating unborn refs");
+ assert_eq!(
+ out.edits[0],
+ RefEdit {
+ change: Change::Update {
+ log: LogChange {
+ mode: RefLog::AndReference,
+ force_create_reflog: false,
+ message: "action: change unborn ref".into(),
+ },
+ expected: PreviousValue::MustExistAndMatch(Target::Symbolic(
+ "refs/heads/main".try_into().expect("valid"),
+ )),
+ new: Target::Symbolic("refs/heads/main".try_into().expect("valid")),
+ },
+ name: "refs/heads/existing-unborn-symbolic".try_into().expect("valid"),
+ deref: false,
+ }
+ );
+
+ let (mappings, specs) = mapping_from_spec("HEAD:refs/heads/existing-unborn-symbolic-other", &repo);
+ assert_eq!(mappings.len(), 1);
+ let out = fetch::refs::update(
+ &repo,
+ prefixed("action"),
+ &mappings,
+ &specs,
+ &[],
+ fetch::Tags::None,
+ fetch::DryRun::Yes,
+ fetch::WritePackedRefs::Never,
+ )?;
+ assert_eq!(
+ out.updates,
+ vec![fetch::refs::Update {
+ mode: fetch::refs::update::Mode::Forced,
+ type_change: None,
+ edit_index: Some(0)
+ }]
+ );
+ assert_eq!(
+ out.edits.len(),
+ 1,
+ "we are OK with creating unborn refs even without actually forcing it"
+ );
+ assert_eq!(
+ out.edits[0],
+ RefEdit {
+ change: Change::Update {
+ log: LogChange {
+ mode: RefLog::AndReference,
+ force_create_reflog: false,
+ message: "action: change unborn ref".into(),
+ },
+ expected: PreviousValue::MustExistAndMatch(Target::Symbolic(
+ "refs/heads/other".try_into().expect("valid"),
+ )),
+ new: Target::Symbolic("refs/heads/main".try_into().expect("valid")),
+ },
+ name: "refs/heads/existing-unborn-symbolic-other".try_into().expect("valid"),
+ deref: false,
+ }
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn remote_symbolic_refs_with_locally_unavailable_target_result_in_valid_peeled_branches() -> Result {
+ let remote_repo = named_repo("one-commit-with-symref");
+ let local_repo = named_repo("unborn");
+ let (mappings, specs) = mapping_from_spec("refs/heads/symbolic:refs/heads/new", &remote_repo);
+ assert_eq!(mappings.len(), 1);
+
+ let out = fetch::refs::update(
+ &local_repo,
+ prefixed("action"),
+ &mappings,
+ &specs,
+ &[],
+ fetch::Tags::None,
+ fetch::DryRun::Yes,
+ fetch::WritePackedRefs::Never,
+ )?;
+ assert_eq!(
+ out.updates,
+ vec![fetch::refs::Update {
+ mode: fetch::refs::update::Mode::New,
+ type_change: None,
+ edit_index: Some(0)
+ }]
+ );
+ assert_eq!(out.edits.len(), 1);
+ let target = Target::Peeled(hex_to_id("66f16e4e8baf5c77bb6d0484495bebea80e916ce"));
+ assert_eq!(
+ out.edits[0],
+ RefEdit {
+ change: Change::Update {
+ log: LogChange {
+ mode: RefLog::AndReference,
+ force_create_reflog: false,
+ message: "action: storing head".into(),
+ },
+ expected: PreviousValue::ExistingMustMatch(target.clone()),
+ new: target,
+ },
+ name: "refs/heads/new".try_into().expect("valid"),
+ deref: false,
+ },
+ "we create local-refs whose targets aren't present yet, even though the remote knows them.\
+ This leaves the caller with assuring all refs are mentioned in mappings."
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn remote_symbolic_refs_with_locally_unavailable_target_dont_overwrite_valid_local_branches() -> Result {
+ let remote_repo = named_repo("one-commit-with-symref");
+ let local_repo = named_repo("one-commit-with-symref-missing-branch");
+ let (mappings, specs) = mapping_from_spec("refs/heads/unborn:refs/heads/valid-locally", &remote_repo);
+ assert_eq!(mappings.len(), 1);
+
+ let out = fetch::refs::update(
+ &local_repo,
+ prefixed("action"),
+ &mappings,
+ &specs,
+ &[],
+ fetch::Tags::None,
+ fetch::DryRun::Yes,
+ fetch::WritePackedRefs::Never,
+ )?;
+ assert_eq!(
+ out.updates,
+ vec![fetch::refs::Update {
+ mode: fetch::refs::update::Mode::RejectedToReplaceWithUnborn,
+ type_change: None,
+ edit_index: None
+ }]
+ );
+ assert_eq!(out.edits.len(), 0);
+ Ok(())
+ }
+
+ #[test]
+ fn unborn_remote_refs_dont_overwrite_valid_local_refs() -> Result {
+ let remote_repo = named_repo("unborn");
+ let local_repo = named_repo("one-commit-with-symref");
+ let (mappings, specs) =
+ mapping_from_spec("refs/heads/existing-unborn-symbolic:refs/heads/branch", &remote_repo);
+ assert_eq!(mappings.len(), 1);
+
+ let out = fetch::refs::update(
+ &local_repo,
+ prefixed("action"),
+ &mappings,
+ &specs,
+ &[],
+ fetch::Tags::None,
+ fetch::DryRun::Yes,
+ fetch::WritePackedRefs::Never,
+ )?;
+ assert_eq!(
+ out.updates,
+ vec![fetch::refs::Update {
+ mode: fetch::refs::update::Mode::RejectedToReplaceWithUnborn,
+ type_change: None,
+ edit_index: None
+ }],
+ "we don't overwrite locally present refs with unborn ones for safety"
+ );
+ assert_eq!(out.edits.len(), 0);
+ Ok(())
+ }
+
+ #[test]
+ fn local_symbolic_refs_can_be_overwritten() {
let repo = repo("two-origins");
- for source in ["refs/heads/main", "refs/heads/symbolic", "HEAD"] {
- let (mappings, specs) = mapping_from_spec(&format!("{source}:refs/heads/symbolic"), &repo);
+ for (source, destination, expected_update, expected_edit) in [
+ (
+ // attempt to overwrite HEAD isn't possible as the matching engine will normalize the path. That way, `HEAD`
+ // can never be set. This is by design (of git) and we follow it.
+ "refs/heads/symbolic",
+ "HEAD",
+ fetch::refs::Update {
+ mode: fetch::refs::update::Mode::New,
+ type_change: None,
+ edit_index: Some(0),
+ },
+ Some(RefEdit {
+ change: Change::Update {
+ log: LogChange {
+ mode: RefLog::AndReference,
+ force_create_reflog: false,
+ message: "action: storing head".into(),
+ },
+ expected: PreviousValue::ExistingMustMatch(Target::Symbolic(
+ "refs/heads/main".try_into().expect("valid"),
+ )),
+ new: Target::Symbolic("refs/heads/main".try_into().expect("valid")),
+ },
+ name: "refs/heads/HEAD".try_into().expect("valid"),
+ deref: false,
+ }),
+ ),
+ (
+ // attempt to overwrite checked out branch fails
+ "refs/remotes/origin/b", // strange, but the remote-refs are simulated and based on local refs
+ "refs/heads/main",
+ fetch::refs::Update {
+ mode: fetch::refs::update::Mode::RejectedCurrentlyCheckedOut {
+ worktree_dirs: vec![repo.work_dir().expect("present").to_owned()],
+ },
+ type_change: None,
+ edit_index: None,
+ },
+ None,
+ ),
+ (
+ // symbolic becomes direct
+ "refs/heads/main",
+ "refs/heads/symbolic",
+ fetch::refs::Update {
+ mode: fetch::refs::update::Mode::NoChangeNeeded,
+ type_change: Some(TypeChange::SymbolicToDirect),
+ edit_index: Some(0),
+ },
+ Some(RefEdit {
+ change: Change::Update {
+ log: LogChange {
+ mode: RefLog::AndReference,
+ force_create_reflog: false,
+ message: "action: no update will be performed".into(),
+ },
+ expected: PreviousValue::MustExistAndMatch(Target::Symbolic(
+ "refs/heads/main".try_into().expect("valid"),
+ )),
+ new: Target::Peeled(hex_to_id("f99771fe6a1b535783af3163eba95a927aae21d5")),
+ },
+ name: "refs/heads/symbolic".try_into().expect("valid"),
+ deref: false,
+ }),
+ ),
+ (
+ // direct becomes symbolic
+ "refs/heads/symbolic",
+ "refs/remotes/origin/a",
+ fetch::refs::Update {
+ mode: fetch::refs::update::Mode::NoChangeNeeded,
+ type_change: Some(TypeChange::DirectToSymbolic),
+ edit_index: Some(0),
+ },
+ Some(RefEdit {
+ change: Change::Update {
+ log: LogChange {
+ mode: RefLog::AndReference,
+ force_create_reflog: false,
+ message: "action: no update will be performed".into(),
+ },
+ expected: PreviousValue::MustExistAndMatch(Target::Peeled(hex_to_id(
+ "f99771fe6a1b535783af3163eba95a927aae21d5",
+ ))),
+ new: Target::Symbolic("refs/heads/main".try_into().expect("valid")),
+ },
+ name: "refs/remotes/origin/a".try_into().expect("valid"),
+ deref: false,
+ }),
+ ),
+ (
+ // symbolic to symbolic (same)
+ "refs/heads/symbolic",
+ "refs/heads/symbolic",
+ fetch::refs::Update {
+ mode: fetch::refs::update::Mode::NoChangeNeeded,
+ type_change: None,
+ edit_index: Some(0),
+ },
+ Some(RefEdit {
+ change: Change::Update {
+ log: LogChange {
+ mode: RefLog::AndReference,
+ force_create_reflog: false,
+ message: "action: no update will be performed".into(),
+ },
+ expected: PreviousValue::MustExistAndMatch(Target::Symbolic(
+ "refs/heads/main".try_into().expect("valid"),
+ )),
+ new: Target::Symbolic("refs/heads/main".try_into().expect("valid")),
+ },
+ name: "refs/heads/symbolic".try_into().expect("valid"),
+ deref: false,
+ }),
+ ),
+ ] {
+ let (mappings, specs) = mapping_from_spec(&format!("{source}:{destination}"), &repo);
+ assert_eq!(mappings.len(), 1);
let out = fetch::refs::update(
&repo,
prefixed("action"),
@@ -239,15 +591,11 @@ mod update {
)
.unwrap();
- assert_eq!(out.edits.len(), 0);
- assert_eq!(
- out.updates,
- vec![fetch::refs::Update {
- mode: fetch::refs::update::Mode::RejectedSymbolic,
- edit_index: None
- }],
- "we don't overwrite these as the checked-out check needs to consider much more than it currently does, we are playing it safe"
- );
+ assert_eq!(out.edits.len(), usize::from(expected_edit.is_some()));
+ assert_eq!(out.updates, vec![expected_update]);
+ if let Some(expected) = expected_edit {
+ assert_eq!(out.edits, vec![expected]);
+ }
}
}
@@ -275,17 +623,19 @@ mod update {
)
.unwrap();
- assert_eq!(out.edits.len(), 1);
+ assert_eq!(out.edits.len(), 2, "symbolic refs are handled just like any other ref");
assert_eq!(
out.updates,
vec![
fetch::refs::Update {
mode: fetch::refs::update::Mode::New,
+ type_change: None,
edit_index: Some(0)
},
fetch::refs::Update {
- mode: fetch::refs::update::Mode::RejectedSymbolic,
- edit_index: None
+ mode: fetch::refs::update::Mode::NoChangeNeeded,
+ type_change: Some(TypeChange::SymbolicToDirect),
+ edit_index: Some(1)
}
],
);
@@ -303,7 +653,7 @@ mod update {
}
#[test]
- fn local_direct_refs_are_never_written_with_symbolic_ones_but_see_only_the_destination() {
+ fn local_direct_refs_are_written_with_symbolic_ones() {
let repo = repo("two-origins");
let (mappings, specs) = mapping_from_spec("refs/heads/symbolic:refs/heads/not-currently-checked-out", &repo);
let out = fetch::refs::update(
@@ -323,6 +673,7 @@ mod update {
out.updates,
vec![fetch::refs::Update {
mode: fetch::refs::update::Mode::NoChangeNeeded,
+ type_change: Some(fetch::refs::update::TypeChange::DirectToSymbolic),
edit_index: Some(0)
}],
);
@@ -349,6 +700,7 @@ mod update {
out.updates,
vec![fetch::refs::Update {
mode: fetch::refs::update::Mode::New,
+ type_change: None,
edit_index: Some(0),
}],
);
@@ -399,10 +751,12 @@ mod update {
vec![
fetch::refs::Update {
mode: fetch::refs::update::Mode::New,
+ type_change: None,
edit_index: Some(0),
},
fetch::refs::Update {
mode: fetch::refs::update::Mode::NoChangeNeeded,
+ type_change: None,
edit_index: Some(1),
}
],
@@ -446,6 +800,7 @@ mod update {
out.updates,
vec![fetch::refs::Update {
mode: fetch::refs::update::Mode::FastForward,
+ type_change: None,
edit_index: Some(0),
}],
"The caller has to be aware and note that dry-runs can't know about fast-forwards as they don't have remote objects"
@@ -480,6 +835,7 @@ mod update {
out.updates,
vec![fetch::refs::Update {
mode: fetch::refs::update::Mode::RejectedNonFastForward,
+ type_change: None,
edit_index: None,
}]
);
@@ -502,6 +858,7 @@ mod update {
out.updates,
vec![fetch::refs::Update {
mode: fetch::refs::update::Mode::FastForward,
+ type_change: None,
edit_index: Some(0),
}]
);
@@ -535,6 +892,7 @@ mod update {
out.updates,
vec![fetch::refs::Update {
mode: fetch::refs::update::Mode::FastForward,
+ type_change: None,
edit_index: Some(0),
}]
);
@@ -548,12 +906,15 @@ mod update {
}
}
- fn mapping_from_spec(spec: &str, repo: &gix::Repository) -> (Vec<fetch::Mapping>, Vec<gix::refspec::RefSpec>) {
+ fn mapping_from_spec(
+ spec: &str,
+ remote_repo: &gix::Repository,
+ ) -> (Vec<fetch::Mapping>, Vec<gix::refspec::RefSpec>) {
let spec = gix_refspec::parse(spec.into(), gix_refspec::parse::Operation::Fetch).unwrap();
let group = gix_refspec::MatchGroup::from_fetch_specs(Some(spec));
- let references = repo.references().unwrap();
+ let references = remote_repo.references().unwrap();
let mut references: Vec<_> = references.all().unwrap().map(|r| into_remote_ref(r.unwrap())).collect();
- references.push(into_remote_ref(repo.find_reference("HEAD").unwrap()));
+ references.push(into_remote_ref(remote_repo.find_reference("HEAD").unwrap()));
let mappings = group
.match_remotes(references.iter().map(remote_ref_to_item))
.mappings
@@ -566,7 +927,7 @@ mod update {
},
|idx| fetch::Source::Ref(references[idx].clone()),
),
- local: m.rhs.map(|r| r.into_owned()),
+ local: m.rhs.map(std::borrow::Cow::into_owned),
spec_index: SpecIndex::ExplicitInRemote(m.spec_index),
})
.collect();
@@ -582,11 +943,14 @@ mod update {
},
TargetRef::Symbolic(name) => {
let target = name.as_bstr().into();
- let id = r.peel_to_id_in_place().unwrap();
- gix_protocol::handshake::Ref::Symbolic {
- full_ref_name,
- target,
- object: id.detach(),
+ match r.peel_to_id_in_place() {
+ Ok(id) => gix_protocol::handshake::Ref::Symbolic {
+ full_ref_name,
+ target,
+ tag: None,
+ object: id.detach(),
+ },
+ Err(_) => gix_protocol::handshake::Ref::Unborn { full_ref_name, target },
}
}
}
@@ -594,9 +958,10 @@ mod update {
fn remote_ref_to_item(r: &gix_protocol::handshake::Ref) -> gix_refspec::match_group::Item<'_> {
let (full_ref_name, target, object) = r.unpack();
+ static NULL: gix_hash::ObjectId = gix_hash::Kind::Sha1.null();
gix_refspec::match_group::Item {
full_ref_name,
- target: target.expect("no unborn HEAD"),
+ target: target.unwrap_or(NULL.as_ref()),
object,
}
}
diff --git a/vendor/gix/src/remote/connection/fetch/update_refs/update.rs b/vendor/gix/src/remote/connection/fetch/update_refs/update.rs
index 6eda1ffc0..41ed3753d 100644
--- a/vendor/gix/src/remote/connection/fetch/update_refs/update.rs
+++ b/vendor/gix/src/remote/connection/fetch/update_refs/update.rs
@@ -10,7 +10,7 @@ mod error {
#[error(transparent)]
FindReference(#[from] crate::reference::find::Error),
#[error("A remote reference had a name that wasn't considered valid. Corrupt remote repo or insufficient checks on remote?")]
- InvalidRefName(#[from] gix_validate::refname::Error),
+ InvalidRefName(#[from] gix_validate::reference::name::Error),
#[error("Failed to update references to their new position to match their remote locations")]
EditReferences(#[from] crate::reference::edit::Error),
#[error("Failed to read or iterate worktree dir")]
@@ -19,6 +19,10 @@ mod error {
OpenWorktreeRepo(#[from] crate::open::Error),
#[error("Could not find local commit for fast-forward ancestor check")]
FindCommit(#[from] crate::object::find::existing::Error),
+ #[error("Could not peel symbolic local reference to its ID")]
+ PeelToId(#[from] crate::reference::peel::Error),
+ #[error("Failed to follow a symbolic reference to assure worktree isn't affected")]
+ FollowSymref(#[from] gix_ref::file::find::existing::Error),
}
}
@@ -35,11 +39,14 @@ pub struct Outcome {
pub updates: Vec<super::Update>,
}
-/// Describe the way a ref was updated
+/// Describe the way a ref was updated, with particular focus on how the (peeled) target commit was affected.
+///
+/// Note that for all the variants that signal a change or `NoChangeNeeded` it's additionally possible to change the target type
+/// from symbolic to direct, or the other way around.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mode {
/// No change was attempted as the remote ref didn't change compared to the current ref, or because no remote ref was specified
- /// in the ref-spec.
+ /// in the ref-spec. Note that the expected value is still asserted to uncover potential race conditions with other processes.
NoChangeNeeded,
/// The old ref's commit was an ancestor of the new one, allowing for a fast-forward without a merge.
FastForward,
@@ -62,14 +69,19 @@ pub enum Mode {
RejectedTagUpdate,
/// The reference update would not have been a fast-forward, and force is not specified in the ref-spec.
RejectedNonFastForward,
- /// The update of a local symbolic reference was rejected.
- RejectedSymbolic,
+ /// The remote has an unborn symbolic reference where we have one that is set. This means the remote
+ /// has reset itself to a newly initialized state or a state that is highly unusual.
+ /// It may also mean that the remote knows the target name, but it's not available locally and not included in the ref-mappings
+ /// to be created, so we would effectively change a valid local ref into one that seems unborn, which is rejected.
+ /// Note that this mode may have an associated ref-edit that is a no-op, or current-state assertion, for logistical reasons only
+ /// and having no edit would be preferred.
+ RejectedToReplaceWithUnborn,
/// The update was rejected because the branch is checked out in the given worktree_dir.
///
/// Note that the check applies to any known worktree, whether it's present on disk or not.
RejectedCurrentlyCheckedOut {
- /// The path to the worktree directory where the branch is checked out.
- worktree_dir: PathBuf,
+ /// The path(s) to the worktree directory where the branch is checked out.
+ worktree_dirs: Vec<PathBuf>,
},
}
@@ -84,12 +96,16 @@ impl std::fmt::Display for Mode {
Mode::RejectedSourceObjectNotFound { id } => return write!(f, "rejected ({id} not found)"),
Mode::RejectedTagUpdate => "rejected (would overwrite existing tag)",
Mode::RejectedNonFastForward => "rejected (non-fast-forward)",
- Mode::RejectedSymbolic => "rejected (refusing to write symbolic refs)",
- Mode::RejectedCurrentlyCheckedOut { worktree_dir } => {
+ Mode::RejectedToReplaceWithUnborn => "rejected (refusing to overwrite existing with unborn ref)",
+ Mode::RejectedCurrentlyCheckedOut { worktree_dirs } => {
return write!(
f,
"rejected (cannot write into checked-out branch at \"{}\")",
- worktree_dir.display()
+ worktree_dirs
+ .iter()
+ .filter_map(|d| d.to_str())
+ .collect::<Vec<_>>()
+ .join(", ")
)
}
}
@@ -97,6 +113,15 @@ impl std::fmt::Display for Mode {
}
}
+/// Indicates that a ref changes its type.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
+pub enum TypeChange {
+ /// A local direct reference is changed into a symbolic one.
+ DirectToSymbolic,
+ /// A local symbolic reference is changed into a direct one.
+ SymbolicToDirect,
+}
+
impl Outcome {
/// Produce an iterator over all information used to produce the this outcome, ref-update by ref-update, using the `mappings`
/// used when producing the ref update.
diff --git a/vendor/gix/src/remote/connection/ref_map.rs b/vendor/gix/src/remote/connection/ref_map.rs
index 95ddb6214..f1b40d56e 100644
--- a/vendor/gix/src/remote/connection/ref_map.rs
+++ b/vendor/gix/src/remote/connection/ref_map.rs
@@ -133,7 +133,7 @@ where
)
.await?;
let num_explicit_specs = self.remote.fetch_specs.len();
- let group = gix_refspec::MatchGroup::from_fetch_specs(specs.iter().map(|s| s.to_ref()));
+ let group = gix_refspec::MatchGroup::from_fetch_specs(specs.iter().map(gix_refspec::RefSpec::to_ref));
let (res, fixes) = group
.match_remotes(remote.refs.iter().map(|r| {
let (full_ref_name, target, object) = r.unpack();
@@ -157,7 +157,7 @@ where
},
|idx| fetch::Source::Ref(remote.refs[idx].clone()),
),
- local: m.rhs.map(|c| c.into_owned()),
+ local: m.rhs.map(std::borrow::Cow::into_owned),
spec_index: if m.spec_index < num_explicit_specs {
SpecIndex::ExplicitInRemote(m.spec_index)
} else {
@@ -204,7 +204,7 @@ where
self.transport_options = self
.remote
.repo
- .transport_options(url.as_ref(), self.remote.name().map(|n| n.as_bstr()))
+ .transport_options(url.as_ref(), self.remote.name().map(crate::remote::Name::as_bstr))
.map_err(|err| Error::GatherTransportConfig {
source: err,
url: url.into_owned(),
diff --git a/vendor/gix/src/remote/errors.rs b/vendor/gix/src/remote/errors.rs
index 20060cedf..34ed8246b 100644
--- a/vendor/gix/src/remote/errors.rs
+++ b/vendor/gix/src/remote/errors.rs
@@ -2,7 +2,7 @@
pub mod find {
use crate::{bstr::BString, config, remote};
- /// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()].
+ /// The error returned by [`Repository::find_remote(…)`](crate::Repository::find_remote()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
@@ -30,7 +30,7 @@ pub mod find {
pub mod existing {
use crate::bstr::BString;
- /// The error returned by [`Repository::find_remote(…)`][crate::Repository::find_remote()].
+ /// The error returned by [`Repository::find_remote(…)`](crate::Repository::find_remote()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
@@ -42,4 +42,23 @@ pub mod find {
NotFound { name: BString },
}
}
+
+ ///
+ pub mod for_fetch {
+ /// The error returned by [`Repository::find_fetch_remote(…)`](crate::Repository::find_fetch_remote()).
+ #[derive(Debug, thiserror::Error)]
+ #[allow(missing_docs)]
+ pub enum Error {
+ #[error(transparent)]
+ FindExisting(#[from] super::existing::Error),
+ #[error(transparent)]
+ FindExistingReferences(#[from] crate::reference::find::existing::Error),
+ #[error("Could not initialize a URL remote")]
+ Init(#[from] crate::remote::init::Error),
+ #[error("remote name could not be parsed as URL")]
+ UrlParse(#[from] gix_url::parse::Error),
+ #[error("No configured remote could be found, or too many were available")]
+ ExactlyOneRemoteNotAvailable,
+ }
+ }
}
diff --git a/vendor/gix/src/remote/fetch.rs b/vendor/gix/src/remote/fetch.rs
index 0947ace3f..4700201de 100644
--- a/vendor/gix/src/remote/fetch.rs
+++ b/vendor/gix/src/remote/fetch.rs
@@ -1,17 +1,20 @@
///
pub mod negotiate {
+ #[cfg(feature = "credentials")]
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,
+ add_wants, make_refmapping_ignore_predicate, 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};
+pub use super::connection::fetch::{
+ outcome, 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)]
@@ -152,6 +155,18 @@ impl Source {
}
}
+ /// Return the target that this symbolic ref is pointing to, or `None` if it is no symbolic ref.
+ pub fn as_target(&self) -> Option<&crate::bstr::BStr> {
+ match self {
+ Source::ObjectId(_) => None,
+ Source::Ref(r) => match r {
+ gix_protocol::handshake::Ref::Peeled { .. } | gix_protocol::handshake::Ref::Direct { .. } => None,
+ gix_protocol::handshake::Ref::Symbolic { target, .. }
+ | gix_protocol::handshake::Ref::Unborn { target, .. } => Some(target.as_ref()),
+ },
+ }
+ }
+
/// 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 {
diff --git a/vendor/gix/src/remote/init.rs b/vendor/gix/src/remote/init.rs
index bba116946..13b747eda 100644
--- a/vendor/gix/src/remote/init.rs
+++ b/vendor/gix/src/remote/init.rs
@@ -67,7 +67,18 @@ impl<'repo> Remote<'repo> {
Url: TryInto<gix_url::Url, Error = E>,
gix_url::parse::Error: From<E>,
{
- let url = url.try_into().map_err(|err| Error::Url(err.into()))?;
+ Self::from_fetch_url_inner(
+ url.try_into().map_err(|err| Error::Url(err.into()))?,
+ should_rewrite_urls,
+ repo,
+ )
+ }
+
+ fn from_fetch_url_inner(
+ url: gix_url::Url,
+ should_rewrite_urls: bool,
+ repo: &'repo Repository,
+ ) -> Result<Self, Error> {
let (url_alias, _) = should_rewrite_urls
.then(|| rewrite_urls(&repo.config, Some(&url), None))
.unwrap_or(Ok((None, None)))?;
diff --git a/vendor/gix/src/remote/save.rs b/vendor/gix/src/remote/save.rs
index ad6a75b14..2a91dfa9c 100644
--- a/vendor/gix/src/remote/save.rs
+++ b/vendor/gix/src/remote/save.rs
@@ -1,5 +1,7 @@
use std::convert::TryInto;
+use gix_macros::momo;
+
use crate::{
bstr::{BStr, BString},
config, remote, Remote,
@@ -25,7 +27,7 @@ pub enum AsError {
Name(#[from] crate::remote::name::Error),
}
-/// Serialize into gix-config.
+/// Serialize into git-config.
impl Remote<'_> {
/// Save ourselves to the given `config` if we are a named remote or fail otherwise.
///
@@ -111,6 +113,7 @@ impl Remote<'_> {
/// If this name is different from the current one, the git configuration will still contain the previous name,
/// and the caller should account for that.
#[allow(clippy::result_large_err)]
+ #[momo]
pub fn save_as_to(
&mut self,
name: impl Into<BString>,