Skip to content

Commit 71efcbb

Browse files
committed
Merge branch 'fix-no-want-detection'
2 parents fe59956 + 67c06d9 commit 71efcbb

File tree

4 files changed

+105
-54
lines changed

4 files changed

+105
-54
lines changed

gix/src/remote/connection/fetch/negotiate.rs

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub(crate) fn mark_complete_and_common_ref(
6868
graph: &mut gix_negotiate::Graph<'_>,
6969
ref_map: &fetch::RefMap,
7070
shallow: &fetch::Shallow,
71+
mapping_is_ignored: impl Fn(&fetch::Mapping) -> bool,
7172
) -> Result<Action, Error> {
7273
if let fetch::Shallow::Deepen(0) = shallow {
7374
// Avoid deepening (relative) with zero as it seems to upset the server. Git also doesn't actually
@@ -89,6 +90,7 @@ pub(crate) fn mark_complete_and_common_ref(
8990
let mut cutoff_date = None::<SecondsSinceUnixEpoch>;
9091
let mut num_mappings_with_change = 0;
9192
let mut remote_ref_target_known: Vec<bool> = std::iter::repeat(false).take(ref_map.mappings.len()).collect();
93+
let mut remote_ref_included: Vec<bool> = std::iter::repeat(false).take(ref_map.mappings.len()).collect();
9294

9395
for (mapping_idx, mapping) in ref_map.mappings.iter().enumerate() {
9496
let want_id = mapping.remote.as_id();
@@ -98,9 +100,13 @@ pub(crate) fn mark_complete_and_common_ref(
98100
r.target().try_id().map(ToOwned::to_owned)
99101
});
100102

101-
// Like git, we don't let known unchanged mappings participate in the tree traversal
102-
if want_id.zip(have_id).map_or(true, |(want, have)| want != have) {
103-
num_mappings_with_change += 1;
103+
// Even for ignored mappings we want to know if the `want` is already present locally, so skip nothing else.
104+
if !mapping_is_ignored(mapping) {
105+
remote_ref_included[mapping_idx] = true;
106+
// Like git, we don't let known unchanged mappings participate in the tree traversal
107+
if want_id.zip(have_id).map_or(true, |(want, have)| want != have) {
108+
num_mappings_with_change += 1;
109+
}
104110
}
105111

106112
if let Some(commit) = want_id
@@ -114,11 +120,15 @@ pub(crate) fn mark_complete_and_common_ref(
114120
}
115121
}
116122

117-
// If any kind of shallowing operation is desired, the server may still create a pack for us.
118123
if matches!(shallow, Shallow::NoChange) {
119124
if num_mappings_with_change == 0 {
120125
return Ok(Action::NoChange);
121-
} else if remote_ref_target_known.iter().all(|known| *known) {
126+
} else if remote_ref_target_known
127+
.iter()
128+
.zip(remote_ref_included)
129+
.filter_map(|(known, included)| included.then_some(known))
130+
.all(|known| *known)
131+
{
122132
return Ok(Action::SkipToRefUpdate);
123133
}
124134
}
@@ -167,40 +177,50 @@ pub(crate) fn mark_complete_and_common_ref(
167177
})
168178
}
169179

170-
/// Add all `wants` to `arguments`, which is the unpeeled direct target that the advertised remote ref points to.
171-
pub(crate) fn add_wants(
172-
repo: &crate::Repository,
173-
arguments: &mut gix_protocol::fetch::Arguments,
174-
ref_map: &fetch::RefMap,
175-
mapping_known: &[bool],
176-
shallow: &fetch::Shallow,
180+
/// Create a predicate that checks if a refspec mapping should be ignored.
181+
///
182+
/// We want to ignore mappings during negotiation if they would be handled implicitly by the server, which is the case
183+
/// when tags would be sent implicitly due to `Tags::Included`.
184+
pub(crate) fn make_refmapping_ignore_predicate(
177185
fetch_tags: fetch::Tags,
178-
) {
186+
ref_map: &fetch::RefMap,
187+
) -> impl Fn(&fetch::Mapping) -> bool + '_ {
179188
// With included tags, we have to keep mappings of tags to handle them later when updating refs, but we don't want to
180189
// explicitly `want` them as the server will determine by itself which tags are pointing to a commit it wants to send.
181190
// If we would not exclude implicit tag mappings like this, we would get too much of the graph.
182191
let tag_refspec_to_ignore = matches!(fetch_tags, crate::remote::fetch::Tags::Included)
183192
.then(|| fetch_tags.to_refspec())
184193
.flatten();
194+
move |mapping| {
195+
tag_refspec_to_ignore.map_or(false, |tag_spec| {
196+
mapping
197+
.spec_index
198+
.implicit_index()
199+
.and_then(|idx| ref_map.extra_refspecs.get(idx))
200+
.map_or(false, |spec| spec.to_ref() == tag_spec)
201+
})
202+
}
203+
}
185204

205+
/// Add all `wants` to `arguments`, which is the unpeeled direct target that the advertised remote ref points to.
206+
pub(crate) fn add_wants(
207+
repo: &crate::Repository,
208+
arguments: &mut gix_protocol::fetch::Arguments,
209+
ref_map: &fetch::RefMap,
210+
mapping_known: &[bool],
211+
shallow: &fetch::Shallow,
212+
mapping_is_ignored: impl Fn(&fetch::Mapping) -> bool,
213+
) {
186214
// When using shallow, we can't exclude `wants` as the remote won't send anything then. Thus we have to resend everything
187215
// we have as want instead to get exactly the same graph, but possibly deepened.
188216
let is_shallow = !matches!(shallow, fetch::Shallow::NoChange);
189217
let wants = ref_map
190218
.mappings
191219
.iter()
192220
.zip(mapping_known)
193-
.filter_map(|(m, known)| (is_shallow || !*known).then_some(m));
221+
.filter_map(|(m, known)| (is_shallow || !*known).then_some(m))
222+
.filter(|m| !mapping_is_ignored(m));
194223
for want in wants {
195-
// Here we ignore implicit tag mappings if needed.
196-
if tag_refspec_to_ignore.map_or(false, |tag_spec| {
197-
want.spec_index
198-
.implicit_index()
199-
.and_then(|idx| ref_map.extra_refspecs.get(idx))
200-
.map_or(false, |spec| spec.to_ref() == tag_spec)
201-
}) {
202-
continue;
203-
}
204224
let id_on_remote = want.remote.as_id();
205225
if !arguments.can_use_ref_in_want() || matches!(want.remote, fetch::Source::ObjectId(_)) {
206226
if let Some(id) = id_on_remote {

gix/src/remote/connection/fetch/receive_pack.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ where
138138
&mut graph,
139139
&self.ref_map,
140140
&self.shallow,
141+
negotiate::make_refmapping_ignore_predicate(con.remote.fetch_tags, &self.ref_map),
141142
)?;
142143
let mut previous_response = None::<gix_protocol::fetch::Response>;
143144
let mut round = 1;
@@ -155,7 +156,7 @@ where
155156
&self.ref_map,
156157
remote_ref_target_known,
157158
&self.shallow,
158-
con.remote.fetch_tags,
159+
negotiate::make_refmapping_ignore_predicate(con.remote.fetch_tags, &self.ref_map),
159160
);
160161
let is_stateless =
161162
arguments.is_stateless(!con.transport.connection_persists_across_multiple_requests());

gix/src/remote/fetch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub mod negotiate {
66
pub use super::super::connection::fetch::negotiate::Error;
77
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
88
pub(crate) use super::super::connection::fetch::negotiate::{
9-
add_wants, mark_complete_and_common_ref, one_round, Action,
9+
add_wants, make_refmapping_ignore_predicate, mark_complete_and_common_ref, one_round, Action,
1010
};
1111
}
1212

gix/tests/remote/fetch.rs

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -335,38 +335,68 @@ mod blocking_and_async_io {
335335
async(feature = "async-network-client-async-std", async_std::test)
336336
)]
337337
async fn fetch_empty_pack() -> crate::Result {
338-
let daemon = spawn_git_daemon_if_async(repo_path("base"))?;
339-
for (fetch_tags, expected_ref_count) in [
340-
(fetch::Tags::None, 1),
341-
(fetch::Tags::Included, 7),
342-
(fetch::Tags::All, 7),
338+
for version in [
339+
gix::protocol::transport::Protocol::V1,
340+
gix::protocol::transport::Protocol::V2,
343341
] {
344-
let (repo, _tmp) = repo_rw("two-origins");
345-
let mut remote = into_daemon_remote_if_async(
346-
repo.head()?
347-
.into_remote(Fetch)
348-
.expect("present")?
349-
.with_fetch_tags(fetch_tags),
350-
daemon.as_ref(),
351-
None,
352-
);
353-
remote.replace_refspecs(Some("HEAD:refs/remotes/origin/does-not-yet-exist"), Fetch)?;
354-
355-
let res = remote
356-
.connect(Fetch)
357-
.await?
358-
.prepare_fetch(gix::progress::Discard, Default::default())
359-
.await?
360-
.receive(gix::progress::Discard, &AtomicBool::default())
361-
.await?;
342+
for (shallow_args, expected) in [(None, [1, 7, 7]), (Some("--depth=2"), [1, 2, 2])] {
343+
if version == gix::protocol::transport::Protocol::V1 && shallow_args.is_some() {
344+
// TODO: We cannot yet handle shallow mode for V1 as it will send shallow-info as part of the handshake :/.
345+
// It's probaby not the most important thing to be able to clone from a shallow remote anyway.
346+
continue;
347+
}
348+
for (fetch_tags, expected_ref_count) in [fetch::Tags::None, fetch::Tags::Included, fetch::Tags::All]
349+
.into_iter()
350+
.zip(expected)
351+
{
352+
let (mut repo, _tmp) = try_repo_rw_args(
353+
"two-origins",
354+
shallow_args,
355+
if shallow_args.is_some() {
356+
Mode::CloneWithShallowSupport
357+
} else {
358+
Mode::FastClone
359+
},
360+
)?;
361+
let daemon = spawn_git_daemon_if_async(
362+
repo.work_dir()
363+
.expect("non-bare")
364+
.ancestors()
365+
.nth(1)
366+
.expect("parent")
367+
.join("base"),
368+
)?;
369+
repo.config_snapshot_mut().set_value(
370+
&gix::config::tree::Protocol::VERSION,
371+
(version as u8).to_string().as_str(),
372+
)?;
373+
let mut remote = into_daemon_remote_if_async(
374+
repo.head()?
375+
.into_remote(Fetch)
376+
.expect("present")?
377+
.with_fetch_tags(fetch_tags),
378+
daemon.as_ref(),
379+
None,
380+
);
381+
remote.replace_refspecs(Some("HEAD:refs/remotes/origin/does-not-yet-exist"), Fetch)?;
382+
383+
let res = remote
384+
.connect(Fetch)
385+
.await?
386+
.prepare_fetch(gix::progress::Discard, Default::default())
387+
.await?
388+
.receive(gix::progress::Discard, &AtomicBool::default())
389+
.await?;
362390

363-
match res.status {
364-
fetch::Status::NoPackReceived { update_refs } => {
365-
assert_eq!(update_refs.edits.len(), expected_ref_count);
391+
match res.status {
392+
fetch::Status::NoPackReceived { update_refs } => {
393+
assert_eq!(update_refs.edits.len(), expected_ref_count, "{shallow_args:?}|{fetch_tags:?}");
394+
},
395+
_ => unreachable!(
396+
"{shallow_args:?}|{fetch_tags:?}: default negotiation is able to realize nothing is required and doesn't get to receiving a pack"
397+
),
398+
}
366399
}
367-
_ => unreachable!(
368-
"default negotiation is able to realize nothing is required and doesn't get to receiving a pack"
369-
),
370400
}
371401
}
372402
Ok(())

0 commit comments

Comments
 (0)