Skip to content

gix-protocol::fetch() without Delegates #1634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 16, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -304,6 +304,7 @@ members = [
"gix-ref/tests",
"gix-config/tests",
"gix-traverse/tests",
"gix-shallow"
]

[workspace.dependencies]
405 changes: 145 additions & 260 deletions gitoxide-core/src/pack/receive.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gitoxide-core/src/repository/clone.rs
Original file line number Diff line number Diff line change
@@ -89,7 +89,7 @@ pub(crate) mod function {

if handshake_info {
writeln!(out, "Handshake Information")?;
writeln!(out, "\t{:?}", fetch_outcome.ref_map.handshake)?;
writeln!(out, "\t{:?}", fetch_outcome.handshake)?;
}

match fetch_outcome.status {
8 changes: 4 additions & 4 deletions gitoxide-core/src/repository/fetch.rs
Original file line number Diff line number Diff line change
@@ -70,7 +70,7 @@ pub(crate) mod function {

if handshake_info {
writeln!(out, "Handshake Information")?;
writeln!(out, "\t{:?}", res.ref_map.handshake)?;
writeln!(out, "\t{:?}", res.handshake)?;
}

let ref_specs = remote.refspecs(gix::remote::Direction::Fetch);
@@ -210,7 +210,7 @@ pub(crate) mod function {
mut out: impl std::io::Write,
mut err: impl std::io::Write,
) -> anyhow::Result<()> {
let mut last_spec_index = gix::remote::fetch::SpecIndex::ExplicitInRemote(usize::MAX);
let mut last_spec_index = gix::remote::fetch::refmap::SpecIndex::ExplicitInRemote(usize::MAX);
let mut updates = update_refs
.iter_mapping_updates(&map.mappings, refspecs, &map.extra_refspecs)
.filter_map(|(update, mapping, spec, edit)| spec.map(|spec| (update, mapping, spec, edit)))
@@ -258,10 +258,10 @@ pub(crate) mod function {

write!(out, "\t")?;
match &mapping.remote {
gix::remote::fetch::Source::ObjectId(id) => {
gix::remote::fetch::refmap::Source::ObjectId(id) => {
write!(out, "{}", id.attach(repo).shorten_or_id())?;
}
gix::remote::fetch::Source::Ref(r) => {
gix::remote::fetch::refmap::Source::Ref(r) => {
crate::repository::remote::refs::print_ref(&mut out, r)?;
}
};
12 changes: 6 additions & 6 deletions gitoxide-core/src/repository/remote.rs
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ mod refs_impl {
use gix::{
protocol::handshake,
refspec::{match_group::validate::Fix, RefSpec},
remote::fetch::Source,
remote::fetch::refmap::Source,
};

use super::by_name_or_url;
@@ -72,7 +72,7 @@ mod refs_impl {
.context("Remote didn't have a URL to connect to")?
.to_bstring()
));
let map = remote
let (map, handshake) = remote
.connect(gix::remote::Direction::Fetch)
.await?
.ref_map(
@@ -86,7 +86,7 @@ mod refs_impl {

if handshake_info {
writeln!(out, "Handshake Information")?;
writeln!(out, "\t{:?}", map.handshake)?;
writeln!(out, "\t{handshake:?}")?;
}
match kind {
refs::Kind::Tracking { .. } => print_refmap(
@@ -119,7 +119,7 @@ mod refs_impl {
mut out: impl std::io::Write,
mut err: impl std::io::Write,
) -> anyhow::Result<()> {
let mut last_spec_index = gix::remote::fetch::SpecIndex::ExplicitInRemote(usize::MAX);
let mut last_spec_index = gix::remote::fetch::refmap::SpecIndex::ExplicitInRemote(usize::MAX);
map.mappings.sort_by_key(|m| m.spec_index);
for mapping in &map.mappings {
if mapping.spec_index != last_spec_index {
@@ -146,11 +146,11 @@ mod refs_impl {

write!(out, "\t")?;
let target_id = match &mapping.remote {
gix::remote::fetch::Source::ObjectId(id) => {
gix::remote::fetch::refmap::Source::ObjectId(id) => {
write!(out, "{id}")?;
id
}
gix::remote::fetch::Source::Ref(r) => print_ref(&mut out, r)?,
gix::remote::fetch::refmap::Source::Ref(r) => print_ref(&mut out, r)?,
};
match &mapping.local {
Some(local) => {
46 changes: 37 additions & 9 deletions gix-protocol/Cargo.toml
Original file line number Diff line number Diff line change
@@ -23,26 +23,46 @@ doctest = false
#! Specifying both causes a compile error, preventing the use of `--all-features`.

## If set, blocking command implementations are available and will use the blocking version of the `gix-transport` crate.
blocking-client = ["gix-transport/blocking-client", "maybe-async/is_sync"]
blocking-client = [
"gix-transport/blocking-client",
"maybe-async/is_sync",
"handshake",
"fetch"
]
## As above, but provides async implementations instead.
async-client = [
"gix-transport/async-client",
"async-trait",
"futures-io",
"futures-lite",
"dep:async-trait",
"dep:futures-io",
"dep:futures-lite",
"handshake",
"fetch"
]

## Add implementations for performing a `handshake` along with the dependencies needed for it.
handshake = ["dep:gix-credentials"]

## Add implementations for performing a `fetch` (for packs) along with the dependencies needed for it.
fetch = [
"dep:gix-negotiate",
"dep:gix-object",
"dep:gix-revwalk",
"dep:gix-lock",
"dep:gix-refspec",
"dep:gix-trace",
]

#! ### Other
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
serde = ["dep:serde", "bstr/serde", "gix-transport/serde", "gix-hash/serde"]
serde = ["dep:serde", "bstr/serde", "gix-transport/serde", "gix-hash/serde", "gix-shallow/serde"]

[[test]]
name = "blocking-client-protocol"
name = "blocking"
path = "tests/blocking-protocol.rs"
required-features = ["blocking-client"]

[[test]]
name = "async-client-protocol"
name = "async"
path = "tests/async-protocol.rs"
required-features = ["async-client"]

@@ -52,9 +72,18 @@ gix-features = { version = "^0.39.1", path = "../gix-features", features = [
] }
gix-transport = { version = "^0.43.1", path = "../gix-transport" }
gix-hash = { version = "^0.15.1", path = "../gix-hash" }
gix-shallow = { version = "^0.1.0", path = "../gix-shallow" }
gix-date = { version = "^0.9.2", path = "../gix-date" }
gix-credentials = { version = "^0.25.1", path = "../gix-credentials" }
gix-utils = { version = "^0.1.13", path = "../gix-utils" }
gix-ref = { version = "^0.49.0", path = "../gix-ref" }

gix-trace = { version = "^0.1.11", path = "../gix-trace", optional = true }
gix-negotiate = { version = "^0.17.0", path = "../gix-negotiate", optional = true }
gix-object = { version = "^0.46.0", path = "../gix-object", optional = true }
gix-revwalk = { version = "^0.17.0", path = "../gix-revwalk", optional = true }
gix-credentials = { version = "^0.25.1", path = "../gix-credentials", optional = true }
gix-refspec = { version = "^0.27.0", path = "../gix-refspec", optional = true }
gix-lock = { version = "^15.0.0", path = "../gix-lock", optional = true }

thiserror = "2.0.0"
serde = { version = "1.0.114", optional = true, default-features = false, features = [
@@ -77,7 +106,6 @@ document-features = { version = "0.2.0", optional = true }
[dev-dependencies]
async-std = { version = "1.9.0", features = ["attributes"] }
gix-packetline = { path = "../gix-packetline", version = "^0.18.1" }
gix-testtools = { path = "../tests/tools" }

[package.metadata.docs.rs]
features = ["blocking-client", "document-features", "serde"]
54 changes: 41 additions & 13 deletions gix-protocol/src/command/mod.rs → gix-protocol/src/command.rs
Original file line number Diff line number Diff line change
@@ -90,9 +90,10 @@ mod with_io {
}
}

/// Compute initial arguments based on the given `features`. They are typically provided by the `default_features(…)` method.
/// Only useful for V2
pub(crate) fn initial_arguments(&self, features: &[Feature]) -> Vec<BString> {
/// Provide the initial arguments based on the given `features`.
/// They are typically provided by the [`Self::default_features`] method.
/// Only useful for V2, and based on heuristics/experimentation.
pub fn initial_v2_arguments(&self, features: &[Feature]) -> Vec<BString> {
match self {
Command::Fetch => ["thin-pack", "ofs-delta"]
.iter()
@@ -157,20 +158,24 @@ mod with_io {
Command::LsRefs => vec![],
}
}
/// Panics if the given arguments and features don't match what's statically known. It's considered a bug in the delegate.
pub(crate) fn validate_argument_prefixes_or_panic(
/// Return an error if the given `arguments` and `features` don't match what's statically known.
pub fn validate_argument_prefixes(
&self,
version: gix_transport::Protocol,
server: &Capabilities,
arguments: &[BString],
features: &[Feature],
) {
) -> Result<(), validate_argument_prefixes::Error> {
use validate_argument_prefixes::Error;
let allowed = self.all_argument_prefixes();
for arg in arguments {
if allowed.iter().any(|allowed| arg.starts_with(allowed.as_bytes())) {
continue;
}
panic!("{}: argument {} is not known or allowed", self.as_str(), arg);
return Err(Error::UnsupportedArgument {
command: self.as_str(),
argument: arg.clone(),
});
}
match version {
gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => {
@@ -181,14 +186,17 @@ mod with_io {
{
continue;
}
panic!("{}: capability {} is not supported", self.as_str(), feature);
return Err(Error::UnsupportedCapability {
command: self.as_str(),
feature: feature.to_string(),
});
}
}
gix_transport::Protocol::V2 => {
let allowed = server
.iter()
.find_map(|c| {
if c.name() == self.as_str().as_bytes().as_bstr() {
if c.name() == self.as_str() {
c.values().map(|v| v.map(ToString::to_string).collect::<Vec<_>>())
} else {
None
@@ -201,14 +209,34 @@ mod with_io {
}
match *feature {
"agent" => {}
_ => panic!("{}: V2 feature/capability {} is not supported", self.as_str(), feature),
_ => {
return Err(Error::UnsupportedCapability {
command: self.as_str(),
feature: feature.to_string(),
})
}
}
}
}
}
Ok(())
}
}
}

#[cfg(test)]
mod tests;
///
pub mod validate_argument_prefixes {
use bstr::BString;

/// The error returned by [Command::validate_argument_prefixes()](super::Command::validate_argument_prefixes()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("{command}: argument {argument} is not known or allowed")]
UnsupportedArgument { command: &'static str, argument: BString },
#[error("{command}: capability {feature} is not supported")]
UnsupportedCapability { command: &'static str, feature: String },
}
}
}
#[cfg(any(test, feature = "async-client", feature = "blocking-client"))]
pub use with_io::validate_argument_prefixes;
5 changes: 4 additions & 1 deletion gix-protocol/src/fetch/arguments/mod.rs
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ pub struct Arguments {
#[cfg(any(feature = "async-client", feature = "blocking-client"))]
version: gix_transport::Protocol,

#[cfg(any(feature = "async-client", feature = "blocking-client"))]
trace: bool,
}

@@ -164,6 +165,7 @@ impl Arguments {
/// Permanently allow the server to include tags that point to commits or objects it would return.
///
/// Needs to only be called once.
#[cfg(any(feature = "async-client", feature = "blocking-client"))]
pub fn use_include_tag(&mut self) {
debug_assert!(self.supports_include_tag, "'include-tag' feature required");
if self.supports_include_tag {
@@ -176,6 +178,7 @@ impl Arguments {
/// Note that sending an unknown or unsupported feature may cause the remote to terminate
/// the connection. Use this method if you know what you are doing *and* there is no specialized
/// method for this, e.g. [`Self::use_include_tag()`].
#[cfg(any(feature = "async-client", feature = "blocking-client"))]
pub fn add_feature(&mut self, feature: &str) {
match self.version {
gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => {
@@ -228,7 +231,7 @@ impl Arguments {
}
gix_transport::Protocol::V2 => {
supports_include_tag = true;
(Command::Fetch.initial_arguments(&features), None)
(Command::Fetch.initial_v2_arguments(&features), None)
}
};

313 changes: 0 additions & 313 deletions gix-protocol/src/fetch/delegate.rs

This file was deleted.

57 changes: 42 additions & 15 deletions gix-protocol/src/fetch/error.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
use std::io;

use gix_transport::client;

use crate::{fetch::response, handshake, ls_refs};

/// The error used in [`fetch()`][crate::fetch()].
/// The error returned by [`fetch()`](crate::fetch()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Could not decode server reply")]
FetchResponse(#[from] crate::fetch::response::Error),
#[error("Cannot fetch from a remote that uses {remote} while local repository uses {local} for object hashes")]
IncompatibleObjectHash {
local: gix_hash::Kind,
remote: gix_hash::Kind,
},
#[error(transparent)]
Handshake(#[from] handshake::Error),
#[error("Could not access repository or failed to read streaming pack file")]
Io(#[from] io::Error),
#[error(transparent)]
Transport(#[from] client::Error),
Negotiate(#[from] crate::fetch::negotiate::Error),
#[error(transparent)]
LsRefs(#[from] ls_refs::Error),
#[error(transparent)]
Response(#[from] response::Error),
Client(#[from] crate::transport::client::Error),
#[error("Server lack feature {feature:?}: {description}")]
MissingServerFeature {
feature: &'static str,
description: &'static str,
},
#[error("Could not write 'shallow' file to incorporate remote updates after fetching")]
WriteShallowFile(#[from] gix_shallow::write::Error),
#[error("Could not read 'shallow' file to send current shallow boundary")]
ReadShallowFile(#[from] gix_shallow::read::Error),
#[error("'shallow' file could not be locked in preparation for writing changes")]
LockShallowFile(#[from] gix_lock::acquire::Error),
#[error("Receiving objects from shallow remotes is prohibited due to the value of `clone.rejectShallow`")]
RejectShallowRemote,
#[error("None of the refspec(s) {} matched any of the {num_remote_refs} refs on the remote", refspecs.iter().map(|r| r.to_ref().instruction().to_bstring().to_string()).collect::<Vec<_>>().join(", "))]
NoMapping {
refspecs: Vec<gix_refspec::RefSpec>,
num_remote_refs: usize,
},
#[error("Failed to consume the pack sent by the remove")]
ConsumePack(Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Failed to read remaining bytes in stream")]
ReadRemainingBytes(#[source] std::io::Error),
}

impl crate::transport::IsSpuriousError for Error {
fn is_spurious(&self) -> bool {
match self {
Error::FetchResponse(err) => err.is_spurious(),
Error::Client(err) => err.is_spurious(),
_ => false,
}
}
}
276 changes: 276 additions & 0 deletions gix-protocol/src/fetch/function.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
use crate::fetch::{
negotiate, Context, Error, Negotiate, NegotiateOutcome, Options, Outcome, ProgressId, RefMap, Shallow, Tags,
};
use crate::{fetch::Arguments, transport::packetline::read::ProgressAction};
use gix_features::progress::DynNestedProgress;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};

/// Perform one fetch operation, relying on a `transport`, right after a [`ref_map`](RefMap::new()) was created so
/// it's clear what the remote has.
/// `negotiate` is used to run the negotiation of objects that should be contained in the pack, *if* one is to be received.
/// `progress` and `should_interrupt` is passed to all potentially long-running parts of the operation.
///
/// `consume_pack(pack_read, progress, interrupt) -> bool` is always called to consume all bytes that are sent by the server, returning `true` if we should assure the pack is read to the end,
/// or `false` to do nothing. Dropping the reader without reading to EOF (i.e. returning `false`) is an offense to the server, and
/// `transport` won't be in the correct state to perform additional operations, or indicate the end of operation.
/// Note that the passed reader blocking as the pack-writing is blocking as well.
///
/// The `Context` and `Options` further define parts of this `fetch` operation.
///
/// As opposed to a full `git fetch`, this operation does *not*…
///
/// * …update local refs
/// * …end the interaction after the fetch
///
/// Note that the interaction will never be ended, even on error or failure, leaving it up to the caller to do that, maybe
/// with the help of [`SendFlushOnDrop`](crate::SendFlushOnDrop) which can wrap `transport`.
/// Generally, the `transport` is left in a state that allows for more commands to be run.
///
/// Return `Ok(None)` if there was nothing to do because all remote refs are at the same state as they are locally, or `Ok(Some(outcome))`
/// to inform about all the changes that were made.
#[maybe_async::maybe_async]
pub async fn fetch<P, T, E>(
ref_map: &RefMap,
negotiate: &mut impl Negotiate,
consume_pack: impl FnOnce(&mut dyn std::io::BufRead, &mut dyn DynNestedProgress, &AtomicBool) -> Result<bool, E>,
mut progress: P,
should_interrupt: &AtomicBool,
Context {
handshake,
transport,
user_agent,
trace_packetlines,
}: Context<'_, T>,
Options {
shallow_file,
shallow,
tags,
expected_object_hash,
reject_shallow_remote,
}: Options<'_>,
) -> Result<Option<Outcome>, Error>
where
P: gix_features::progress::NestedProgress,
P::SubProgress: 'static,
T: gix_transport::client::Transport,
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
let _span = gix_trace::coarse!("gix_protocol::fetch()");

if ref_map.mappings.is_empty() && !ref_map.remote_refs.is_empty() {
let mut specs = ref_map.refspecs.clone();
specs.extend(ref_map.extra_refspecs.clone());
return Err(Error::NoMapping {
refspecs: specs,
num_remote_refs: ref_map.remote_refs.len(),
});
}

let v1_shallow_updates = handshake.v1_shallow_updates.take();
let protocol_version = handshake.server_protocol_version;

let fetch = crate::Command::Fetch;
let fetch_features = {
let mut f = fetch.default_features(protocol_version, &handshake.capabilities);
f.push(user_agent);
f
};

crate::fetch::Response::check_required_features(protocol_version, &fetch_features)?;
let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all");
let mut arguments = Arguments::new(protocol_version, fetch_features, trace_packetlines);
if matches!(tags, Tags::Included) {
if !arguments.can_use_include_tag() {
return Err(Error::MissingServerFeature {
feature: "include-tag",
description:
// NOTE: if this is an issue, we could probably do what's proposed here.
"To make this work we would have to implement another pass to fetch attached tags separately",
});
}
arguments.use_include_tag();
}
let (shallow_commits, mut shallow_lock) = add_shallow_args(&mut arguments, shallow, &shallow_file)?;

if ref_map.object_hash != expected_object_hash {
return Err(Error::IncompatibleObjectHash {
local: expected_object_hash,
remote: ref_map.object_hash,
});
}

let negotiate_span = gix_trace::detail!(
"negotiate",
protocol_version = handshake.server_protocol_version as usize
);
let action = negotiate.mark_complete_and_common_ref()?;
let mut previous_response = None::<crate::fetch::Response>;
match &action {
negotiate::Action::NoChange | negotiate::Action::SkipToRefUpdate => Ok(None),
negotiate::Action::MustNegotiate {
remote_ref_target_known,
} => {
negotiate.add_wants(&mut arguments, remote_ref_target_known);
let mut rounds = Vec::new();
let is_stateless = arguments.is_stateless(!transport.connection_persists_across_multiple_requests());
let mut state = negotiate::one_round::State::new(is_stateless);
let mut reader = 'negotiation: loop {
let _round = gix_trace::detail!("negotiate round", round = rounds.len() + 1);
progress.step();
progress.set_name(format!("negotiate (round {})", rounds.len() + 1));

let is_done = match negotiate.one_round(&mut state, &mut arguments, previous_response.as_ref()) {
Ok((round, is_done)) => {
rounds.push(round);
is_done
}
Err(err) => {
return Err(err.into());
}
};
let mut reader = arguments.send(transport, is_done).await?;
if sideband_all {
setup_remote_progress(&mut progress, &mut reader, should_interrupt);
}
let response =
crate::fetch::Response::from_line_reader(protocol_version, &mut reader, is_done, !is_done).await?;
let has_pack = response.has_pack();
previous_response = Some(response);
if has_pack {
progress.step();
progress.set_name("receiving pack".into());
if !sideband_all {
setup_remote_progress(&mut progress, &mut reader, should_interrupt);
}
break 'negotiation reader;
}
};
drop(negotiate_span);

let mut previous_response = previous_response.expect("knowledge of a pack means a response was received");
previous_response.append_v1_shallow_updates(v1_shallow_updates);
if !previous_response.shallow_updates().is_empty() && shallow_lock.is_none() {
if reject_shallow_remote {
return Err(Error::RejectShallowRemote);
}
shallow_lock = acquire_shallow_lock(&shallow_file).map(Some)?;
}

#[cfg(feature = "async-client")]
let mut rd = crate::futures_lite::io::BlockOn::new(reader);
#[cfg(not(feature = "async-client"))]
let mut rd = reader;
let may_read_to_end =
consume_pack(&mut rd, &mut progress, should_interrupt).map_err(|err| Error::ConsumePack(err.into()))?;
#[cfg(feature = "async-client")]
{
reader = rd.into_inner();
}
#[cfg(not(feature = "async-client"))]
{
reader = rd;
}

if may_read_to_end {
// Assure the final flush packet is consumed.
let has_read_to_end = reader.stopped_at().is_some();
#[cfg(feature = "async-client")]
{
if !has_read_to_end {
futures_lite::io::copy(&mut reader, &mut futures_lite::io::sink())
.await
.map_err(Error::ReadRemainingBytes)?;
}
}
#[cfg(not(feature = "async-client"))]
{
if !has_read_to_end {
std::io::copy(&mut reader, &mut std::io::sink()).map_err(Error::ReadRemainingBytes)?;
}
}
}
drop(reader);

if let Some(shallow_lock) = shallow_lock {
if !previous_response.shallow_updates().is_empty() {
gix_shallow::write(shallow_lock, shallow_commits, previous_response.shallow_updates())?;
}
}
Ok(Some(Outcome {
last_response: previous_response,
negotiate: NegotiateOutcome { action, rounds },
}))
}
}
}

fn acquire_shallow_lock(shallow_file: &Path) -> Result<gix_lock::File, Error> {
gix_lock::File::acquire_to_update_resource(shallow_file, gix_lock::acquire::Fail::Immediately, None)
.map_err(Into::into)
}

fn add_shallow_args(
args: &mut Arguments,
shallow: &Shallow,
shallow_file: &std::path::Path,
) -> Result<(Option<Vec<gix_hash::ObjectId>>, Option<gix_lock::File>), Error> {
let expect_change = *shallow != Shallow::NoChange;
let shallow_lock = expect_change.then(|| acquire_shallow_lock(shallow_file)).transpose()?;

let shallow_commits = gix_shallow::read(shallow_file)?;
if (shallow_commits.is_some() || expect_change) && !args.can_use_shallow() {
// NOTE: if this is an issue, we can always unshallow the repo ourselves.
return Err(Error::MissingServerFeature {
feature: "shallow",
description: "shallow clones need server support to remain shallow, otherwise bigger than expected packs are sent effectively unshallowing the repository",
});
}
if let Some(shallow_commits) = &shallow_commits {
for commit in shallow_commits.iter() {
args.shallow(commit);
}
}
match shallow {
Shallow::NoChange => {}
Shallow::DepthAtRemote(commits) => args.deepen(commits.get() as usize),
Shallow::Deepen(commits) => {
args.deepen(*commits as usize);
args.deepen_relative();
}
Shallow::Since { cutoff } => {
args.deepen_since(cutoff.seconds);
}
Shallow::Exclude {
remote_refs,
since_cutoff,
} => {
if let Some(cutoff) = since_cutoff {
args.deepen_since(cutoff.seconds);
}
for ref_ in remote_refs {
args.deepen_not(ref_.as_ref().as_bstr());
}
}
}
Ok((shallow_commits, shallow_lock))
}

fn setup_remote_progress<'a>(
progress: &mut dyn gix_features::progress::DynNestedProgress,
reader: &mut Box<dyn crate::transport::client::ExtendedBufRead<'a> + Unpin + 'a>,
should_interrupt: &'a AtomicBool,
) {
use crate::transport::client::ExtendedBufRead;
reader.set_progress_handler(Some(Box::new({
let mut remote_progress = progress.add_child_with_id("remote".to_string(), ProgressId::RemoteProgress.into());
move |is_err: bool, data: &[u8]| {
crate::RemoteProgress::translate_to_progress(is_err, data, &mut remote_progress);
if should_interrupt.load(Ordering::Relaxed) {
ProgressAction::Interrupt
} else {
ProgressAction::Continue
}
}
}) as crate::transport::client::HandleProgress<'a>));
}
50 changes: 41 additions & 9 deletions gix-protocol/src/fetch/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
/// A module providing low-level primitives to flexibly perform various `fetch` related activities. Note that the typesystem isn't used
/// to assure they are always performed in the right order, the caller has to follow some parts of the protocol itself.
///
/// ### Order for receiving a pack
///
/// * [handshake](handshake())
/// * **ls-refs**
/// * [get available refs by refspecs](RefMap::new())
/// * **fetch pack**
/// * `negotiate` until a pack can be received (TBD)
/// * [officially terminate the connection](crate::indicate_end_of_interaction())
/// - Consider wrapping the transport in [`SendFlushOnDrop`](crate::SendFlushOnDrop) to be sure the connection is terminated
/// gracefully even if there is an application error.
///
/// Note that this flow doesn't involve actually writing the pack, or indexing it. Nor does it contain machinery
/// to write or update references based on the fetched remote references.
///
/// Also, when the server supports [version 2](crate::transport::Protocol::V2) of the protocol, then each of the listed commands,
/// `ls-refs` and `fetch` can be invoked multiple times in any order.
// Note: for ease of use, this is tested in `gix` itself. The test-suite here uses a legacy implementation.
mod arguments;
pub use arguments::Arguments;

///
pub mod delegate;
#[cfg(any(feature = "async-client", feature = "blocking-client"))]
pub use delegate::Delegate;
pub use delegate::{Action, DelegateBlocking};

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
#[cfg(feature = "fetch")]
mod error;
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
#[cfg(feature = "fetch")]
pub use error::Error;
///
pub mod response;
pub use response::Response;

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
#[cfg(feature = "fetch")]
pub(crate) mod function;

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
#[cfg(feature = "handshake")]
mod handshake;
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
#[cfg(feature = "handshake")]
pub use handshake::upload_pack as handshake;

#[cfg(test)]
mod tests;
#[cfg(feature = "fetch")]
pub mod negotiate;

///
#[cfg(feature = "fetch")]
pub mod refmap;

mod types;
pub use types::*;

Large diffs are not rendered by default.

174 changes: 174 additions & 0 deletions gix-protocol/src/fetch/refmap/init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use std::collections::HashSet;

use crate::fetch;
use crate::fetch::refmap::{Mapping, Source, SpecIndex};
use crate::fetch::RefMap;
use crate::transport::client::Transport;
use bstr::{BString, ByteVec};
use gix_features::progress::Progress;

/// The error returned by [`RefMap::new()`].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("The object format {format:?} as used by the remote is unsupported")]
UnknownObjectFormat { format: BString },
#[error(transparent)]
MappingValidation(#[from] gix_refspec::match_group::validate::Error),
#[error(transparent)]
ListRefs(#[from] crate::ls_refs::Error),
}

/// For use in [`RefMap::new()`].
#[derive(Debug, Clone)]
pub struct Options {
/// Use a two-component prefix derived from the ref-spec's source, like `refs/heads/` to let the server pre-filter refs
/// with great potential for savings in traffic and local CPU time. Defaults to `true`.
pub prefix_from_spec_as_filter_on_remote: bool,
/// A list of refspecs to use as implicit refspecs which won't be saved or otherwise be part of the remote in question.
///
/// This is useful for handling `remote.<name>.tagOpt` for example.
pub extra_refspecs: Vec<gix_refspec::RefSpec>,
}

impl Default for Options {
fn default() -> Self {
Options {
prefix_from_spec_as_filter_on_remote: true,
extra_refspecs: Vec::new(),
}
}
}

impl RefMap {
/// Create a new instance by obtaining all references on the remote that have been filtered through our remote's
/// for _fetching_.
///
/// A [context](fetch::Context) is provided to bundle what would be additional parameters,
/// and [options](Options) are used to further configure the call.
///
/// * `progress` is used if `ls-refs` is invoked on the remote. Always the case when V2 is used.
/// * `fetch_refspecs` are all explicit refspecs to identify references on the remote that you are interested in.
/// Note that these are copied to [`RefMap::refspecs`] for convenience, as `RefMap::mappings` refer to them by index.
#[allow(clippy::result_large_err)]
#[maybe_async::maybe_async]
pub async fn new<T>(
mut progress: impl Progress,
fetch_refspecs: &[gix_refspec::RefSpec],
fetch::Context {
handshake,
transport,
user_agent,
trace_packetlines,
}: fetch::Context<'_, T>,
Options {
prefix_from_spec_as_filter_on_remote,
extra_refspecs,
}: Options,
) -> Result<Self, Error>
where
T: Transport,
{
let _span = gix_trace::coarse!("gix_protocol::fetch::RefMap::new()");
let null = gix_hash::ObjectId::null(gix_hash::Kind::Sha1); // OK to hardcode Sha1, it's not supposed to match, ever.

let all_refspecs = {
let mut s: Vec<_> = fetch_refspecs.to_vec();
s.extend(extra_refspecs.clone());
s
};
let remote_refs = match handshake.refs.take() {
Some(refs) => refs,
None => {
crate::ls_refs(
transport,
&handshake.capabilities,
|_capabilities, arguments, features| {
features.push(user_agent);
if prefix_from_spec_as_filter_on_remote {
let mut seen = HashSet::new();
for spec in &all_refspecs {
let spec = spec.to_ref();
if seen.insert(spec.instruction()) {
let mut prefixes = Vec::with_capacity(1);
spec.expand_prefixes(&mut prefixes);
for mut prefix in prefixes {
prefix.insert_str(0, "ref-prefix ");
arguments.push(prefix);
}
}
}
}
Ok(crate::ls_refs::Action::Continue)
},
&mut progress,
trace_packetlines,
)
.await?
}
};
let num_explicit_specs = fetch_refspecs.len();
let group = gix_refspec::MatchGroup::from_fetch_specs(all_refspecs.iter().map(gix_refspec::RefSpec::to_ref));
let (res, fixes) = group
.match_remotes(remote_refs.iter().map(|r| {
let (full_ref_name, target, object) = r.unpack();
gix_refspec::match_group::Item {
full_ref_name,
target: target.unwrap_or(&null),
object,
}
}))
.validated()?;

let mappings = res.mappings;
let mappings = mappings
.into_iter()
.map(|m| Mapping {
remote: m.item_index.map_or_else(
|| {
Source::ObjectId(match m.lhs {
gix_refspec::match_group::SourceRef::ObjectId(id) => id,
_ => unreachable!("no item index implies having an object id"),
})
},
|idx| Source::Ref(remote_refs[idx].clone()),
),
local: m.rhs.map(std::borrow::Cow::into_owned),
spec_index: if m.spec_index < num_explicit_specs {
SpecIndex::ExplicitInRemote(m.spec_index)
} else {
SpecIndex::Implicit(m.spec_index - num_explicit_specs)
},
})
.collect();

let object_hash = extract_object_format(handshake)?;
Ok(RefMap {
mappings,
refspecs: fetch_refspecs.to_vec(),
extra_refspecs,
fixes,
remote_refs,
object_hash,
})
}
}

/// Assume sha1 if server says nothing, otherwise configure anything beyond sha1 in the local repo configuration
#[allow(clippy::result_large_err)]
fn extract_object_format(outcome: &crate::handshake::Outcome) -> Result<gix_hash::Kind, Error> {
use bstr::ByteSlice;
let object_hash =
if let Some(object_format) = outcome.capabilities.capability("object-format").and_then(|c| c.value()) {
let object_format = object_format.to_str().map_err(|_| Error::UnknownObjectFormat {
format: object_format.into(),
})?;
match object_format {
"sha1" => gix_hash::Kind::Sha1,
unknown => return Err(Error::UnknownObjectFormat { format: unknown.into() }),
}
} else {
gix_hash::Kind::Sha1
};
Ok(object_hash)
}
105 changes: 105 additions & 0 deletions gix-protocol/src/fetch/refmap/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
///
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub mod init;

/// Either an object id that the remote has or the matched remote ref itself.
#[derive(Debug, Clone)]
pub enum Source {
/// An object id, as the matched ref-spec was an object id itself.
ObjectId(gix_hash::ObjectId),
/// The remote reference that matched the ref-specs name.
Ref(crate::handshake::Ref),
}

impl Source {
/// Return either the direct object id we refer to or the direct target that a reference refers to.
/// The latter may be a direct or a symbolic reference.
/// If unborn, `None` is returned.
pub fn as_id(&self) -> Option<&gix_hash::oid> {
match self {
Source::ObjectId(id) => Some(id),
Source::Ref(r) => r.unpack().1,
}
}

/// Return the target that this symbolic ref is pointing to, or `None` if it is no symbolic ref.
pub fn as_target(&self) -> Option<&bstr::BStr> {
match self {
Source::ObjectId(_) => None,
Source::Ref(r) => match r {
crate::handshake::Ref::Peeled { .. } | crate::handshake::Ref::Direct { .. } => None,
crate::handshake::Ref::Symbolic { target, .. } | crate::handshake::Ref::Unborn { target, .. } => {
Some(target.as_ref())
}
},
}
}

/// Returns the peeled id of this instance, that is the object that can't be de-referenced anymore.
pub fn peeled_id(&self) -> Option<&gix_hash::oid> {
match self {
Source::ObjectId(id) => Some(id),
Source::Ref(r) => {
let (_name, target, peeled) = r.unpack();
peeled.or(target)
}
}
}

/// Return ourselves as the full name of the reference we represent, or `None` if this source isn't a reference but an object.
pub fn as_name(&self) -> Option<&bstr::BStr> {
match self {
Source::ObjectId(_) => None,
Source::Ref(r) => match r {
crate::handshake::Ref::Unborn { full_ref_name, .. }
| crate::handshake::Ref::Symbolic { full_ref_name, .. }
| crate::handshake::Ref::Direct { full_ref_name, .. }
| crate::handshake::Ref::Peeled { full_ref_name, .. } => Some(full_ref_name.as_ref()),
},
}
}
}

/// An index into various lists of refspecs that have been used in a [Mapping] of remote references to local ones.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum SpecIndex {
/// An index into the _refspecs of the remote_ that triggered a fetch operation.
/// These refspecs are explicit and visible to the user.
ExplicitInRemote(usize),
/// An index into the list of [extra refspecs](crate::fetch::RefMap::extra_refspecs) that are implicit
/// to a particular fetch operation.
Implicit(usize),
}

impl SpecIndex {
/// Depending on our index variant, get the index either from `refspecs` or from `extra_refspecs` for `Implicit` variants.
pub fn get<'a>(
self,
refspecs: &'a [gix_refspec::RefSpec],
extra_refspecs: &'a [gix_refspec::RefSpec],
) -> Option<&'a gix_refspec::RefSpec> {
match self {
SpecIndex::ExplicitInRemote(idx) => refspecs.get(idx),
SpecIndex::Implicit(idx) => extra_refspecs.get(idx),
}
}

/// If this is an `Implicit` variant, return its index.
pub fn implicit_index(self) -> Option<usize> {
match self {
SpecIndex::Implicit(idx) => Some(idx),
SpecIndex::ExplicitInRemote(_) => None,
}
}
}

/// A mapping between a single remote reference and its advertised objects to a local destination which may or may not exist.
#[derive(Debug, Clone)]
pub struct Mapping {
/// The reference on the remote side, along with information about the objects they point to as advertised by the server.
pub remote: Source,
/// The local tracking reference to update after fetching the object visible via `remote`.
pub local: Option<bstr::BString>,
/// The index into the fetch ref-specs used to produce the mapping, allowing it to be recovered.
pub spec_index: SpecIndex,
}
3 changes: 2 additions & 1 deletion gix-protocol/src/fetch/response/async_io.rs
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ use gix_transport::{client, Protocol};

use crate::fetch::{
response,
response::shallow_update_from_line,
response::{Acknowledgement, ShallowUpdate, WantedRef},
Response,
};
@@ -132,7 +133,7 @@ impl Response {
}
}
"shallow-info" => {
if parse_v2_section(&mut line, reader, &mut shallows, ShallowUpdate::from_line).await? {
if parse_v2_section(&mut line, reader, &mut shallows, shallow_update_from_line).await? {
break 'section false;
}
}
3 changes: 2 additions & 1 deletion gix-protocol/src/fetch/response/blocking_io.rs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ use std::io;

use gix_transport::{client, Protocol};

use crate::fetch::response::shallow_update_from_line;
use crate::fetch::{
response,
response::{Acknowledgement, ShallowUpdate, WantedRef},
@@ -128,7 +129,7 @@ impl Response {
}
}
"shallow-info" => {
if parse_v2_section(&mut line, reader, &mut shallows, ShallowUpdate::from_line)? {
if parse_v2_section(&mut line, reader, &mut shallows, shallow_update_from_line)? {
break 'section false;
}
}
48 changes: 15 additions & 33 deletions gix-protocol/src/fetch/response/mod.rs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ use bstr::BString;
use gix_transport::{client, Protocol};

use crate::command::Feature;
use crate::fetch::Response;

/// The error returned in the [response module][crate::fetch::response].
#[derive(Debug, thiserror::Error)]
@@ -59,15 +60,7 @@ pub enum Acknowledgement {
Nak,
}

/// A shallow line received from the server.
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ShallowUpdate {
/// Shallow the given `id`.
Shallow(gix_hash::ObjectId),
/// Don't shallow the given `id` anymore.
Unshallow(gix_hash::ObjectId),
}
pub use gix_shallow::Update as ShallowUpdate;

/// A wanted-ref line received from the server.
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
@@ -79,21 +72,19 @@ pub struct WantedRef {
pub path: BString,
}

impl ShallowUpdate {
/// Parse a `ShallowUpdate` from a `line` as received to the server.
pub fn from_line(line: &str) -> Result<ShallowUpdate, Error> {
match line.trim_end().split_once(' ') {
Some((prefix, id)) => {
let id = gix_hash::ObjectId::from_hex(id.as_bytes())
.map_err(|_| Error::UnknownLineType { line: line.to_owned() })?;
Ok(match prefix {
"shallow" => ShallowUpdate::Shallow(id),
"unshallow" => ShallowUpdate::Unshallow(id),
_ => return Err(Error::UnknownLineType { line: line.to_owned() }),
})
}
None => Err(Error::UnknownLineType { line: line.to_owned() }),
/// Parse a `ShallowUpdate` from a `line` as received to the server.
pub fn shallow_update_from_line(line: &str) -> Result<ShallowUpdate, Error> {
match line.trim_end().split_once(' ') {
Some((prefix, id)) => {
let id = gix_hash::ObjectId::from_hex(id.as_bytes())
.map_err(|_| Error::UnknownLineType { line: line.to_owned() })?;
Ok(match prefix {
"shallow" => ShallowUpdate::Shallow(id),
"unshallow" => ShallowUpdate::Unshallow(id),
_ => return Err(Error::UnknownLineType { line: line.to_owned() }),
})
}
None => Err(Error::UnknownLineType { line: line.to_owned() }),
}
}

@@ -148,15 +139,6 @@ impl WantedRef {
}
}

/// A representation of a complete fetch response
#[derive(Debug)]
pub struct Response {
acks: Vec<Acknowledgement>,
shallows: Vec<ShallowUpdate>,
wanted_refs: Vec<WantedRef>,
has_pack: bool,
}

impl Response {
/// Return true if the response has a pack which can be read next.
pub fn has_pack(&self) -> bool {
@@ -236,7 +218,7 @@ impl Response {
}
None => acks.push(ack),
},
Err(_) => match ShallowUpdate::from_line(peeked_line) {
Err(_) => match shallow_update_from_line(peeked_line) {
Ok(shallow) => {
shallows.push(shallow);
}
416 changes: 0 additions & 416 deletions gix-protocol/src/fetch/tests.rs

This file was deleted.

243 changes: 243 additions & 0 deletions gix-protocol/src/fetch/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
use crate::fetch::response::{Acknowledgement, ShallowUpdate, WantedRef};
use std::path::PathBuf;

/// Options for use in [`fetch()`](`crate::fetch()`)
#[derive(Debug, Clone)]
pub struct Options<'a> {
/// The path to the file containing the shallow commit boundary.
///
/// When needed, it will be locked in preparation for being modified.
pub shallow_file: PathBuf,
/// How to deal with shallow repositories. It does affect how negotiations are performed.
pub shallow: &'a Shallow,
/// Describe how to handle tags when fetching.
pub tags: Tags,
/// The hash the remote repository is expected to use, as it's what the local repository is initialized as.
pub expected_object_hash: gix_hash::Kind,
/// If `true`, if we fetch from a remote that only offers shallow clones, the operation will fail with an error
/// instead of writing the shallow boundary to the shallow file.
pub reject_shallow_remote: bool,
}

/// For use in [`RefMap::new()`] and [`fetch`](crate::fetch()).
#[cfg(feature = "handshake")]
pub struct Context<'a, T> {
/// The outcome of the handshake performed with the remote.
///
/// Note that it's mutable as depending on the protocol, it may contain refs that have been sent unconditionally.
pub handshake: &'a mut crate::handshake::Outcome,
/// The transport to use when making an `ls-refs` or `fetch` call.
///
/// This is always done if the underlying protocol is V2, which is implied by the absence of refs in the `handshake` outcome.
pub transport: &'a mut T,
/// How to self-identify during the `ls-refs` call in [`RefMap::new()`] or the `fetch` call in [`fetch()`](crate::fetch()).
///
/// This could be read from the `gitoxide.userAgent` configuration variable.
pub user_agent: (&'static str, Option<std::borrow::Cow<'static, str>>),
/// If `true`, output all packetlines using the the `gix-trace` machinery.
pub trace_packetlines: bool,
}

#[cfg(feature = "fetch")]
mod with_fetch {
use crate::fetch;
use crate::fetch::{negotiate, refmap};

/// For use in [`fetch`](crate::fetch()).
pub struct NegotiateContext<'a, 'b, 'c, Objects, Alternates, AlternatesOut, AlternatesErr, Find>
where
Objects: gix_object::Find + gix_object::FindHeader + gix_object::Exists,
Alternates: FnOnce() -> Result<AlternatesOut, AlternatesErr>,
AlternatesErr: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
AlternatesOut: Iterator<Item = (gix_ref::file::Store, Find)>,
Find: gix_object::Find,
{
/// Access to the object database.
/// *Note* that the `exists()` calls must not trigger a refresh of the ODB packs as plenty of them might fail, i.e. find on object.
pub objects: &'a Objects,
/// Access to the git references database.
pub refs: &'a gix_ref::file::Store,
/// A function that returns an iterator over `(refs, objects)` for each alternate repository, to assure all known objects are added also according to their tips.
pub alternates: Alternates,
/// The implementation that performs the negotiation later, i.e. prepare wants and haves.
pub negotiator: &'a mut dyn gix_negotiate::Negotiator,
/// The commit-graph for use by the `negotiator` - we populate it with tips to initialize the graph traversal.
pub graph: &'a mut gix_negotiate::Graph<'b, 'c>,
}

/// A trait to encapsulate steps to negotiate the contents of the pack.
///
/// Typical implementations use the utilities found in the [`negotiate`] module.
pub trait Negotiate {
/// Typically invokes [`negotiate::mark_complete_and_common_ref()`].
fn mark_complete_and_common_ref(&mut self) -> Result<negotiate::Action, negotiate::Error>;
/// Typically invokes [`negotiate::add_wants()`].
fn add_wants(&mut self, arguments: &mut fetch::Arguments, remote_ref_target_known: &[bool]);
/// Typically invokes [`negotiate::one_round()`].
fn one_round(
&mut self,
state: &mut negotiate::one_round::State,
arguments: &mut fetch::Arguments,
previous_response: Option<&fetch::Response>,
) -> Result<(negotiate::Round, bool), negotiate::Error>;
}

/// The outcome of [`fetch()`](crate::fetch()).
#[derive(Debug, Clone)]
pub struct Outcome {
/// The most recent server response.
///
/// Useful to obtain information about new shallow boundaries.
pub last_response: fetch::Response,
/// Information about the negotiation to receive the new pack.
pub negotiate: NegotiateOutcome,
}

/// The negotiation-specific outcome of [`fetch()`](crate::fetch()).
#[derive(Debug, Clone)]
pub struct NegotiateOutcome {
/// The outcome of the negotiation stage of the fetch operation.
///
/// If it is…
///
/// * [`negotiate::Action::MustNegotiate`] there will always be a `pack`.
/// * [`negotiate::Action::SkipToRefUpdate`] there is no `pack` but references can be updated right away.
///
/// Note that this is never [negotiate::Action::NoChange`] as this would mean there is no negotiation information at all
/// so this structure wouldn't be present.
pub action: negotiate::Action,
/// Additional information for each round of negotiation.
pub rounds: Vec<negotiate::Round>,
}

/// Information about the relationship between our refspecs, and remote references with their local counterparts.
///
/// It's the first stage that offers connection to the server, and is typically required to perform one or more fetch operations.
#[derive(Default, Debug, Clone)]
pub struct RefMap {
/// A mapping between a remote reference and a local tracking branch.
pub mappings: Vec<refmap::Mapping>,
/// The explicit refspecs that were supposed to be used for fetching.
///
/// Typically, they are configured by the remote and are referred to by
/// [`refmap::SpecIndex::ExplicitInRemote`] in [`refmap::Mapping`].
pub refspecs: Vec<gix_refspec::RefSpec>,
/// Refspecs which have been added implicitly due to settings of the `remote`, usually pre-initialized from
/// [`extra_refspecs` in RefMap options](refmap::init::Options).
/// They are referred to by [`refmap::SpecIndex::Implicit`] in [`refmap::Mapping`].
///
/// They are never persisted nor are they typically presented to the user.
pub extra_refspecs: Vec<gix_refspec::RefSpec>,
/// Information about the fixes applied to the `mapping` due to validation and sanitization.
pub fixes: Vec<gix_refspec::match_group::validate::Fix>,
/// All refs advertised by the remote.
pub remote_refs: Vec<crate::handshake::Ref>,
/// The kind of hash used for all data sent by the server, if understood by this client implementation.
///
/// It was extracted from the `handshake` as advertised by the server.
pub object_hash: gix_hash::Kind,
}
}
#[cfg(feature = "fetch")]
pub use with_fetch::*;

/// Describe how shallow clones are handled when fetching, with variants defining how the *shallow boundary* is handled.
///
/// The *shallow boundary* is a set of commits whose parents are not present in the repository.
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub enum Shallow {
/// Fetch all changes from the remote without affecting the shallow boundary at all.
///
/// This also means that repositories that aren't shallow will remain like that.
#[default]
NoChange,
/// Receive update to `depth` commits in the history of the refs to fetch (from the viewpoint of the remote),
/// with the value of `1` meaning to receive only the commit a ref is pointing to.
///
/// This may update the shallow boundary to increase or decrease the amount of available history.
DepthAtRemote(std::num::NonZeroU32),
/// Increase the number of commits and thus expand the shallow boundary by `depth` commits as seen from our local
/// shallow boundary, with a value of `0` having no effect.
Deepen(u32),
/// Set the shallow boundary at the `cutoff` time, meaning that there will be no commits beyond that time.
Since {
/// The date beyond which there will be no history.
cutoff: gix_date::Time,
},
/// Receive all history excluding all commits reachable from `remote_refs`. These can be long or short
/// ref names or tag names.
Exclude {
/// The ref names to exclude, short or long. Note that ambiguous short names will cause the remote to abort
/// without an error message being transferred (because the protocol does not support it)
remote_refs: Vec<gix_ref::PartialName>,
/// If some, this field has the same meaning as [`Shallow::Since`] which can be used in combination
/// with excluded references.
since_cutoff: Option<gix_date::Time>,
},
}

impl Shallow {
/// Produce a variant that causes the repository to loose its shallow boundary, effectively by extending it
/// beyond all limits.
pub fn undo() -> Self {
Shallow::DepthAtRemote((i32::MAX as u32).try_into().expect("valid at compile time"))
}
}

/// Describe how to handle tags when fetching
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
pub enum Tags {
/// Fetch all tags from the remote, even if these are not reachable from objects referred to by our refspecs.
All,
/// Fetch only the tags that point to the objects being sent.
/// That way, annotated tags that point to an object we receive are automatically transmitted and their refs are created.
/// The same goes for lightweight tags.
#[default]
Included,
/// Do not fetch any tags.
None,
}

impl Tags {
/// Obtain a refspec that determines whether or not to fetch all tags, depending on this variant.
///
/// The returned refspec is the default refspec for tags, but won't overwrite local tags ever.
#[cfg(feature = "fetch")]
pub fn to_refspec(&self) -> Option<gix_refspec::RefSpecRef<'static>> {
match self {
Tags::All | Tags::Included => Some(
gix_refspec::parse("refs/tags/*:refs/tags/*".into(), gix_refspec::parse::Operation::Fetch)
.expect("valid"),
),
Tags::None => None,
}
}
}

/// A representation of a complete fetch response
#[derive(Debug, Clone)]
pub struct Response {
pub(crate) acks: Vec<Acknowledgement>,
pub(crate) shallows: Vec<ShallowUpdate>,
pub(crate) wanted_refs: Vec<WantedRef>,
pub(crate) has_pack: bool,
}

/// The progress ids used in during various steps of the fetch operation.
///
/// Note that tagged progress isn't very widely available yet, but support can be improved as needed.
///
/// Use this information to selectively extract the progress of interest in case the parent application has custom visualization.
#[derive(Debug, Copy, Clone)]
pub enum ProgressId {
/// The progress name is defined by the remote and the progress messages it sets, along with their progress values and limits.
RemoteProgress,
}

impl From<ProgressId> for gix_features::progress::Id {
fn from(v: ProgressId) -> Self {
match v {
ProgressId::RemoteProgress => *b"FERP",
}
}
}
183 changes: 0 additions & 183 deletions gix-protocol/src/fetch_fn.rs

This file was deleted.

17 changes: 10 additions & 7 deletions gix-protocol/src/handshake/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use bstr::BString;
use gix_transport::client::Capabilities;

///
pub mod refs;

/// A git reference, commonly referred to as 'ref', as returned by a git server before sending a pack.
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
@@ -51,18 +53,20 @@ pub enum Ref {
/// The result of the [`handshake()`][super::handshake()] function.
#[derive(Default, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(feature = "handshake")]
pub struct Outcome {
/// The protocol version the server responded with. It might have downgraded the desired version.
pub server_protocol_version: gix_transport::Protocol,
/// The references reported as part of the `Protocol::V1` handshake, or `None` otherwise as V2 requires a separate request.
pub refs: Option<Vec<Ref>>,
/// Shallow updates as part of the `Protocol::V1`, to shallow a particular object.
/// Note that unshallowing isn't supported here.
pub v1_shallow_updates: Option<Vec<ShallowUpdate>>,
pub v1_shallow_updates: Option<Vec<crate::fetch::response::ShallowUpdate>>,
/// The server capabilities.
pub capabilities: Capabilities,
pub capabilities: gix_transport::client::Capabilities,
}

#[cfg(feature = "handshake")]
mod error {
use bstr::BString;
use gix_transport::client;
@@ -96,10 +100,9 @@ mod error {
}
}
}
use crate::fetch::response::ShallowUpdate;
#[cfg(feature = "handshake")]
pub use error::Error;

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
#[cfg(feature = "handshake")]
pub(crate) mod function;

///
pub mod refs;
3 changes: 0 additions & 3 deletions gix-protocol/src/handshake/refs/mod.rs
Original file line number Diff line number Diff line change
@@ -74,6 +74,3 @@ pub use async_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities,
mod blocking_io;
#[cfg(feature = "blocking-client")]
pub use blocking_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs};

#[cfg(test)]
mod tests;
35 changes: 35 additions & 0 deletions gix-protocol/src/handshake/refs/shared.rs
Original file line number Diff line number Diff line change
@@ -285,3 +285,38 @@ pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result<Ref, Error> {
_ => Err(Error::MalformedV2RefLine(trimmed.to_owned().into())),
}
}

#[cfg(test)]
mod tests {
use gix_transport::client;

use crate::handshake::{refs, refs::shared::InternalRef};

#[test]
fn extract_symbolic_references_from_capabilities() -> Result<(), client::Error> {
let caps = client::Capabilities::from_bytes(
b"\0unrelated symref=HEAD:refs/heads/main symref=ANOTHER:refs/heads/foo symref=MISSING_NAMESPACE_TARGET:(null) agent=git/2.28.0",
)?
.0;
let out = refs::shared::from_capabilities(caps.iter()).expect("a working example");

assert_eq!(
out,
vec![
InternalRef::SymbolicForLookup {
path: "HEAD".into(),
target: Some("refs/heads/main".into())
},
InternalRef::SymbolicForLookup {
path: "ANOTHER".into(),
target: Some("refs/heads/foo".into())
},
InternalRef::SymbolicForLookup {
path: "MISSING_NAMESPACE_TARGET".into(),
target: None
}
]
);
Ok(())
}
}
37 changes: 22 additions & 15 deletions gix-protocol/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
//! An abstraction over [fetching][fetch()] a pack from the server.
//!
//! This implementation hides the transport layer, statefulness and the protocol version to the [fetch delegate][fetch::Delegate],
//! the actual client implementation.
//! Generally, there is the following order of operations.
//!
//! * create a [`Transport`](gix_transport::client::Transport)
//! * perform a [`handshake()`]
//! * execute a [`Command`]
//! - [list references](ls_refs())
//! - create a mapping between [refspecs and references](fetch::RefMap)
//! - [receive a pack](fetch())
//!
//! ## Feature Flags
#![cfg_attr(
all(doc, feature = "document-features"),
@@ -10,6 +17,12 @@
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))]
#![deny(missing_docs, rust_2018_idioms, unsafe_code)]

/// A function that performs a given credential action, trying to obtain credentials for an operation that needs it.
///
/// Useful for both `fetch` and `push`.
#[cfg(feature = "handshake")]
pub type AuthenticateFn<'a> = Box<dyn FnMut(gix_credentials::helper::Action) -> gix_credentials::protocol::Result + 'a>;

/// A selector for V2 commands to invoke on the server for purpose of pre-invocation validation.
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
pub enum Command {
@@ -20,25 +33,22 @@ pub enum Command {
}
pub mod command;

#[cfg(feature = "async-trait")]
#[cfg(feature = "async-client")]
pub use async_trait;
#[cfg(feature = "futures-io")]
#[cfg(feature = "async-client")]
pub use futures_io;
#[cfg(feature = "futures-lite")]
#[cfg(feature = "async-client")]
pub use futures_lite;
#[cfg(feature = "handshake")]
pub use gix_credentials as credentials;
/// A convenience export allowing users of gix-protocol to use the transport layer without their own cargo dependency.
pub use gix_transport as transport;
pub use maybe_async;

///
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub mod fetch;

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
mod fetch_fn;
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub use fetch_fn::{fetch, FetchConnection};
pub use fetch::function::fetch;

mod remote_progress;
pub use remote_progress::RemoteProgress;
@@ -47,18 +57,15 @@ pub use remote_progress::RemoteProgress;
compile_error!("Cannot set both 'blocking-client' and 'async-client' features as they are mutually exclusive");

///
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub mod handshake;
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
#[cfg(feature = "handshake")]
pub use handshake::function::handshake;

///
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub mod ls_refs;
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub use ls_refs::function::ls_refs;

mod util;
pub use util::agent;
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub use util::indicate_end_of_interaction;
pub use util::*;
11 changes: 8 additions & 3 deletions gix-protocol/src/ls_refs.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
mod error {
use crate::handshake::refs::parse;

@@ -11,6 +12,8 @@ mod error {
Transport(#[from] gix_transport::client::Error),
#[error(transparent)]
Parse(#[from] parse::Error),
#[error(transparent)]
ArgumentValidation(#[from] crate::command::validate_argument_prefixes::Error),
}

impl gix_transport::IsSpuriousError for Error {
@@ -23,6 +26,7 @@ mod error {
}
}
}
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub use error::Error;

/// What to do after preparing ls-refs in [`ls_refs()`][crate::ls_refs()].
@@ -37,6 +41,7 @@ pub enum Action {
Skip,
}

#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub(crate) mod function {
use std::borrow::Cow;

@@ -70,7 +75,7 @@ pub(crate) mod function {
let _span = gix_features::trace::detail!("gix_protocol::ls_refs()", capabilities = ?capabilities);
let ls_refs = Command::LsRefs;
let mut ls_features = ls_refs.default_features(gix_transport::Protocol::V2, capabilities);
let mut ls_args = ls_refs.initial_arguments(&ls_features);
let mut ls_args = ls_refs.initial_v2_arguments(&ls_features);
if capabilities
.capability("ls-refs")
.and_then(|cap| cap.supports("unborn"))
@@ -81,12 +86,12 @@ pub(crate) mod function {
let refs = match prepare_ls_refs(capabilities, &mut ls_args, &mut ls_features) {
Ok(Action::Skip) => Vec::new(),
Ok(Action::Continue) => {
ls_refs.validate_argument_prefixes_or_panic(
ls_refs.validate_argument_prefixes(
gix_transport::Protocol::V2,
capabilities,
&ls_args,
&ls_features,
);
)?;

progress.step();
progress.set_name("list refs".into());
105 changes: 86 additions & 19 deletions gix-protocol/src/util.rs
Original file line number Diff line number Diff line change
@@ -6,25 +6,92 @@ pub fn agent(name: impl Into<String>) -> String {
}
name
}

/// Send a message to indicate the remote side that there is nothing more to expect from us, indicating a graceful shutdown.
/// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate.
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
#[maybe_async::maybe_async]
pub async fn indicate_end_of_interaction(
mut transport: impl gix_transport::client::Transport,
trace: bool,
) -> Result<(), gix_transport::client::Error> {
// An empty request marks the (early) end of the interaction. Only relevant in stateful transports though.
if transport.connection_persists_across_multiple_requests() {
transport
.request(
gix_transport::client::WriteMode::Binary,
gix_transport::client::MessageKind::Flush,
trace,
)?
.into_read()
.await?;
mod with_transport {
use gix_transport::client::Transport;

/// Send a message to indicate the remote side that there is nothing more to expect from us, indicating a graceful shutdown.
/// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate.
#[maybe_async::maybe_async]
pub async fn indicate_end_of_interaction(
mut transport: impl gix_transport::client::Transport,
trace: bool,
) -> Result<(), gix_transport::client::Error> {
// An empty request marks the (early) end of the interaction. Only relevant in stateful transports though.
if transport.connection_persists_across_multiple_requests() {
transport
.request(
gix_transport::client::WriteMode::Binary,
gix_transport::client::MessageKind::Flush,
trace,
)?
.into_read()
.await?;
}
Ok(())
}

/// A utility to automatically send a flush packet when the instance is dropped, assuring a graceful termination of any
/// interaction with the server.
pub struct SendFlushOnDrop<T>
where
T: Transport,
{
/// The actual transport instance.
pub inner: T,
/// If `true`, the packetline used to indicate the end of interaction will be traced using `gix-trace`.
trace_packetlines: bool,
/// If `true`, we should not send another flush packet.
flush_packet_sent: bool,
}

impl<T> SendFlushOnDrop<T>
where
T: Transport,
{
/// Create a new instance with `transport`, while optionally tracing packetlines with `trace_packetlines`.
pub fn new(transport: T, trace_packetlines: bool) -> Self {
Self {
inner: transport,
trace_packetlines,
flush_packet_sent: false,
}
}

/// Useful to explicitly invalidate the connection by sending a flush-packet.
/// This will happen exactly once, and it is not considered an error to call it multiple times.
///
/// For convenience, this is not consuming, but could be to assure the underlying transport isn't used anymore.
#[maybe_async::maybe_async]
pub async fn indicate_end_of_interaction(&mut self) -> Result<(), gix_transport::client::Error> {
if self.flush_packet_sent {
return Ok(());
}

self.flush_packet_sent = true;
indicate_end_of_interaction(&mut self.inner, self.trace_packetlines).await
}
}

impl<T> Drop for SendFlushOnDrop<T>
where
T: Transport,
{
fn drop(&mut self) {
#[cfg(feature = "async-client")]
{
// TODO: this should be an async drop once the feature is available.
// Right now we block the executor by forcing this communication, but that only
// happens if the user didn't actually try to receive a pack, which consumes the
// connection in an async context.
crate::futures_lite::future::block_on(self.indicate_end_of_interaction()).ok();
}
#[cfg(not(feature = "async-client"))]
{
self.indicate_end_of_interaction().ok();
}
}
}
Ok(())
}
#[cfg(any(feature = "blocking-client", feature = "async-client"))]
pub use with_transport::*;
11 changes: 2 additions & 9 deletions gix-protocol/tests/async-protocol.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,2 @@
type Result = std::result::Result<(), Box<dyn std::error::Error>>;

pub fn fixture_bytes(path: &str) -> Vec<u8> {
std::fs::read(std::path::PathBuf::from("tests").join("fixtures").join(path))
.expect("fixture to be present and readable")
}

mod fetch;
mod remote_progress;
mod protocol;
pub use protocol::*;
11 changes: 2 additions & 9 deletions gix-protocol/tests/blocking-protocol.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,2 @@
type Result = std::result::Result<(), Box<dyn std::error::Error>>;

pub fn fixture_bytes(path: &str) -> Vec<u8> {
std::fs::read(std::path::PathBuf::from("tests").join("fixtures").join(path))
.expect("fixture to be present and readable")
}

mod fetch;
mod remote_progress;
mod protocol;
pub use protocol::*;
Original file line number Diff line number Diff line change
@@ -8,10 +8,8 @@ mod v1 {
const GITHUB_CAPABILITIES: &str = "multi_ack thin-pack side-band ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag allow-tip-sha1-in-want allow-reachable-sha1-in-want no-done symref=HEAD:refs/heads/main filter agent=git/github-gdf51a71f0236";
mod fetch {
mod default_features {
use crate::{
command::tests::v1::{capabilities, GITHUB_CAPABILITIES},
Command,
};
use super::super::{capabilities, GITHUB_CAPABILITIES};
use gix_protocol::Command;

#[test]
fn it_chooses_the_best_multi_ack_and_sideband() {
@@ -60,7 +58,8 @@ mod v2 {

mod fetch {
mod default_features {
use crate::{command::tests::v2::capabilities, Command};
use super::super::capabilities;
use gix_protocol::Command;

#[test]
fn all_features() {
@@ -80,12 +79,13 @@ mod v2 {
mod initial_arguments {
use bstr::ByteSlice;

use crate::{command::tests::v2::capabilities, Command};
use super::super::capabilities;
use gix_protocol::Command;

#[test]
fn for_all_features() {
assert_eq!(
Command::Fetch.initial_arguments(&Command::Fetch.default_features(
Command::Fetch.initial_v2_arguments(&Command::Fetch.default_features(
gix_transport::Protocol::V2,
&capabilities("fetch", "shallow filter sideband-all packfile-uris")
)),
@@ -101,7 +101,8 @@ mod v2 {

mod ls_refs {
mod default_features {
use crate::{command::tests::v2::capabilities, Command};
use super::super::capabilities;
use gix_protocol::Command;

#[test]
fn default_as_there_are_no_features() {
@@ -118,37 +119,50 @@ mod v2 {
mod validate {
use bstr::ByteSlice;

use crate::{command::tests::v2::capabilities, Command};
use super::super::capabilities;
use gix_protocol::Command;

#[test]
fn ref_prefixes_can_always_be_used() {
Command::LsRefs.validate_argument_prefixes_or_panic(
gix_transport::Protocol::V2,
&capabilities("something else", "do-not-matter"),
&[b"ref-prefix hello/".as_bstr().into()],
&[],
);
assert!(Command::LsRefs
.validate_argument_prefixes(
gix_transport::Protocol::V2,
&capabilities("something else", "do-not-matter"),
&[b"ref-prefix hello/".as_bstr().into()],
&[],
)
.is_ok());
}

#[test]
#[should_panic]
fn unknown_argument() {
Command::LsRefs.validate_argument_prefixes_or_panic(
gix_transport::Protocol::V2,
&capabilities("other", "do-not-matter"),
&[b"definitely-nothing-we-know".as_bstr().into()],
&[],
assert_eq!(
Command::LsRefs
.validate_argument_prefixes(
gix_transport::Protocol::V2,
&capabilities("other", "do-not-matter"),
&[b"definitely-nothing-we-know".as_bstr().into()],
&[],
)
.unwrap_err()
.to_string(),
"ls-refs: argument definitely-nothing-we-know is not known or allowed"
);
}

#[test]
#[should_panic]
fn unknown_feature() {
Command::LsRefs.validate_argument_prefixes_or_panic(
gix_transport::Protocol::V2,
&capabilities("other", "do-not-matter"),
&[],
&[("some-feature-that-does-not-exist", None)],
assert_eq!(
Command::LsRefs
.validate_argument_prefixes(
gix_transport::Protocol::V2,
&capabilities("other", "do-not-matter"),
&[],
&[("some-feature-that-does-not-exist", None)],
)
.unwrap_err()
.to_string(),
"ls-refs: capability some-feature-that-does-not-exist is not supported"
);
}
}
Loading