Skip to content

Commit af0ef2f

Browse files
committed
feat: use gix-negotiate in fetch machinery.
Thanks to it we are finally able to do pack negotiations just like git can, as many rounds as it takes and with all available algorithms. Works for V1 and V2 and for stateless and stateful transports.
1 parent 6a3c021 commit af0ef2f

File tree

19 files changed

+882
-292
lines changed

19 files changed

+882
-292
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crate-status.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ Check out the [performance discussion][gix-traverse-performance] as well.
233233
* [x] nested traversal
234234
* **commits**
235235
* [x] ancestor graph traversal similar to `git revlog`
236+
* [ ] `commitgraph` support
236237
* [x] API documentation
237238
* [ ] Examples
238239

@@ -670,8 +671,11 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/gix-lock/README.
670671
* [x] fetch
671672
* [x] shallow (remains shallow, options to adjust shallow boundary)
672673
* [ ] a way to auto-explode small packs to avoid them to pile up
673-
* [ ] 'ref-in-want'
674-
* [ ] standard negotiation algorithms (right now we only have a 'naive' one)
674+
* [x] 'ref-in-want'
675+
* [ ] 'wanted-ref'
676+
* [ ] standard negotiation algorithms `consecutive`, `skipping` and `noop`.
677+
* [ ] a more efficient way to deal [with common `have`](https://github.com/git/git/blob/9e49351c3060e1fa6e0d2de64505b7becf157f28/fetch-pack.c#L525)
678+
during negotiation - we would submit known non-common `haves` each round in stateless transports whereas git prunes the set to known common ones.
675679
* [ ] push
676680
* [x] ls-refs
677681
* [x] ls-refs with ref-spec filter

gix/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ gix-object = { version = "^0.29.2", path = "../gix-object" }
131131
gix-actor = { version = "^0.20.0", path = "../gix-actor" }
132132
gix-pack = { version = "^0.35.0", path = "../gix-pack", features = ["object-cache-dynamic"] }
133133
gix-revision = { version = "^0.14.0", path = "../gix-revision" }
134+
gix-negotiate = { version = "0.1.0", path = "../gix-negotiate" }
134135

135136
gix-path = { version = "^0.8.0", path = "../gix-path" }
136137
gix-url = { version = "^0.18.0", path = "../gix-url" }

gix/src/clone/fetch/mod.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ impl PrepareFetch {
4444
/// it was newly initialized.
4545
///
4646
/// Note that all data we created will be removed once this instance drops if the operation wasn't successful.
47-
pub fn fetch_only<P>(
47+
///
48+
/// ### Note for users of `async`
49+
///
50+
/// Even though
51+
#[gix_protocol::maybe_async::maybe_async]
52+
pub async fn fetch_only<P>(
4853
&mut self,
4954
mut progress: P,
5055
should_interrupt: &std::sync::atomic::AtomicBool,
@@ -101,17 +106,19 @@ impl PrepareFetch {
101106
.expect("valid")
102107
.to_owned();
103108
let pending_pack: remote::fetch::Prepare<'_, '_, _> = {
104-
let mut connection = remote.connect(remote::Direction::Fetch)?;
109+
let mut connection = remote.connect(remote::Direction::Fetch).await?;
105110
if let Some(f) = self.configure_connection.as_mut() {
106111
f(&mut connection).map_err(|err| Error::RemoteConnection(err))?;
107112
}
108-
connection.prepare_fetch(&mut progress, {
109-
let mut opts = self.fetch_options.clone();
110-
if !opts.extra_refspecs.contains(&head_refspec) {
111-
opts.extra_refspecs.push(head_refspec)
112-
}
113-
opts
114-
})?
113+
connection
114+
.prepare_fetch(&mut progress, {
115+
let mut opts = self.fetch_options.clone();
116+
if !opts.extra_refspecs.contains(&head_refspec) {
117+
opts.extra_refspecs.push(head_refspec)
118+
}
119+
opts
120+
})
121+
.await?
115122
};
116123
if pending_pack.ref_map().object_hash != repo.object_hash() {
117124
unimplemented!("configure repository to expect a different object hash as advertised by the server")
@@ -127,7 +134,8 @@ impl PrepareFetch {
127134
message: reflog_message.clone(),
128135
})
129136
.with_shallow(self.shallow.clone())
130-
.receive(progress, should_interrupt)?;
137+
.receive(progress, should_interrupt)
138+
.await?;
131139

132140
util::append_config_to_repo_config(repo, config);
133141
util::update_head(
@@ -141,6 +149,7 @@ impl PrepareFetch {
141149
}
142150

143151
/// Similar to [`fetch_only()`][Self::fetch_only()`], but passes ownership to a utility type to configure a checkout operation.
152+
#[cfg(feature = "blocking-network-client")]
144153
pub fn fetch_then_checkout<P>(
145154
&mut self,
146155
progress: P,
@@ -155,5 +164,4 @@ impl PrepareFetch {
155164
}
156165
}
157166

158-
#[cfg(feature = "blocking-network-client")]
159167
mod util;

gix/src/clone/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ mod access_feat {
160160
}
161161

162162
///
163-
#[cfg(feature = "blocking-network-client")]
163+
#[cfg(any(feature = "async-network-client-async-std", feature = "blocking-network-client"))]
164164
pub mod fetch;
165165

166166
///

gix/src/config/tree/keys.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,8 @@ pub mod validate {
511511
gix_config::Integer::try_from(value)?
512512
.to_decimal()
513513
.ok_or_else(|| format!("integer {value} cannot be represented as `usize`"))?,
514-
)?;
514+
)
515+
.map_err(|_| "cannot use sign for unsigned integer")?;
515516
Ok(())
516517
}
517518
}

gix/src/config/tree/sections/protocol.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use crate::{
66
impl Protocol {
77
/// The `protocol.allow` key.
88
pub const ALLOW: Allow = Allow::new_with_validate("allow", &config::Tree::PROTOCOL, validate::Allow);
9+
/// The `protocol.version` key.
10+
pub const VERSION: Version = Version::new_with_validate("version", &config::Tree::PROTOCOL, validate::Version);
911

1012
/// The `protocol.<name>` subsection
1113
pub const NAME_PARAMETER: NameParameter = NameParameter;
@@ -14,6 +16,9 @@ impl Protocol {
1416
/// The `protocol.allow` key type.
1517
pub type Allow = keys::Any<validate::Allow>;
1618

19+
/// The `protocol.version` key.
20+
pub type Version = keys::Any<validate::Version>;
21+
1722
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
1823
mod allow {
1924
use std::borrow::Cow;
@@ -39,7 +44,7 @@ mod allow {
3944
pub struct NameParameter;
4045

4146
impl NameParameter {
42-
/// The `credential.<url>.helper` key.
47+
/// The `protocol.<name>.allow` key.
4348
pub const ALLOW: Allow = Allow::new_with_validate("allow", &Protocol::NAME_PARAMETER, validate::Allow);
4449
}
4550

@@ -63,14 +68,46 @@ impl Section for Protocol {
6368
}
6469

6570
fn keys(&self) -> &[&dyn Key] {
66-
&[&Self::ALLOW]
71+
&[&Self::ALLOW, &Self::VERSION]
6772
}
6873

6974
fn sub_sections(&self) -> &[&dyn Section] {
7075
&[&Self::NAME_PARAMETER]
7176
}
7277
}
7378

79+
mod key_impls {
80+
impl super::Version {
81+
/// Convert `value` into the corresponding protocol version, possibly applying the correct default.
82+
#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
83+
pub fn try_into_protocol_version(
84+
&'static self,
85+
value: Option<Result<i64, gix_config::value::Error>>,
86+
) -> Result<gix_protocol::transport::Protocol, crate::config::key::GenericErrorWithValue> {
87+
let value = match value {
88+
None => return Ok(gix_protocol::transport::Protocol::V2),
89+
Some(v) => v,
90+
};
91+
Ok(match value {
92+
Ok(0) => gix_protocol::transport::Protocol::V0,
93+
Ok(1) => gix_protocol::transport::Protocol::V1,
94+
Ok(2) => gix_protocol::transport::Protocol::V2,
95+
Ok(other) => {
96+
return Err(crate::config::key::GenericErrorWithValue::from_value(
97+
self,
98+
other.to_string().into(),
99+
))
100+
}
101+
Err(err) => {
102+
return Err(
103+
crate::config::key::GenericErrorWithValue::from_value(self, "unknown".into()).with_source(err),
104+
)
105+
}
106+
})
107+
}
108+
}
109+
}
110+
74111
mod validate {
75112
use crate::{bstr::BStr, config::tree::keys};
76113

@@ -82,4 +119,17 @@ mod validate {
82119
Ok(())
83120
}
84121
}
122+
123+
pub struct Version;
124+
impl keys::Validate for Version {
125+
fn validate(&self, value: &BStr) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
126+
let value = gix_config::Integer::try_from(value)?
127+
.to_decimal()
128+
.ok_or_else(|| format!("integer {value} cannot be represented as integer"))?;
129+
match value {
130+
0 | 1 | 2 => Ok(()),
131+
_ => Err(format!("protocol version {value} is unknown").into()),
132+
}
133+
}
134+
}
85135
}

gix/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub use gix_ignore as ignore;
8181
#[doc(inline)]
8282
pub use gix_index as index;
8383
pub use gix_lock as lock;
84+
pub use gix_negotiate as negotiate;
8485
pub use gix_object as objs;
8586
pub use gix_object::bstr;
8687
pub use gix_odb as odb;

gix/src/remote/connect.rs

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ mod error {
2424
Connect(#[from] gix_protocol::transport::client::connect::Error),
2525
#[error("The {} url was missing - don't know where to establish a connection to", direction.as_str())]
2626
MissingUrl { direction: remote::Direction },
27-
#[error("Protocol named {given:?} is not a valid protocol. Choose between 1 and 2")]
28-
UnknownProtocol { given: BString },
27+
#[error("The given protocol version was invalid. Choose between 1 and 2")]
28+
UnknownProtocol { source: config::key::GenericErrorWithValue },
2929
#[error("Could not verify that \"{}\" url is a valid git directory before attempting to use it", url.to_bstring())]
3030
FileUrl {
3131
source: Box<gix_discover::is_git::Error>,
@@ -128,25 +128,9 @@ impl<'repo> Remote<'repo> {
128128
Ok(url)
129129
}
130130

131-
use gix_protocol::transport::Protocol;
132-
let version = self
133-
.repo
134-
.config
135-
.resolved
136-
.integer("protocol", None, "version")
137-
.unwrap_or(Ok(2))
138-
.map_err(|err| Error::UnknownProtocol { given: err.input })
139-
.and_then(|num| {
140-
Ok(match num {
141-
1 => Protocol::V1,
142-
2 => Protocol::V2,
143-
num => {
144-
return Err(Error::UnknownProtocol {
145-
given: num.to_string().into(),
146-
})
147-
}
148-
})
149-
})?;
131+
let version = crate::config::tree::Protocol::VERSION
132+
.try_into_protocol_version(self.repo.config.resolved.integer("protocol", None, "version"))
133+
.map_err(|err| Error::UnknownProtocol { source: err })?;
150134

151135
let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned();
152136
if !self.repo.config.url_scheme()?.allow(&url.scheme) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@ impl RefLogMessage {
4343
pub enum Status {
4444
/// Nothing changed as the remote didn't have anything new compared to our tracking branches, thus no pack was received
4545
/// and no new object was added.
46+
///
47+
/// As we could determine that nothing changed without remote interaction, there was no negotiation at all.
4648
NoPackReceived {
4749
/// However, depending on the refspecs, references might have been updated nonetheless to point to objects as
4850
/// reported by the remote.
4951
update_refs: refs::update::Outcome,
5052
},
5153
/// There was at least one tip with a new object which we received.
5254
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,
5357
/// Information collected while writing the pack and its index.
5458
write_pack_bundle: gix_pack::bundle::write::Outcome,
5559
/// Information collected while updating references.
@@ -58,6 +62,8 @@ pub enum Status {
5862
/// A dry run was performed which leaves the local repository without any change
5963
/// nor will a pack have been received.
6064
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,
6167
/// Information about what updates to refs would have been done.
6268
update_refs: refs::update::Outcome,
6369
},

0 commit comments

Comments
 (0)