use std::borrow::Cow;
use std::collections::HashSet;
use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use super::{PreparationProgress, PushCommand, PushFailure as Error, PushLimits, graph};
use crate::packet::{check_cancelled, packet, put};
use crate::{ObjectId, Objects};
#[derive(Debug)]
pub struct PreparedPush {
pub(super) format: crate::ObjectFormat,
pub(super) commands: Vec<PushCommand>,
pub(super) request: Vec<u8>,
pub(super) pack: crate::fetch::Bytes,
pub(super) limits: PushLimits,
objects: u32,
pub(super) receiver_roots: Vec<ObjectId>,
pub(super) has_options: bool,
pub(super) options: Vec<Vec<u8>>,
pub(super) progress: bool,
}
impl PreparedPush {
pub fn new(
objects: &Objects,
commands: Vec<PushCommand>,
receiver_roots: &[ObjectId],
limits: PushLimits,
cancel: &AtomicBool,
) -> Result<Self, Error> {
Self::prepare(objects, commands, receiver_roots, limits, cancel, |_| {})
}
pub fn new_with_progress(
objects: &Objects,
commands: Vec<PushCommand>,
receiver_roots: &[ObjectId],
limits: PushLimits,
cancel: &AtomicBool,
observe: impl FnMut(PreparationProgress),
) -> Result<Self, Error> {
Self::prepare(objects, commands, receiver_roots, limits, cancel, observe)
}
fn prepare(
objects: &Objects,
commands: Vec<PushCommand>,
receiver_roots: &[ObjectId],
limits: PushLimits,
cancel: &AtomicBool,
mut observe: impl FnMut(PreparationProgress),
) -> Result<Self, Error> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"push.prepare",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
commands = commands.len(),
objects = tracing::field::Empty,
pack_bytes = tracing::field::Empty,
);
let operation = || {
check_cancelled(cancel)?;
if !objects.shallow_roots().is_empty() {
return Err(Error::Unsupported("shallow push preparation"));
}
for id in receiver_roots {
id.require_format(objects.object_format())?;
}
let request = encode_commands(
objects.object_format(),
&commands,
&[],
limits,
cancel,
false,
false,
)?;
observe(PreparationProgress::Reading { objects: 0 });
check_cancelled(cancel)?;
if commands.iter().all(PushCommand::deletes) {
return Ok(Self {
format: objects.object_format(),
commands,
request,
pack: vec![].into(),
limits,
objects: 0,
receiver_roots: vec![],
has_options: false,
options: vec![],
progress: false,
});
}
let mut selection = graph::select(
objects,
&commands,
receiver_roots,
limits,
cancel,
|objects| observe(PreparationProgress::Reading { objects }),
)?;
let delta_bases: Vec<_> = selection
.delta_bases
.iter()
.map(|&(id, kind, _)| (id, kind))
.collect();
let mut temporary = tempfile::NamedTempFile::new()?;
let mut pack = CancelWriter {
output: std::io::BufWriter::with_capacity(64 * 1024, temporary.as_file_mut()),
cancel,
};
let mut index = std::io::sink();
let result = crate::pack::write_transfer(
objects,
&selection.objects,
&delta_bases,
(&mut pack, &mut index),
limits.pack,
limits.compression,
(limits.read, cancel),
limits.read.max_delta_depth,
crate::pack::DeltaFraming::Identity,
&mut || {
if cancel.load(Ordering::Relaxed) {
Err(io::Error::other("push cancelled").into())
} else {
Ok(())
}
},
&mut |done, total| {
observe(PreparationProgress::Packing {
objects: (done, total),
})
},
);
check_cancelled(cancel)?;
pack.flush()?;
drop(pack);
let (written, used_bases) = result.map_err(|error| match error {
crate::pack::ReuseError::Read { id, source } => Error::Read { id, source },
crate::pack::ReuseError::Write(error) => Error::Pack(error),
})?;
for (base, _, root) in selection.delta_bases {
if used_bases.contains(&base) {
selection.receiver_roots.push(root);
}
}
selection.receiver_roots.sort_unstable();
selection.receiver_roots.dedup();
Ok(Self {
format: objects.object_format(),
commands,
request,
pack: crate::fetch::Bytes::File(temporary, written.pack_bytes as usize),
limits,
objects: written.objects,
receiver_roots: selection.receiver_roots,
has_options: false,
options: vec![],
progress: false,
})
};
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = { operation }();
let result = result.and_then(|prepared| {
check_cancelled(cancel)?;
observe(PreparationProgress::Complete);
Ok(prepared)
});
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, crate::trace::push_failure);
#[cfg(feature = "tracing")]
if let Ok(value) = &result {
span.record("objects", value.object_count())
.record("pack_bytes", value.pack_bytes());
}
result
}
pub fn object_count(&self) -> u32 {
self.objects
}
pub fn pack_bytes(&self) -> usize {
self.pack.len()
}
pub fn commands(&self) -> &[PushCommand] {
&self.commands
}
pub fn with_push_options(mut self, options: Vec<Vec<u8>>) -> Result<Self, Error> {
if !options.is_empty() && self.commands.is_empty() {
return Err(Error::Command("push options require commands"));
}
self.request = encode_commands(
self.format,
&self.commands,
&options,
self.limits,
&AtomicBool::new(false),
false,
false,
)?;
self.has_options = !options.is_empty();
self.options = options;
Ok(self)
}
pub fn with_progress(mut self) -> Self {
self.progress = true;
self
}
#[cfg(all(feature = "ssh", any(target_os = "macos", target_os = "linux")))]
pub(super) fn selected_commands(
&self,
names: Vec<crate::refs::RefName>,
) -> Result<Vec<PushCommand>, Error> {
let mut selected: HashSet<_> = names.iter().collect();
if selected.len() != names.len() {
return Err(Error::Command("duplicate selected destination"));
}
let mut commands = Vec::new();
for command in &self.commands {
if selected.remove(&command.name) {
commands.push(command.clone());
}
}
if !selected.is_empty() {
return Err(Error::Command("selected destination was not prepared"));
}
Ok(commands)
}
#[cfg(all(feature = "ssh", any(target_os = "macos", target_os = "linux")))]
pub(super) fn selected_request(
&self,
commands: &[PushCommand],
report_v2: bool,
sideband: bool,
cancel: &AtomicBool,
) -> Result<Vec<u8>, Error> {
let options = if commands.is_empty() {
&[]
} else {
self.options.as_slice()
};
encode_commands(
self.format,
commands,
options,
self.limits,
cancel,
report_v2,
sideband,
)
}
pub(super) fn request_for(
&self,
report_v2: bool,
sideband: bool,
) -> Result<Cow<'_, [u8]>, Error> {
if (!report_v2 && !sideband) || self.commands.is_empty() {
return Ok(Cow::Borrowed(&self.request));
}
encode_commands(
self.format,
&self.commands,
&self.options,
self.limits,
&AtomicBool::new(false),
report_v2,
sideband,
)
.map(Cow::Owned)
}
}
fn encode_commands(
format: crate::ObjectFormat,
commands: &[PushCommand],
options: &[Vec<u8>],
limits: PushLimits,
cancel: &AtomicBool,
report_v2: bool,
sideband: bool,
) -> Result<Vec<u8>, Error> {
if commands.len() > limits.max_commands {
return Err(Error::Limit("commands"));
}
let mut names = HashSet::new();
let mut request = Vec::new();
for (i, command) in commands.iter().enumerate() {
check_cancelled(cancel)?;
command.new.require_format(format)?;
if let Some(id) = command.expected {
id.require_format(format)?;
}
let name = command.name.as_bytes();
if !name.starts_with(b"refs/") {
return Err(Error::Unsupported("destination namespace"));
}
if command.expected.is_some_and(ObjectId::is_null) {
return Err(Error::Command("zero expected object ID"));
}
if command.deletes() && command.expected.is_none() {
return Err(Error::Command("deletion requires an expected old value"));
}
if !names.insert(&command.name) {
return Err(Error::Command("duplicate destination"));
}
let caps = if i == 0 {
let base = match (format, options.is_empty(), report_v2) {
(crate::ObjectFormat::Sha1, true, false) => b"\0report-status".as_slice(),
(crate::ObjectFormat::Sha1, false, false) => {
b"\0report-status push-options".as_slice()
}
(crate::ObjectFormat::Sha256, true, false) => {
b"\0report-status object-format=sha256".as_slice()
}
(crate::ObjectFormat::Sha256, false, false) => {
b"\0report-status push-options object-format=sha256".as_slice()
}
(crate::ObjectFormat::Sha1, true, true) => b"\0report-status-v2".as_slice(),
(crate::ObjectFormat::Sha1, false, true) => {
b"\0report-status-v2 push-options".as_slice()
}
(crate::ObjectFormat::Sha256, true, true) => {
b"\0report-status-v2 object-format=sha256".as_slice()
}
(crate::ObjectFormat::Sha256, false, true) => {
b"\0report-status-v2 push-options object-format=sha256".as_slice()
}
};
let mut caps = base.to_vec();
if sideband {
caps.extend_from_slice(b" side-band-64k");
}
caps
} else {
vec![]
};
let length = (format.digest_len() * 4 + 2)
.checked_add(name.len())
.and_then(|n| n.checked_add(caps.len() + 4))
.ok_or(Error::Limit("command bytes"))?;
if length > 65520 || length > limits.max_command_bytes.saturating_sub(request.len()) {
return Err(Error::Limit("command bytes"));
}
let old = command.expected.unwrap_or(ObjectId::null(format));
let mut line = format!("{old} {} ", command.new).into_bytes();
line.extend_from_slice(name);
line.extend_from_slice(&caps);
packet(&mut request, &line, cancel)?;
}
if limits.max_command_bytes.saturating_sub(request.len()) < 4 {
return Err(Error::Limit("command bytes"));
}
put(&mut request, b"0000", cancel)?;
if !options.is_empty() {
for option in options {
if option.iter().any(|b| *b == 0 || *b == b'\n') {
return Err(Error::Command("invalid push option"));
}
if option.len() > 65516
|| option.len() + 4 > limits.max_command_bytes.saturating_sub(request.len())
{
return Err(Error::Limit("push option bytes"));
}
packet(&mut request, option, cancel)?;
}
if limits.max_command_bytes.saturating_sub(request.len()) < 4 {
return Err(Error::Limit("push option bytes"));
}
put(&mut request, b"0000", cancel)?;
}
Ok(request)
}
struct CancelWriter<'a, W> {
output: W,
cancel: &'a AtomicBool,
}
impl<W: Write> Write for CancelWriter<'_, W> {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
if self.cancel.load(Ordering::Relaxed) {
return Err(io::Error::other("push cancelled"));
}
self.output.write(bytes)
}
fn flush(&mut self) -> io::Result<()> {
self.output.flush()
}
}