use std::sync::atomic::AtomicBool;
use super::{PreparedPush, PushAdvertisement, PushError, PushFailure, PushReport, protocol};
use crate::packet::Wire;
use crate::transport::TransportControl;
use crate::transport::http::{HttpRemote, RequestBody};
#[derive(Debug)]
pub enum HttpPushOutcome {
Declined,
Sent(PushReport),
}
pub async fn send_http(
remote: &HttpRemote,
prepared: PreparedPush,
control: TransportControl<'_>,
) -> Result<PushReport, PushError> {
match send_http_checked(remote, prepared, control, |_| true).await? {
HttpPushOutcome::Sent(report) => Ok(report),
HttpPushOutcome::Declined => unreachable!("unconditional HTTP push cannot decline"),
}
}
pub async fn send_http_checked(
remote: &HttpRemote,
prepared: PreparedPush,
control: TransportControl<'_>,
should_send: impl FnOnce(&PushAdvertisement) -> bool,
) -> Result<HttpPushOutcome, PushError> {
send_http_checked_with_progress(remote, prepared, control, should_send, |_| {}).await
}
pub async fn send_http_checked_with_progress(
remote: &HttpRemote,
mut prepared: PreparedPush,
control: TransportControl<'_>,
should_send: impl FnOnce(&PushAdvertisement) -> bool,
mut progress: impl FnMut(&[u8]) + Send,
) -> Result<HttpPushOutcome, PushError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"push.http",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
accepted = tracing::field::Empty,
rejected = tracing::field::Empty,
pending = tracing::field::Empty,
unpack = tracing::field::Empty,
);
let operation = async {
let preflight = async {
control.check()?;
let (bytes, _) = remote
.discover(
"git-receive-pack",
prepared.limits.max_advertisement_bytes,
control,
)
.await?;
let mut reader = bytes.as_slice();
let mut wire = Wire {
reader: &mut reader,
remaining: prepared.limits.max_advertisement_bytes,
cancel: control.cancel,
};
let (caps, advertisement) = protocol::advertise_with_refs(&mut wire, &prepared)?;
wire.end()?;
control.check()?;
Ok::<_, PushFailure>((caps, advertisement))
};
let (caps, advertisement) = preflight.await.map_err(PushError::NotSent)?;
if !should_send(&advertisement) {
return Ok(HttpPushOutcome::Declined);
}
if caps.report_v2 || caps.sideband {
prepared.request = prepared
.request_for(caps.report_v2, caps.sideband)
.map_err(PushError::NotSent)?
.into_owned();
}
let mut report = PushReport::pending(&prepared.commands);
if prepared.commands.is_empty() {
return Ok(HttpPushOutcome::Sent(report));
}
report.mark_attempted(&prepared.request, prepared.request.len());
let body = RequestBody::new(prepared.request, prepared.pack);
control.check().map_err(|e| PushError::NotSent(e.into()))?;
let mut sideband = LiveProgress::default();
let response = remote
.exchange_observed(
"git-receive-pack",
Some(body),
prepared.limits.max_status_bytes,
control,
|bytes| {
if caps.sideband {
sideband.observe(bytes, &mut progress);
}
},
)
.await;
let retain = AtomicBool::new(false);
let mut reader = response.body.as_slice();
let mut wire = Wire {
reader: &mut reader,
remaining: prepared.limits.max_status_bytes,
cancel: &retain,
};
let parsed = protocol::read_response(
&mut wire,
&mut report,
caps,
prepared.limits.max_status_bytes,
)
.and_then(|()| wire.end().map_err(PushFailure::from));
let result = response.result.map_err(PushFailure::from).and(parsed);
match result {
Ok(()) => Ok(HttpPushOutcome::Sent(report)),
Err(cause) => Err(PushError::Uncertain {
cause,
report: Box::new(report),
}),
}
};
#[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, |error| crate::trace::push(error, &span));
#[cfg(feature = "tracing")]
if let Ok(HttpPushOutcome::Sent(report)) = &result {
crate::trace::push_report(&span, report);
}
result
}
#[derive(Default)]
struct LiveProgress {
offset: usize,
stopped: bool,
}
impl LiveProgress {
fn observe(&mut self, bytes: &[u8], progress: &mut impl FnMut(&[u8])) {
while !self.stopped {
let remaining = &bytes[self.offset..];
let Some(header) = remaining.first_chunk::<4>() else {
return;
};
let Ok(length) = crate::packet::packet_length(header) else {
self.stopped = true;
return;
};
if length == 0 {
self.stopped = true;
return;
}
let Some(packet) = remaining.get(4..length) else {
return;
};
self.offset += length;
match packet.split_first() {
Some((1, _)) => {}
Some((2, payload)) => progress(payload),
_ => self.stopped = true,
}
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::LiveProgress;
#[rstest]
#[case::before_header(0)]
#[case::header_byte(1)]
#[case::half_header(2)]
#[case::almost_header(3)]
#[case::header(4)]
#[case::channel(5)]
#[case::payload(6)]
#[case::first_packet(8)]
#[case::second_header(10)]
#[case::all(16)]
fn progress_preserves_bytes_across_response_chunks(#[case] split: usize) {
let bytes = b"0008\x02a\xff\n0008\x02b\x00\r0000";
let mut observer = LiveProgress::default();
let mut received = Vec::new();
let mut callback = |payload: &[u8]| received.push(payload.to_vec());
observer.observe(&bytes[..split], &mut callback);
observer.observe(bytes, &mut callback);
observer.observe(bytes, &mut callback);
assert_eq!(received, [b"a\xff\n".to_vec(), b"b\x00\r".to_vec()]);
}
#[rstest]
#[case::flush(b"0000")]
#[case::remote_error(b"0006\x03x")]
#[case::unknown_channel(b"0006\x04x")]
#[case::missing_channel(b"0004")]
#[case::invalid_header(b"zzzz")]
#[case::reserved_length(b"0001")]
#[case::oversized(b"ffff")]
#[case::git_error(b"0009ERR x")]
fn progress_stops_at_terminal_or_invalid_packets(#[case] terminal: &[u8]) {
let bytes = [b"0006\x02a".as_slice(), terminal, b"0006\x02b"].concat();
let mut received = Vec::new();
LiveProgress::default().observe(&bytes, &mut |payload| received.push(payload.to_vec()));
assert_eq!(received, [b"a".to_vec()]);
}
#[test]
fn progress_skips_status_and_incomplete_payloads() {
let bytes = b"0006\x01x0006\x02a0009\x02b";
let mut received = Vec::new();
LiveProgress::default().observe(bytes, &mut |payload| received.push(payload.to_vec()));
assert_eq!(received, [b"a".to_vec()]);
}
}