Skip to content

Commit af8f201

Browse files
authored
Merge pull request #1763 from GitoxideLabs/better-refspec-primitives
Remote reverse mapping
2 parents 12f672f + da0e1c7 commit af8f201

File tree

23 files changed

+356
-107
lines changed

23 files changed

+356
-107
lines changed

gix-protocol/src/fetch/refmap/init.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ impl RefMap {
110110
let num_explicit_specs = fetch_refspecs.len();
111111
let group = gix_refspec::MatchGroup::from_fetch_specs(all_refspecs.iter().map(gix_refspec::RefSpec::to_ref));
112112
let (res, fixes) = group
113-
.match_remotes(remote_refs.iter().map(|r| {
113+
.match_lhs(remote_refs.iter().map(|r| {
114114
let (full_ref_name, target, object) = r.unpack();
115115
gix_refspec::match_group::Item {
116116
full_ref_name,

gix-refspec/src/match_group/mod.rs

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::collections::BTreeSet;
33
use crate::{parse::Operation, types::Mode, MatchGroup, RefSpecRef};
44

55
pub(crate) mod types;
6-
pub use types::{Item, Mapping, Outcome, Source, SourceRef};
6+
pub use types::{match_lhs, match_rhs, Item, Mapping, Source, SourceRef};
77

88
///
99
pub mod validate;
@@ -26,13 +26,20 @@ impl<'a> MatchGroup<'a> {
2626
}
2727

2828
/// Matching
29-
impl<'a> MatchGroup<'a> {
29+
impl<'spec> MatchGroup<'spec> {
3030
/// Match all `items` against all *fetch* specs present in this group, returning deduplicated mappings from source to destination.
31-
/// *Note that this method is correct only for specs*, even though it also *works for push-specs*.
31+
/// `items` are expected to be references on the remote, which will be matched and mapped to obtain their local counterparts,
32+
/// i.e. *left side of refspecs is mapped to their right side*.
33+
/// *Note that this method is correct only for fetch-specs*, even though it also *works for push-specs*.
34+
///
35+
/// Object names are never mapped and always returned as match.
3236
///
3337
/// Note that negative matches are not part of the return value, so they are not observable but will be used to remove mappings.
3438
// TODO: figure out how to deal with push-specs, probably when push is being implemented.
35-
pub fn match_remotes<'item>(self, mut items: impl Iterator<Item = Item<'item>> + Clone) -> Outcome<'a, 'item> {
39+
pub fn match_lhs<'item>(
40+
self,
41+
mut items: impl Iterator<Item = Item<'item>> + Clone,
42+
) -> match_lhs::Outcome<'spec, 'item> {
3643
let mut out = Vec::new();
3744
let mut seen = BTreeSet::default();
3845
let mut push_unique = |mapping| {
@@ -67,16 +74,15 @@ impl<'a> MatchGroup<'a> {
6774
continue;
6875
}
6976
for (item_index, item) in items.clone().enumerate() {
70-
if let Some(matcher) = matcher {
71-
let (matched, rhs) = matcher.matches_lhs(item);
72-
if matched {
73-
push_unique(Mapping {
74-
item_index: Some(item_index),
75-
lhs: SourceRef::FullName(item.full_ref_name),
76-
rhs,
77-
spec_index,
78-
});
79-
}
77+
let Some(matcher) = matcher else { continue };
78+
let (matched, rhs) = matcher.matches_lhs(item);
79+
if matched {
80+
push_unique(Mapping {
81+
item_index: Some(item_index),
82+
lhs: SourceRef::FullName(item.full_ref_name.into()),
83+
rhs,
84+
spec_index,
85+
});
8086
}
8187
}
8288
}
@@ -88,12 +94,78 @@ impl<'a> MatchGroup<'a> {
8894
.zip(self.specs.iter())
8995
.filter_map(|(m, spec)| m.and_then(|m| (spec.mode == Mode::Negative).then_some(m)))
9096
{
91-
out.retain(|m| match m.lhs {
97+
out.retain(|m| match &m.lhs {
9298
SourceRef::ObjectId(_) => true,
9399
SourceRef::FullName(name) => {
94100
!matcher
95101
.matches_lhs(Item {
96-
full_ref_name: name,
102+
full_ref_name: name.as_ref(),
103+
target: &null_id,
104+
object: None,
105+
})
106+
.0
107+
}
108+
});
109+
}
110+
}
111+
match_lhs::Outcome {
112+
group: self,
113+
mappings: out,
114+
}
115+
}
116+
117+
/// Match all `items` against all *fetch* specs present in this group, returning deduplicated mappings from destination to source.
118+
/// `items` are expected to be tracking references in the local clone, which will be matched and reverse-mapped to obtain their remote counterparts,
119+
/// i.e. *right side of refspecs is mapped to their left side*.
120+
/// *Note that this method is correct only for fetch-specs*, even though it also *works for push-specs*.
121+
///
122+
/// Note that negative matches are not part of the return value, so they are not observable but will be used to remove mappings.
123+
// Reverse-mapping is implemented here: https://github.com/git/git/blob/76cf4f61c87855ebf0784b88aaf737d6b09f504b/branch.c#L252
124+
pub fn match_rhs<'item>(
125+
self,
126+
mut items: impl Iterator<Item = Item<'item>> + Clone,
127+
) -> match_rhs::Outcome<'spec, 'item> {
128+
let mut out = Vec::<Mapping<'spec, 'item>>::new();
129+
let mut seen = BTreeSet::default();
130+
let mut push_unique = |mapping| {
131+
if seen.insert(calculate_hash(&mapping)) {
132+
out.push(mapping);
133+
}
134+
};
135+
let mut matchers: Vec<Matcher<'_>> = self.specs.iter().copied().map(Matcher::from).collect();
136+
137+
let mut has_negation = false;
138+
for (spec_index, (spec, matcher)) in self.specs.iter().zip(matchers.iter_mut()).enumerate() {
139+
if spec.mode == Mode::Negative {
140+
has_negation = true;
141+
continue;
142+
}
143+
for (item_index, item) in items.clone().enumerate() {
144+
let (matched, lhs) = matcher.matches_rhs(item);
145+
if let Some(lhs) = lhs.filter(|_| matched) {
146+
push_unique(Mapping {
147+
item_index: Some(item_index),
148+
lhs: SourceRef::FullName(lhs),
149+
rhs: Some(item.full_ref_name.into()),
150+
spec_index,
151+
});
152+
}
153+
}
154+
}
155+
156+
if let Some(hash_kind) = has_negation.then(|| items.next().map(|i| i.target.kind())).flatten() {
157+
let null_id = hash_kind.null();
158+
for matcher in matchers
159+
.into_iter()
160+
.zip(self.specs.iter())
161+
.filter_map(|(m, spec)| (spec.mode == Mode::Negative).then_some(m))
162+
{
163+
out.retain(|m| match &m.lhs {
164+
SourceRef::ObjectId(_) => true,
165+
SourceRef::FullName(name) => {
166+
!matcher
167+
.matches_rhs(Item {
168+
full_ref_name: name.as_ref(),
97169
target: &null_id,
98170
object: None,
99171
})
@@ -102,7 +174,7 @@ impl<'a> MatchGroup<'a> {
102174
});
103175
}
104176
}
105-
Outcome {
177+
match_rhs::Outcome {
106178
group: self,
107179
mappings: out,
108180
}

gix-refspec/src/match_group/types.rs

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22

3-
use bstr::{BStr, BString};
3+
use bstr::BStr;
44
use gix_hash::oid;
55

66
use crate::RefSpecRef;
@@ -12,15 +12,38 @@ pub struct MatchGroup<'a> {
1212
pub specs: Vec<RefSpecRef<'a>>,
1313
}
1414

15-
/// The outcome of any matching operation of a [`MatchGroup`].
1615
///
17-
/// It's used to validate and process the contained [mappings][Mapping].
18-
#[derive(Debug, Clone)]
19-
pub struct Outcome<'spec, 'item> {
20-
/// The match group that produced this outcome.
21-
pub group: MatchGroup<'spec>,
22-
/// The mappings derived from matching [items][Item].
23-
pub mappings: Vec<Mapping<'item, 'spec>>,
16+
pub mod match_lhs {
17+
use crate::match_group::Mapping;
18+
use crate::MatchGroup;
19+
20+
/// The outcome of any matching operation of a [`MatchGroup`].
21+
///
22+
/// It's used to validate and process the contained [mappings](Mapping).
23+
#[derive(Debug, Clone)]
24+
pub struct Outcome<'spec, 'item> {
25+
/// The match group that produced this outcome.
26+
pub group: MatchGroup<'spec>,
27+
/// The mappings derived from matching [items](crate::match_group::Item).
28+
pub mappings: Vec<Mapping<'item, 'spec>>,
29+
}
30+
}
31+
32+
///
33+
pub mod match_rhs {
34+
use crate::match_group::Mapping;
35+
use crate::MatchGroup;
36+
37+
/// The outcome of any matching operation of a [`MatchGroup`].
38+
///
39+
/// It's used to validate and process the contained [mappings](Mapping).
40+
#[derive(Debug, Clone)]
41+
pub struct Outcome<'spec, 'item> {
42+
/// The match group that produced this outcome.
43+
pub group: MatchGroup<'spec>,
44+
/// The mappings derived from matching [items](crate::match_group::Item).
45+
pub mappings: Vec<Mapping<'spec, 'item>>,
46+
}
2447
}
2548

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

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

5174
impl SourceRef<'_> {
52-
/// Create a fully owned instance from this one.
53-
pub fn to_owned(&self) -> Source {
75+
/// Create a fully owned instance by consuming this one.
76+
pub fn into_owned(self) -> Source {
5477
match self {
55-
SourceRef::ObjectId(id) => Source::ObjectId(*id),
56-
SourceRef::FullName(name) => Source::FullName((*name).to_owned()),
78+
SourceRef::ObjectId(id) => Source::ObjectId(id),
79+
SourceRef::FullName(name) => Source::FullName(name.into_owned().into()),
5780
}
5881
}
5982
}
6083

61-
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62-
/// The source (or left-hand) side of a mapping, which owns its name.
63-
pub enum Source {
64-
/// A full reference name, which is expected to be valid.
65-
///
66-
/// Validity, however, is not enforced here.
67-
FullName(BString),
68-
/// The name of an object that is expected to exist on the remote side.
69-
/// Note that it might not be advertised by the remote but part of the object graph,
70-
/// and thus gets sent in the pack. The server is expected to fail unless the desired
71-
/// object is present but at some time it is merely a request by the user.
72-
ObjectId(gix_hash::ObjectId),
73-
}
74-
75-
impl std::fmt::Display for Source {
84+
impl std::fmt::Display for SourceRef<'_> {
7685
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7786
match self {
78-
Source::FullName(name) => name.fmt(f),
79-
Source::ObjectId(id) => id.fmt(f),
87+
SourceRef::FullName(name) => name.fmt(f),
88+
SourceRef::ObjectId(id) => id.fmt(f),
8089
}
8190
}
8291
}
8392

93+
/// The source (or left-hand) side of a mapping, which owns its name.
94+
pub type Source = SourceRef<'static>;
95+
8496
/// A mapping from a remote to a local refs for fetches or local to remote refs for pushes.
8597
///
8698
/// Mappings are like edges in a graph, initially without any constraints.

gix-refspec/src/match_group/util.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ pub struct Matcher<'a> {
1212
}
1313

1414
impl<'a> Matcher<'a> {
15-
/// Match `item` against this spec and return `(true, Some<rhs>)` to gain the other side of the match as configured, or `(true, None)`
16-
/// if there was no `rhs` but the `item` matched. Lastly, return `(false, None)` if `item` didn't match at all.
15+
/// Match the lefthand-side `item` against this spec and return `(true, Some<rhs>)` to gain the other,
16+
/// transformed righthand-side of the match as configured by the refspec.
17+
/// Or return `(true, None)` if there was no `rhs` but the `item` matched.
18+
/// Lastly, return `(false, None)` if `item` didn't match at all.
1719
///
1820
/// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob.
1921
pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option<Cow<'a, BStr>>) {
@@ -23,6 +25,20 @@ impl<'a> Matcher<'a> {
2325
(None, _) => (false, None),
2426
}
2527
}
28+
29+
/// Match the righthand-side `item` against this spec and return `(true, Some<lhs>)` to gain the other,
30+
/// transformed lefthand-side of the match as configured by the refspec.
31+
/// Or return `(true, None)` if there was no `lhs` but the `item` matched.
32+
/// Lastly, return `(false, None)` if `item` didn't match at all.
33+
///
34+
/// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob.
35+
pub fn matches_rhs(&self, item: Item<'_>) -> (bool, Option<Cow<'a, BStr>>) {
36+
match (self.lhs, self.rhs) {
37+
(None, Some(rhs)) => (rhs.matches(item).is_match(), None),
38+
(Some(lhs), Some(rhs)) => rhs.matches(item).into_match_outcome(lhs, item),
39+
(_, None) => (false, None),
40+
}
41+
}
2642
}
2743

2844
#[derive(Debug, Copy, Clone)]

0 commit comments

Comments
 (0)