Skip to content

Commit 6f9d32e

Browse files
committed
feat!: provide fetch::outcome::Negotiate for details on what happened during negotiation.
We also remove the `negotiation_rounds` field in favor of a far more detailed `fetch::outcome::Negotiate` struct.
1 parent 2536f49 commit 6f9d32e

File tree

4 files changed

+89
-49
lines changed

4 files changed

+89
-49
lines changed

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

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,27 +46,25 @@ pub enum Status {
4646
///
4747
/// As we could determine that nothing changed without remote interaction, there was no negotiation at all.
4848
NoPackReceived {
49+
/// If `true`, we didn't receive a pack due to dry-run mode being enabled.
50+
dry_run: bool,
51+
/// Information about the pack negotiation phase if negotiation happened at all.
52+
///
53+
/// It's possible that negotiation didn't have to happen as no reference of interest changed on the server.
54+
negotiate: Option<outcome::Negotiate>,
4955
/// However, depending on the refspecs, references might have been updated nonetheless to point to objects as
5056
/// reported by the remote.
5157
update_refs: refs::update::Outcome,
5258
},
5359
/// There was at least one tip with a new object which we received.
5460
Change {
55-
/// The number of rounds it took to minimize the pack to contain only the objects we don't have.
56-
negotiation_rounds: usize,
61+
/// Information about the pack negotiation phase.
62+
negotiate: outcome::Negotiate,
5763
/// Information collected while writing the pack and its index.
5864
write_pack_bundle: gix_pack::bundle::write::Outcome,
5965
/// Information collected while updating references.
6066
update_refs: refs::update::Outcome,
6167
},
62-
/// A dry run was performed which leaves the local repository without any change
63-
/// nor will a pack have been received.
64-
DryRun {
65-
/// The number of rounds it took to minimize the *would-be-sent*-pack to contain only the objects we don't have.
66-
negotiation_rounds: usize,
67-
/// Information about what updates to refs would have been done.
68-
update_refs: refs::update::Outcome,
69-
},
7068
}
7169

7270
/// The outcome of receiving a pack via [`Prepare::receive()`].
@@ -78,6 +76,42 @@ pub struct Outcome {
7876
pub status: Status,
7977
}
8078

79+
/// Additional types related to the outcome of a fetch operation.
80+
pub mod outcome {
81+
/// Information about the negotiation phase of a fetch.
82+
///
83+
/// Note that negotiation can happen even if no pack is ultimately produced.
84+
#[derive(Default, Debug, Clone)]
85+
pub struct Negotiate {
86+
/// The negotiation graph indicating what kind of information 'the algorithm' collected in the end.
87+
pub graph: gix_revision::graph::IdMap<gix_revision::graph::Commit<gix_negotiate::Metadata>>,
88+
/// Additional information for each round of negotiation.
89+
pub rounds: Vec<negotiate::Round>,
90+
}
91+
92+
///
93+
pub mod negotiate {
94+
/// Key information about each round in the pack-negotiation.
95+
#[derive(Debug, Clone)]
96+
pub struct Round {
97+
/// The amount of `HAVE` lines sent this round.
98+
///
99+
/// Each `HAVE` is an object that we tell the server about which would acknowledge each one it has as well.
100+
pub haves_sent: usize,
101+
/// A total counter, over all previous rounds, indicating how many `HAVE`s we sent without seeing a single acknowledgement,
102+
/// i.e. the indication of a common object.
103+
///
104+
/// This number maybe zero or be lower compared to the previous round if we have received at least one acknowledgement.
105+
pub in_vain: usize,
106+
/// The amount of haves we should send in this round.
107+
///
108+
/// If the value is lower than `haves_sent` (the `HAVE` lines actually sent), the negotiation algorithm has run out of options
109+
/// which typically indicates the end of the negotiation phase.
110+
pub haves_to_send: usize,
111+
}
112+
}
113+
}
114+
81115
/// The progress ids used in during various steps of the fetch operation.
82116
///
83117
/// Note that tagged progress isn't very widely available yet, but support can be improved as needed.

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

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ use crate::{
1919
connection::fetch::config,
2020
fetch,
2121
fetch::{
22-
negotiate, negotiate::Algorithm, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Shallow, Status,
22+
negotiate, negotiate::Algorithm, outcome, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage,
23+
Shallow, Status,
2324
},
2425
},
2526
Progress, Repository,
@@ -141,11 +142,10 @@ where
141142
negotiate::make_refmapping_ignore_predicate(con.remote.fetch_tags, &self.ref_map),
142143
)?;
143144
let mut previous_response = None::<gix_protocol::fetch::Response>;
144-
let mut round = 1;
145-
let mut write_pack_bundle = match &action {
145+
let (mut write_pack_bundle, negotiate) = match &action {
146146
negotiate::Action::NoChange | negotiate::Action::SkipToRefUpdate => {
147147
gix_protocol::indicate_end_of_interaction(&mut con.transport).await.ok();
148-
None
148+
(None, None)
149149
}
150150
negotiate::Action::MustNegotiate {
151151
remote_ref_target_known,
@@ -158,6 +158,7 @@ where
158158
&self.shallow,
159159
negotiate::make_refmapping_ignore_predicate(con.remote.fetch_tags, &self.ref_map),
160160
);
161+
let mut rounds = Vec::new();
161162
let is_stateless =
162163
arguments.is_stateless(!con.transport.connection_persists_across_multiple_requests());
163164
let mut haves_to_send = gix_negotiate::window_size(is_stateless, None);
@@ -166,7 +167,7 @@ where
166167
let mut common = is_stateless.then(Vec::new);
167168
let reader = 'negotiation: loop {
168169
progress.step();
169-
progress.set_name(format!("negotiate (round {round})"));
170+
progress.set_name(format!("negotiate (round {})", rounds.len() + 1));
170171

171172
let is_done = match negotiate::one_round(
172173
negotiator.deref_mut(),
@@ -182,6 +183,11 @@ where
182183
}
183184
seen_ack |= ack_seen;
184185
in_vain += haves_sent;
186+
rounds.push(outcome::negotiate::Round {
187+
haves_sent,
188+
in_vain,
189+
haves_to_send,
190+
});
185191
let is_done = haves_sent != haves_to_send || (seen_ack && in_vain >= 256);
186192
haves_to_send = gix_negotiate::window_size(is_stateless, haves_to_send);
187193
is_done
@@ -206,11 +212,9 @@ where
206212
setup_remote_progress(progress, &mut reader, should_interrupt);
207213
}
208214
break 'negotiation reader;
209-
} else {
210-
round += 1;
211215
}
212216
};
213-
drop(graph);
217+
let graph = graph.detach();
214218
drop(graph_repo);
215219
let previous_response = previous_response.expect("knowledge of a pack means a response was received");
216220
if !previous_response.shallow_updates().is_empty() && shallow_lock.is_none() {
@@ -267,7 +271,7 @@ where
267271
crate::shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates())?;
268272
}
269273
}
270-
write_pack_bundle
274+
(write_pack_bundle, Some(outcome::Negotiate { graph, rounds }))
271275
}
272276
};
273277

@@ -294,21 +298,17 @@ where
294298

295299
let out = Outcome {
296300
ref_map: std::mem::take(&mut self.ref_map),
297-
status: if matches!(self.dry_run, fetch::DryRun::Yes) {
298-
assert!(write_pack_bundle.is_none(), "in dry run we never read a bundle");
299-
Status::DryRun {
301+
status: match write_pack_bundle {
302+
Some(write_pack_bundle) => Status::Change {
303+
write_pack_bundle,
300304
update_refs,
301-
negotiation_rounds: round,
302-
}
303-
} else {
304-
match write_pack_bundle {
305-
Some(write_pack_bundle) => Status::Change {
306-
write_pack_bundle,
307-
update_refs,
308-
negotiation_rounds: round,
309-
},
310-
None => Status::NoPackReceived { update_refs },
311-
}
305+
negotiate: negotiate.expect("if we have a pack, we always negotiated it"),
306+
},
307+
None => Status::NoPackReceived {
308+
dry_run: matches!(self.dry_run, fetch::DryRun::Yes),
309+
negotiate,
310+
update_refs,
311+
},
312312
},
313313
};
314314
Ok(out)

gix/src/remote/fetch.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ pub mod negotiate {
1111
}
1212

1313
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
14-
pub use super::connection::fetch::{prepare, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Status};
14+
pub use super::connection::fetch::{
15+
outcome, prepare, refs, Error, Outcome, Prepare, ProgressId, RefLogMessage, Status,
16+
};
1517

1618
/// If `Yes`, don't really make changes but do as much as possible to get an idea of what would be done.
1719
#[derive(Debug, Copy, Clone, PartialEq, Eq)]

gix/tests/remote/fetch.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,13 @@ mod blocking_and_async_io {
137137

138138
match out.status {
139139
Status::Change {
140-
negotiation_rounds,
140+
negotiate,
141141
write_pack_bundle,
142142
..
143143
} => {
144144
assert_eq!(
145-
negotiation_rounds, 1,
145+
negotiate.rounds.len(),
146+
1,
146147
"we don't really have a way to see that tips from alternates were added, I think"
147148
);
148149
assert_eq!(
@@ -215,11 +216,12 @@ mod blocking_and_async_io {
215216
match changes.status {
216217
Status::Change {
217218
write_pack_bundle,
218-
negotiation_rounds,
219+
negotiate,
219220
..
220221
} => {
221222
assert_eq!(
222-
negotiation_rounds, expected_negotiation_rounds,
223+
negotiate.rounds.len(),
224+
expected_negotiation_rounds,
223225
"we need multiple rounds"
224226
);
225227
// the server only has our `b1` and an extra commit or two.
@@ -389,8 +391,9 @@ mod blocking_and_async_io {
389391
.await?;
390392

391393
match res.status {
392-
fetch::Status::NoPackReceived { update_refs } => {
394+
fetch::Status::NoPackReceived { update_refs, negotiate: _, dry_run } => {
393395
assert_eq!(update_refs.edits.len(), expected_ref_count, "{shallow_args:?}|{fetch_tags:?}");
396+
assert!(!dry_run, "we actually perform the operation");
394397
},
395398
_ => unreachable!(
396399
"{shallow_args:?}|{fetch_tags:?}: default negotiation is able to realize nothing is required and doesn't get to receiving a pack"
@@ -446,8 +449,8 @@ mod blocking_and_async_io {
446449
.await?;
447450

448451
match res.status {
449-
gix::remote::fetch::Status::Change { write_pack_bundle, update_refs, negotiation_rounds } => {
450-
assert_eq!(negotiation_rounds, 1);
452+
gix::remote::fetch::Status::Change { write_pack_bundle, update_refs, negotiate } => {
453+
assert_eq!(negotiate.rounds.len(), 1);
451454
assert_eq!(write_pack_bundle.index.data_hash, hex_to_id(expected_data_hash), );
452455
assert_eq!(write_pack_bundle.index.num_objects, 3 + num_objects_offset, "{fetch_tags:?}");
453456
assert!(write_pack_bundle.data_path.as_deref().map_or(false, std::path::Path::is_file));
@@ -530,9 +533,9 @@ mod blocking_and_async_io {
530533
fetch::Status::Change {
531534
write_pack_bundle,
532535
update_refs,
533-
negotiation_rounds,
536+
negotiate,
534537
} => {
535-
assert_eq!(negotiation_rounds, 1);
538+
assert_eq!(negotiate.rounds.len(), 1);
536539
assert_eq!(write_pack_bundle.pack_version, gix::odb::pack::data::Version::V2);
537540
assert_eq!(write_pack_bundle.object_hash, repo.object_hash());
538541
assert_eq!(write_pack_bundle.index.num_objects, 4, "{dry_run}: this value is 4 when git does it with 'consecutive' negotiation style, but could be 33 if completely naive.");
@@ -568,16 +571,17 @@ mod blocking_and_async_io {
568571

569572
update_refs
570573
}
571-
fetch::Status::DryRun {
574+
fetch::Status::NoPackReceived {
575+
dry_run,
572576
update_refs,
573-
negotiation_rounds,
577+
negotiate: _,
574578
} => {
575-
assert_eq!(negotiation_rounds, 1);
579+
assert!(
580+
dry_run,
581+
"the only reason we receive no pack is if we are in dry-run mode"
582+
);
576583
update_refs
577584
}
578-
fetch::Status::NoPackReceived { .. } => {
579-
unreachable!("we firmly expect changes here, as the other origin has changes")
580-
}
581585
};
582586

583587
assert_eq!(

0 commit comments

Comments
 (0)