diff options
Diffstat (limited to 'vendor/gix/src/remote')
-rw-r--r-- | vendor/gix/src/remote/build.rs | 23 | ||||
-rw-r--r-- | vendor/gix/src/remote/connect.rs | 6 | ||||
-rw-r--r-- | vendor/gix/src/remote/connection/fetch/mod.rs | 60 | ||||
-rw-r--r-- | vendor/gix/src/remote/connection/fetch/negotiate.rs | 141 | ||||
-rw-r--r-- | vendor/gix/src/remote/connection/fetch/receive_pack.rs | 129 | ||||
-rw-r--r-- | vendor/gix/src/remote/connection/fetch/update_refs/mod.rs | 368 | ||||
-rw-r--r-- | vendor/gix/src/remote/connection/fetch/update_refs/tests.rs | 427 | ||||
-rw-r--r-- | vendor/gix/src/remote/connection/fetch/update_refs/update.rs | 45 | ||||
-rw-r--r-- | vendor/gix/src/remote/connection/ref_map.rs | 6 | ||||
-rw-r--r-- | vendor/gix/src/remote/errors.rs | 23 | ||||
-rw-r--r-- | vendor/gix/src/remote/fetch.rs | 19 | ||||
-rw-r--r-- | vendor/gix/src/remote/init.rs | 13 | ||||
-rw-r--r-- | vendor/gix/src/remote/save.rs | 5 |
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>, |