Skip to content

Commit

Permalink
Merge pull request GitoxideLabs#1763 from GitoxideLabs/better-refspec…
Browse files Browse the repository at this point in the history
…-primitives

Remote reverse mapping
  • Loading branch information
Byron authored Jan 13, 2025
2 parents 12f672f + da0e1c7 commit af8f201
Show file tree
Hide file tree
Showing 23 changed files with 356 additions and 107 deletions.
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

0 comments on commit af8f201

Please sign in to comment.