Skip to content

Commit bb79ae2

Browse files
committed
feat!: move all possible code from gix to gix-protocol.
For now, just move the code down and immediately re-integrate in `gix` to be able to use its tests to validate it. This is a breaking change as some types move and change the layout.
1 parent d41cc0b commit bb79ae2

File tree

20 files changed

+545
-383
lines changed

20 files changed

+545
-383
lines changed

Cargo.lock

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

gix-protocol/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ required-features = ["async-client"]
5050
gix-features = { version = "^0.39.1", path = "../gix-features", features = [
5151
"progress",
5252
] }
53+
gix-trace = { version = "^0.1.11", path = "../gix-trace" }
5354
gix-transport = { version = "^0.43.1", path = "../gix-transport" }
5455
gix-hash = { version = "^0.15.1", path = "../gix-hash" }
5556
gix-date = { version = "^0.9.2", path = "../gix-date" }
5657
gix-credentials = { version = "^0.25.1", path = "../gix-credentials" }
5758
gix-utils = { version = "^0.1.13", path = "../gix-utils" }
59+
gix-refspec = { version = "^0.27.0", path = "../gix-refspec" }
5860

5961
thiserror = "2.0.0"
6062
serde = { version = "1.0.114", optional = true, default-features = false, features = [

gix-protocol/src/fetch/mod.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
/// A module providing low-level primitives to flexibly perform various `fetch` related activities. Note that the typesystem isn't used
2+
/// to assure they are always performed in the right order, the caller has to follow some parts of the protocol itself.
3+
///
4+
/// ### Order for receiving a pack
5+
///
6+
/// * [handshake](handshake())
7+
/// * **ls-refs**
8+
/// * [get available refs by refspecs](RefMap::new())
9+
/// * **fetch pack**
10+
/// * `negotiate` until a pack can be received (TBD)
11+
/// * [officially terminate the connection](crate::indicate_end_of_interaction())
12+
/// - Consider wrapping the transport in [`SendFlushOnDrop`](crate::SendFlushOnDrop) to be sure the connection is terminated
13+
/// gracefully even if there is an application error.
14+
///
15+
/// Note that this flow doesn't involve actually writing the pack, or indexing it. Nor does it contain machinery
16+
/// to write or update references based on the fetched remote references.
17+
///
18+
/// Also, when the server supports [version 2](crate::transport::Protocol::V2) of the protocol, then each of the listed commands,
19+
/// `ls-refs` and `fetch` can be invoked multiple times in any order.
120
mod arguments;
221
pub use arguments::Arguments;
322

@@ -9,3 +28,34 @@ pub use response::Response;
928

1029
mod handshake;
1130
pub use handshake::upload_pack as handshake;
31+
32+
///
33+
pub mod refmap;
34+
35+
/// Information about the relationship between our refspecs, and remote references with their local counterparts.
36+
///
37+
/// It's the first stage that offers connection to the server, and is typically required to perform one or more fetch operations.
38+
#[derive(Default, Debug, Clone)]
39+
pub struct RefMap {
40+
/// A mapping between a remote reference and a local tracking branch.
41+
pub mappings: Vec<refmap::Mapping>,
42+
/// The explicit refspecs that were supposed to be used for fetching.
43+
///
44+
/// Typically, they are configured by the remote and are referred to by
45+
/// [`refmap::SpecIndex::ExplicitInRemote`] in [`refmap::Mapping`].
46+
pub refspecs: Vec<gix_refspec::RefSpec>,
47+
/// Refspecs which have been added implicitly due to settings of the `remote`, usually pre-initialized from
48+
/// [`extra_refspecs` in RefMap options](refmap::init::Options).
49+
/// They are referred to by [`refmap::SpecIndex::Implicit`] in [`refmap::Mapping`].
50+
///
51+
/// They are never persisted nor are they typically presented to the user.
52+
pub extra_refspecs: Vec<gix_refspec::RefSpec>,
53+
/// Information about the fixes applied to the `mapping` due to validation and sanitization.
54+
pub fixes: Vec<gix_refspec::match_group::validate::Fix>,
55+
/// All refs advertised by the remote.
56+
pub remote_refs: Vec<crate::handshake::Ref>,
57+
/// The kind of hash used for all data sent by the server, if understood by this client implementation.
58+
///
59+
/// It was extracted from the `handshake` as advertised by the server.
60+
pub object_hash: gix_hash::Kind,
61+
}

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

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use std::borrow::Cow;
2+
use std::collections::HashSet;
3+
4+
use crate::fetch::refmap::{Mapping, Source, SpecIndex};
5+
use crate::fetch::RefMap;
6+
use crate::transport::client::Transport;
7+
use bstr::{BString, ByteVec};
8+
use gix_features::progress::Progress;
9+
10+
/// The error returned by [`RefMap::new()`].
11+
#[derive(Debug, thiserror::Error)]
12+
#[allow(missing_docs)]
13+
pub enum Error {
14+
#[error("The object format {format:?} as used by the remote is unsupported")]
15+
UnknownObjectFormat { format: BString },
16+
#[error(transparent)]
17+
MappingValidation(#[from] gix_refspec::match_group::validate::Error),
18+
#[error(transparent)]
19+
ListRefs(#[from] crate::ls_refs::Error),
20+
}
21+
22+
/// For use in [`RefMap::new()`].
23+
#[derive(Debug, Clone)]
24+
pub struct Options {
25+
/// Use a two-component prefix derived from the ref-spec's source, like `refs/heads/` to let the server pre-filter refs
26+
/// with great potential for savings in traffic and local CPU time. Defaults to `true`.
27+
pub prefix_from_spec_as_filter_on_remote: bool,
28+
/// A list of refspecs to use as implicit refspecs which won't be saved or otherwise be part of the remote in question.
29+
///
30+
/// This is useful for handling `remote.<name>.tagOpt` for example.
31+
pub extra_refspecs: Vec<gix_refspec::RefSpec>,
32+
}
33+
34+
/// For use in [`RefMap::new()`].
35+
pub struct Context<'a, T> {
36+
/// The outcome of the handshake performed with the remote.
37+
///
38+
/// Note that it's mutable as depending on the protocol, it may contain refs that have been sent unconditionally.
39+
pub handshake: &'a mut crate::handshake::Outcome,
40+
/// The transport to use when making an `ls-refs` call. This is always done if the underlying protocol is V2, which is
41+
/// implied by the absence of refs in the `handshake` outcome.
42+
pub transport: &'a mut T,
43+
/// How to identify during the `ls-refs` call.
44+
///
45+
/// This could be read from the `gitoxide.userAgent` configuration variable.
46+
pub user_agent: (&'static str, Option<Cow<'static, str>>),
47+
/// If `true`, output all packetlines using the the `gix-trace` machinery.
48+
pub trace_packetlines: bool,
49+
}
50+
51+
impl Default for Options {
52+
fn default() -> Self {
53+
Options {
54+
prefix_from_spec_as_filter_on_remote: true,
55+
extra_refspecs: Vec::new(),
56+
}
57+
}
58+
}
59+
60+
impl RefMap {
61+
/// Create a new instance by obtaining all references on the remote that have been filtered through our remote's
62+
/// for _fetching_. A [context](Context) is provided to bundle what would be additional parameters,
63+
/// and [options](Options) are used to further configure the call.
64+
///
65+
/// * `progress` is used if `ls-refs` is invoked on the remote.
66+
/// * `fetch_refspecs` are all explicit refspecs to identify references on the remote that you are interested in.
67+
/// Note that these are copied to [`RefMap::refspecs`] for convenience, as `RefMap::mappings` refer to them by index.
68+
#[allow(clippy::result_large_err)]
69+
#[maybe_async::maybe_async]
70+
pub async fn new<T>(
71+
mut progress: impl Progress,
72+
fetch_refspecs: &[gix_refspec::RefSpec],
73+
Context {
74+
handshake,
75+
transport,
76+
user_agent,
77+
trace_packetlines,
78+
}: Context<'_, T>,
79+
Options {
80+
prefix_from_spec_as_filter_on_remote,
81+
extra_refspecs,
82+
}: Options,
83+
) -> Result<Self, Error>
84+
where
85+
T: Transport,
86+
{
87+
let _span = gix_trace::coarse!("gix_protocol::fetch::RefMap::new()");
88+
let null = gix_hash::ObjectId::null(gix_hash::Kind::Sha1); // OK to hardcode Sha1, it's not supposed to match, ever.
89+
90+
let all_refspecs = {
91+
let mut s: Vec<_> = fetch_refspecs.to_vec();
92+
s.extend(extra_refspecs.clone());
93+
s
94+
};
95+
let remote_refs = match handshake.refs.take() {
96+
Some(refs) => refs,
97+
None => {
98+
crate::ls_refs(
99+
transport,
100+
&handshake.capabilities,
101+
|_capabilities, arguments, features| {
102+
features.push(user_agent);
103+
if prefix_from_spec_as_filter_on_remote {
104+
let mut seen = HashSet::new();
105+
for spec in &all_refspecs {
106+
let spec = spec.to_ref();
107+
if seen.insert(spec.instruction()) {
108+
let mut prefixes = Vec::with_capacity(1);
109+
spec.expand_prefixes(&mut prefixes);
110+
for mut prefix in prefixes {
111+
prefix.insert_str(0, "ref-prefix ");
112+
arguments.push(prefix);
113+
}
114+
}
115+
}
116+
}
117+
Ok(crate::ls_refs::Action::Continue)
118+
},
119+
&mut progress,
120+
trace_packetlines,
121+
)
122+
.await?
123+
}
124+
};
125+
let num_explicit_specs = fetch_refspecs.len();
126+
let group = gix_refspec::MatchGroup::from_fetch_specs(all_refspecs.iter().map(gix_refspec::RefSpec::to_ref));
127+
let (res, fixes) = group
128+
.match_remotes(remote_refs.iter().map(|r| {
129+
let (full_ref_name, target, object) = r.unpack();
130+
gix_refspec::match_group::Item {
131+
full_ref_name,
132+
target: target.unwrap_or(&null),
133+
object,
134+
}
135+
}))
136+
.validated()?;
137+
138+
let mappings = res.mappings;
139+
let mappings = mappings
140+
.into_iter()
141+
.map(|m| Mapping {
142+
remote: m.item_index.map_or_else(
143+
|| {
144+
Source::ObjectId(match m.lhs {
145+
gix_refspec::match_group::SourceRef::ObjectId(id) => id,
146+
_ => unreachable!("no item index implies having an object id"),
147+
})
148+
},
149+
|idx| Source::Ref(remote_refs[idx].clone()),
150+
),
151+
local: m.rhs.map(std::borrow::Cow::into_owned),
152+
spec_index: if m.spec_index < num_explicit_specs {
153+
SpecIndex::ExplicitInRemote(m.spec_index)
154+
} else {
155+
SpecIndex::Implicit(m.spec_index - num_explicit_specs)
156+
},
157+
})
158+
.collect();
159+
160+
let object_hash = extract_object_format(handshake)?;
161+
Ok(RefMap {
162+
mappings,
163+
refspecs: fetch_refspecs.to_vec(),
164+
extra_refspecs,
165+
fixes,
166+
remote_refs,
167+
object_hash,
168+
})
169+
}
170+
}
171+
172+
/// Assume sha1 if server says nothing, otherwise configure anything beyond sha1 in the local repo configuration
173+
#[allow(clippy::result_large_err)]
174+
fn extract_object_format(outcome: &crate::handshake::Outcome) -> Result<gix_hash::Kind, Error> {
175+
use bstr::ByteSlice;
176+
let object_hash =
177+
if let Some(object_format) = outcome.capabilities.capability("object-format").and_then(|c| c.value()) {
178+
let object_format = object_format.to_str().map_err(|_| Error::UnknownObjectFormat {
179+
format: object_format.into(),
180+
})?;
181+
match object_format {
182+
"sha1" => gix_hash::Kind::Sha1,
183+
unknown => return Err(Error::UnknownObjectFormat { format: unknown.into() }),
184+
}
185+
} else {
186+
gix_hash::Kind::Sha1
187+
};
188+
Ok(object_hash)
189+
}

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
///
2+
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
3+
pub mod init;
4+
5+
/// Either an object id that the remote has or the matched remote ref itself.
6+
#[derive(Debug, Clone)]
7+
pub enum Source {
8+
/// An object id, as the matched ref-spec was an object id itself.
9+
ObjectId(gix_hash::ObjectId),
10+
/// The remote reference that matched the ref-specs name.
11+
Ref(crate::handshake::Ref),
12+
}
13+
14+
impl Source {
15+
/// Return either the direct object id we refer to or the direct target that a reference refers to.
16+
/// The latter may be a direct or a symbolic reference.
17+
/// If unborn, `None` is returned.
18+
pub fn as_id(&self) -> Option<&gix_hash::oid> {
19+
match self {
20+
Source::ObjectId(id) => Some(id),
21+
Source::Ref(r) => r.unpack().1,
22+
}
23+
}
24+
25+
/// Return the target that this symbolic ref is pointing to, or `None` if it is no symbolic ref.
26+
pub fn as_target(&self) -> Option<&bstr::BStr> {
27+
match self {
28+
Source::ObjectId(_) => None,
29+
Source::Ref(r) => match r {
30+
crate::handshake::Ref::Peeled { .. } | crate::handshake::Ref::Direct { .. } => None,
31+
crate::handshake::Ref::Symbolic { target, .. } | crate::handshake::Ref::Unborn { target, .. } => {
32+
Some(target.as_ref())
33+
}
34+
},
35+
}
36+
}
37+
38+
/// Returns the peeled id of this instance, that is the object that can't be de-referenced anymore.
39+
pub fn peeled_id(&self) -> Option<&gix_hash::oid> {
40+
match self {
41+
Source::ObjectId(id) => Some(id),
42+
Source::Ref(r) => {
43+
let (_name, target, peeled) = r.unpack();
44+
peeled.or(target)
45+
}
46+
}
47+
}
48+
49+
/// Return ourselves as the full name of the reference we represent, or `None` if this source isn't a reference but an object.
50+
pub fn as_name(&self) -> Option<&bstr::BStr> {
51+
match self {
52+
Source::ObjectId(_) => None,
53+
Source::Ref(r) => match r {
54+
crate::handshake::Ref::Unborn { full_ref_name, .. }
55+
| crate::handshake::Ref::Symbolic { full_ref_name, .. }
56+
| crate::handshake::Ref::Direct { full_ref_name, .. }
57+
| crate::handshake::Ref::Peeled { full_ref_name, .. } => Some(full_ref_name.as_ref()),
58+
},
59+
}
60+
}
61+
}
62+
63+
/// An index into various lists of refspecs that have been used in a [Mapping] of remote references to local ones.
64+
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
65+
pub enum SpecIndex {
66+
/// An index into the _refspecs of the remote_ that triggered a fetch operation.
67+
/// These refspecs are explicit and visible to the user.
68+
ExplicitInRemote(usize),
69+
/// An index into the list of [extra refspecs][crate::remote::fetch::RefMap::extra_refspecs] that are implicit
70+
/// to a particular fetch operation.
71+
Implicit(usize),
72+
}
73+
74+
impl SpecIndex {
75+
/// Depending on our index variant, get the index either from `refspecs` or from `extra_refspecs` for `Implicit` variants.
76+
pub fn get<'a>(
77+
self,
78+
refspecs: &'a [gix_refspec::RefSpec],
79+
extra_refspecs: &'a [gix_refspec::RefSpec],
80+
) -> Option<&'a gix_refspec::RefSpec> {
81+
match self {
82+
SpecIndex::ExplicitInRemote(idx) => refspecs.get(idx),
83+
SpecIndex::Implicit(idx) => extra_refspecs.get(idx),
84+
}
85+
}
86+
87+
/// If this is an `Implicit` variant, return its index.
88+
pub fn implicit_index(self) -> Option<usize> {
89+
match self {
90+
SpecIndex::Implicit(idx) => Some(idx),
91+
SpecIndex::ExplicitInRemote(_) => None,
92+
}
93+
}
94+
}
95+
96+
/// A mapping between a single remote reference and its advertised objects to a local destination which may or may not exist.
97+
#[derive(Debug, Clone)]
98+
pub struct Mapping {
99+
/// The reference on the remote side, along with information about the objects they point to as advertised by the server.
100+
pub remote: Source,
101+
/// The local tracking reference to update after fetching the object visible via `remote`.
102+
pub local: Option<bstr::BString>,
103+
/// The index into the fetch ref-specs used to produce the mapping, allowing it to be recovered.
104+
pub spec_index: SpecIndex,
105+
}

gix-protocol/src/lib.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))]
1111
#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
1212

13+
/// A function that performs a given credential action, trying to obtain credentials for an operation that needs it.
14+
///
15+
/// Useful for both `fetch` and `push`.
16+
pub type AuthenticateFn<'a> = Box<dyn FnMut(gix_credentials::helper::Action) -> gix_credentials::protocol::Result + 'a>;
17+
1318
/// A selector for V2 commands to invoke on the server for purpose of pre-invocation validation.
1419
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
1520
pub enum Command {
@@ -54,6 +59,4 @@ pub mod ls_refs;
5459
pub use ls_refs::function::ls_refs;
5560

5661
mod util;
57-
pub use util::agent;
58-
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
59-
pub use util::indicate_end_of_interaction;
62+
pub use util::*;

0 commit comments

Comments
 (0)