use std::num::NonZeroU32;
use std::sync::Arc;
use super::live_progress::LiveProgress;
use super::v2::Capabilities;
use super::{
Advertisement, DownloadedFetch, FetchError, FetchLimits, FetchOptions, KnownHistory, protocol,
};
use crate::ObjectId;
use crate::packet::Wire;
use crate::transport::TransportControl;
use crate::transport::ssh::SshRemote;
pub(super) async fn open(
session: &mut crate::transport::ssh::Session,
requested: crate::transport::RequestedProtocol,
limits: FetchLimits,
control: TransportControl<'_>,
diagnostics: &mut impl FnMut(&[u8]),
) -> Result<
(
Advertisement,
protocol::Dialect,
super::ProtocolVersion,
usize,
),
FetchError,
> {
let budget = limits.max_advertisement_bytes.min(limits.max_wire_bytes);
let bytes = session
.advertise_with_diagnostics(budget, control, diagnostics)
.await?;
let mut reader = bytes.as_slice();
let mut wire = Wire {
reader: &mut reader,
remaining: budget,
cancel: control.cancel,
};
if !bytes.starts_with(b"000eversion 2\n") {
let (advertisement, version) = protocol::advertise_with_version(&mut wire, limits)?;
wire.end()?;
return Ok((advertisement, protocol::Dialect::V0, version, bytes.len()));
}
if requested != crate::transport::RequestedProtocol::V2 {
return Err(FetchError::Unsupported("protocol version"));
}
wire.packet()?;
let capabilities = Capabilities::read(&mut wire, limits)?;
wire.end()?;
let mut request = Vec::new();
capabilities.write_ls_refs(&mut request, &[], limits, control.cancel)?;
let left = budget.saturating_sub(bytes.len());
let response = session
.command_with_diagnostics(&request, left, control, diagnostics)
.await?;
let mut reader = response.as_slice();
let mut wire = Wire {
reader: &mut reader,
remaining: left,
cancel: control.cancel,
};
let advertisement = capabilities.read_ls_refs(&mut wire, limits)?;
wire.end()?;
advertisement.object_format()?;
Ok((
advertisement,
protocol::Dialect::V2(capabilities),
super::ProtocolVersion::V2,
bytes.len() + response.len(),
))
}
pub async fn receive_ssh(
remote: &SshRemote,
select: impl FnOnce(&Advertisement) -> Vec<ObjectId>,
known: Option<Arc<KnownHistory>>,
limits: FetchLimits,
control: TransportControl<'_>,
) -> Result<DownloadedFetch, FetchError> {
receive_ssh_with_depth(remote, select, known, None, limits, control).await
}
pub async fn receive_ssh_with_depth(
remote: &SshRemote,
select: impl FnOnce(&Advertisement) -> Vec<ObjectId>,
known: Option<Arc<KnownHistory>>,
depth: Option<NonZeroU32>,
limits: FetchLimits,
control: TransportControl<'_>,
) -> Result<DownloadedFetch, FetchError> {
receive_ssh_with_progress(
remote,
select,
known,
FetchOptions { limits, depth },
control,
|_| {},
|_| {},
)
.await
}
pub async fn receive_ssh_with_progress(
remote: &SshRemote,
select: impl FnOnce(&Advertisement) -> Vec<ObjectId>,
mut known: Option<Arc<KnownHistory>>,
options: FetchOptions,
control: TransportControl<'_>,
mut diagnostics: impl FnMut(&[u8]),
mut progress: impl FnMut(&[u8]),
) -> Result<DownloadedFetch, FetchError> {
let FetchOptions { limits, depth } = options;
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"fetch.ssh",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
wire_bytes = tracing::field::Empty,
);
let operation = async {
control.check()?;
let empty = KnownHistory::default();
let history = known.as_deref().unwrap_or(&empty);
protocol::validate_known(history, limits, control.cancel)?;
let mut session = remote.connect("git-upload-pack", control)?;
let (advertisement, dialect, _, used) = open(
&mut session,
remote.protocol(),
limits,
control,
&mut diagnostics,
)
.await?;
let wants = select(&advertisement);
if depth.is_none()
&& known
.as_ref()
.is_some_and(|history| !history.applies_to(&wants))
{
known = known
.as_ref()
.map(|history| Arc::new(history.shallow_only()));
}
let history = known.as_deref().unwrap_or(&empty);
let remaining = limits.max_wire_bytes - used;
control.check()?;
let mut request = Vec::new();
let negotiation = protocol::request(
&mut request,
&advertisement,
&dialect,
wants,
history,
depth,
limits,
control.cancel,
)?;
control.check()?;
let mut live = if negotiation.is_v2() {
LiveProgress::new_v2(limits.max_shallow_roots)
} else {
LiveProgress::new(depth.is_some(), limits.max_shallow_roots)
};
let mut validate_prefix = |bytes: &[u8]| {
let mut reader = bytes;
let mut wire = Wire {
reader: &mut reader,
remaining,
cancel: control.cancel,
};
protocol::read_response_prefix(&mut wire, &negotiation, limits).is_ok()
&& wire.end().is_ok()
};
let mut body = super::bytes::Bytes::temporary()?;
let mut io_error = None;
let (result, _) = session
.exchange_streaming(
&request,
&[],
remaining,
control,
&mut diagnostics,
&mut |bytes| {
if let Err(error) = body.append(bytes) {
io_error = Some(error);
return Err(crate::transport::ssh::SshError::Io(
std::io::ErrorKind::Other,
));
}
if negotiation.needs_pack {
live.feed(bytes, &mut validate_prefix, &mut progress);
}
Ok(())
},
)
.await;
if let Some(error) = io_error {
return Err(error);
}
result?;
if !negotiation.needs_pack && !body.is_empty() {
return Err(FetchError::Protocol("trailing response bytes"));
}
Ok(DownloadedFetch {
#[cfg(feature = "tracing")]
trace: crate::trace::DownloadContext::capture(),
advertisement,
negotiation,
known,
limits,
remaining,
body,
})
};
#[cfg(feature = "tracing")]
let result = tracing::Instrument::instrument(operation, span.clone()).await;
#[cfg(not(feature = "tracing"))]
let result = operation.await;
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, crate::trace::fetch);
#[cfg(feature = "tracing")]
if let Ok(value) = &result {
span.record("wire_bytes", value.body.len());
}
result
}