Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remote reverse mapping #1763

Merged
merged 4 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gix-protocol/src/fetch/refmap/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ impl RefMap {
let num_explicit_specs = fetch_refspecs.len();
let group = gix_refspec::MatchGroup::from_fetch_specs(all_refspecs.iter().map(gix_refspec::RefSpec::to_ref));
let (res, fixes) = group
.match_remotes(remote_refs.iter().map(|r| {
.match_lhs(remote_refs.iter().map(|r| {
let (full_ref_name, target, object) = r.unpack();
gix_refspec::match_group::Item {
full_ref_name,
Expand Down
106 changes: 89 additions & 17 deletions gix-refspec/src/match_group/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::BTreeSet;
use crate::{parse::Operation, types::Mode, MatchGroup, RefSpecRef};

pub(crate) mod types;
pub use types::{Item, Mapping, Outcome, Source, SourceRef};
pub use types::{match_lhs, match_rhs, Item, Mapping, Source, SourceRef};

///
pub mod validate;
Expand All @@ -26,13 +26,20 @@ impl<'a> MatchGroup<'a> {
}

/// Matching
impl<'a> MatchGroup<'a> {
impl<'spec> MatchGroup<'spec> {
/// Match all `items` against all *fetch* specs present in this group, returning deduplicated mappings from source to destination.
/// *Note that this method is correct only for specs*, even though it also *works for push-specs*.
/// `items` are expected to be references on the remote, which will be matched and mapped to obtain their local counterparts,
/// i.e. *left side of refspecs is mapped to their right side*.
/// *Note that this method is correct only for fetch-specs*, even though it also *works for push-specs*.
///
/// Object names are never mapped and always returned as match.
///
/// Note that negative matches are not part of the return value, so they are not observable but will be used to remove mappings.
// TODO: figure out how to deal with push-specs, probably when push is being implemented.
pub fn match_remotes<'item>(self, mut items: impl Iterator<Item = Item<'item>> + Clone) -> Outcome<'a, 'item> {
pub fn match_lhs<'item>(
self,
mut items: impl Iterator<Item = Item<'item>> + Clone,
) -> match_lhs::Outcome<'spec, 'item> {
let mut out = Vec::new();
let mut seen = BTreeSet::default();
let mut push_unique = |mapping| {
Expand Down Expand Up @@ -67,16 +74,15 @@ impl<'a> MatchGroup<'a> {
continue;
}
for (item_index, item) in items.clone().enumerate() {
if let Some(matcher) = matcher {
let (matched, rhs) = matcher.matches_lhs(item);
if matched {
push_unique(Mapping {
item_index: Some(item_index),
lhs: SourceRef::FullName(item.full_ref_name),
rhs,
spec_index,
});
}
let Some(matcher) = matcher else { continue };
let (matched, rhs) = matcher.matches_lhs(item);
if matched {
push_unique(Mapping {
item_index: Some(item_index),
lhs: SourceRef::FullName(item.full_ref_name.into()),
rhs,
spec_index,
});
}
}
}
Expand All @@ -88,12 +94,78 @@ impl<'a> MatchGroup<'a> {
.zip(self.specs.iter())
.filter_map(|(m, spec)| m.and_then(|m| (spec.mode == Mode::Negative).then_some(m)))
{
out.retain(|m| match m.lhs {
out.retain(|m| match &m.lhs {
SourceRef::ObjectId(_) => true,
SourceRef::FullName(name) => {
!matcher
.matches_lhs(Item {
full_ref_name: name,
full_ref_name: name.as_ref(),
target: &null_id,
object: None,
})
.0
}
});
}
}
match_lhs::Outcome {
group: self,
mappings: out,
}
}

/// Match all `items` against all *fetch* specs present in this group, returning deduplicated mappings from destination to source.
/// `items` are expected to be tracking references in the local clone, which will be matched and reverse-mapped to obtain their remote counterparts,
/// i.e. *right side of refspecs is mapped to their left side*.
/// *Note that this method is correct only for fetch-specs*, even though it also *works for push-specs*.
///
/// Note that negative matches are not part of the return value, so they are not observable but will be used to remove mappings.
// Reverse-mapping is implemented here: https://github.com/git/git/blob/76cf4f61c87855ebf0784b88aaf737d6b09f504b/branch.c#L252
pub fn match_rhs<'item>(
self,
mut items: impl Iterator<Item = Item<'item>> + Clone,
) -> match_rhs::Outcome<'spec, 'item> {
let mut out = Vec::<Mapping<'spec, 'item>>::new();
let mut seen = BTreeSet::default();
let mut push_unique = |mapping| {
if seen.insert(calculate_hash(&mapping)) {
out.push(mapping);
}
};
let mut matchers: Vec<Matcher<'_>> = self.specs.iter().copied().map(Matcher::from).collect();

let mut has_negation = false;
for (spec_index, (spec, matcher)) in self.specs.iter().zip(matchers.iter_mut()).enumerate() {
if spec.mode == Mode::Negative {
has_negation = true;
continue;
}
for (item_index, item) in items.clone().enumerate() {
let (matched, lhs) = matcher.matches_rhs(item);
if let Some(lhs) = lhs.filter(|_| matched) {
push_unique(Mapping {
item_index: Some(item_index),
lhs: SourceRef::FullName(lhs),
rhs: Some(item.full_ref_name.into()),
spec_index,
});
}
}
}

if let Some(hash_kind) = has_negation.then(|| items.next().map(|i| i.target.kind())).flatten() {
let null_id = hash_kind.null();
for matcher in matchers
.into_iter()
.zip(self.specs.iter())
.filter_map(|(m, spec)| (spec.mode == Mode::Negative).then_some(m))
{
out.retain(|m| match &m.lhs {
SourceRef::ObjectId(_) => true,
SourceRef::FullName(name) => {
!matcher
.matches_rhs(Item {
full_ref_name: name.as_ref(),
target: &null_id,
object: None,
})
Expand All @@ -102,7 +174,7 @@ impl<'a> MatchGroup<'a> {
});
}
}
Outcome {
match_rhs::Outcome {
group: self,
mappings: out,
}
Expand Down
78 changes: 45 additions & 33 deletions gix-refspec/src/match_group/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::borrow::Cow;

use bstr::{BStr, BString};
use bstr::BStr;
use gix_hash::oid;

use crate::RefSpecRef;
Expand All @@ -12,15 +12,38 @@ pub struct MatchGroup<'a> {
pub specs: Vec<RefSpecRef<'a>>,
}

/// The outcome of any matching operation of a [`MatchGroup`].
///
/// It's used to validate and process the contained [mappings][Mapping].
#[derive(Debug, Clone)]
pub struct Outcome<'spec, 'item> {
/// The match group that produced this outcome.
pub group: MatchGroup<'spec>,
/// The mappings derived from matching [items][Item].
pub mappings: Vec<Mapping<'item, 'spec>>,
pub mod match_lhs {
use crate::match_group::Mapping;
use crate::MatchGroup;

/// The outcome of any matching operation of a [`MatchGroup`].
///
/// It's used to validate and process the contained [mappings](Mapping).
#[derive(Debug, Clone)]
pub struct Outcome<'spec, 'item> {
/// The match group that produced this outcome.
pub group: MatchGroup<'spec>,
/// The mappings derived from matching [items](crate::match_group::Item).
pub mappings: Vec<Mapping<'item, 'spec>>,
}
}

///
pub mod match_rhs {
use crate::match_group::Mapping;
use crate::MatchGroup;

/// The outcome of any matching operation of a [`MatchGroup`].
///
/// It's used to validate and process the contained [mappings](Mapping).
#[derive(Debug, Clone)]
pub struct Outcome<'spec, 'item> {
/// The match group that produced this outcome.
pub group: MatchGroup<'spec>,
/// The mappings derived from matching [items](crate::match_group::Item).
pub mappings: Vec<Mapping<'spec, 'item>>,
}
}

/// An item to match, input to various matching operations.
Expand All @@ -34,13 +57,13 @@ pub struct Item<'a> {
pub object: Option<&'a oid>,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
/// The source (or left-hand) side of a mapping, which references its name.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// The source (or left-hand) side of a mapping.
pub enum SourceRef<'a> {
/// A full reference name, which is expected to be valid.
///
/// Validity, however, is not enforced here.
FullName(&'a BStr),
FullName(Cow<'a, BStr>),
/// The name of an object that is expected to exist on the remote side.
/// Note that it might not be advertised by the remote but part of the object graph,
/// and thus gets sent in the pack. The server is expected to fail unless the desired
Expand All @@ -49,38 +72,27 @@ pub enum SourceRef<'a> {
}

impl SourceRef<'_> {
/// Create a fully owned instance from this one.
pub fn to_owned(&self) -> Source {
/// Create a fully owned instance by consuming this one.
pub fn into_owned(self) -> Source {
match self {
SourceRef::ObjectId(id) => Source::ObjectId(*id),
SourceRef::FullName(name) => Source::FullName((*name).to_owned()),
SourceRef::ObjectId(id) => Source::ObjectId(id),
SourceRef::FullName(name) => Source::FullName(name.into_owned().into()),
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// The source (or left-hand) side of a mapping, which owns its name.
pub enum Source {
/// A full reference name, which is expected to be valid.
///
/// Validity, however, is not enforced here.
FullName(BString),
/// The name of an object that is expected to exist on the remote side.
/// Note that it might not be advertised by the remote but part of the object graph,
/// and thus gets sent in the pack. The server is expected to fail unless the desired
/// object is present but at some time it is merely a request by the user.
ObjectId(gix_hash::ObjectId),
}

impl std::fmt::Display for Source {
impl std::fmt::Display for SourceRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Source::FullName(name) => name.fmt(f),
Source::ObjectId(id) => id.fmt(f),
SourceRef::FullName(name) => name.fmt(f),
SourceRef::ObjectId(id) => id.fmt(f),
}
}
}

/// The source (or left-hand) side of a mapping, which owns its name.
pub type Source = SourceRef<'static>;

/// A mapping from a remote to a local refs for fetches or local to remote refs for pushes.
///
/// Mappings are like edges in a graph, initially without any constraints.
Expand Down
20 changes: 18 additions & 2 deletions gix-refspec/src/match_group/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ pub struct Matcher<'a> {
}

impl<'a> Matcher<'a> {
/// Match `item` against this spec and return `(true, Some<rhs>)` to gain the other side of the match as configured, or `(true, None)`
/// if there was no `rhs` but the `item` matched. Lastly, return `(false, None)` if `item` didn't match at all.
/// Match the lefthand-side `item` against this spec and return `(true, Some<rhs>)` to gain the other,
/// transformed righthand-side of the match as configured by the refspec.
/// Or return `(true, None)` if there was no `rhs` but the `item` matched.
/// Lastly, return `(false, None)` if `item` didn't match at all.
///
/// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob.
pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option<Cow<'a, BStr>>) {
Expand All @@ -23,6 +25,20 @@ impl<'a> Matcher<'a> {
(None, _) => (false, None),
}
}

/// Match the righthand-side `item` against this spec and return `(true, Some<lhs>)` to gain the other,
/// transformed lefthand-side of the match as configured by the refspec.
/// Or return `(true, None)` if there was no `lhs` but the `item` matched.
/// Lastly, return `(false, None)` if `item` didn't match at all.
///
/// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob.
pub fn matches_rhs(&self, item: Item<'_>) -> (bool, Option<Cow<'a, BStr>>) {
match (self.lhs, self.rhs) {
(None, Some(rhs)) => (rhs.matches(item).is_match(), None),
(Some(lhs), Some(rhs)) => rhs.matches(item).into_match_outcome(lhs, item),
(_, None) => (false, None),
}
}
}

#[derive(Debug, Copy, Clone)]
Expand Down
Loading
Loading