Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
9.7 kB · 334 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335use gix_object::bstr::{BStr, BString, ByteVec};
use crate::{Options, Update};
/// The command block could not be built.#[derive(Debug, thiserror::Error)]pub enum Error { #[error("there is nothing to push")] NoUpdates, #[error("the receiving end does not support `{0}`")] UnsupportedCapability(&'static str), #[error(transparent)] Io(#[from] std::io::Error),}
/// Intersect what this push needs with what the remote advertised, producing the capability list/// that rides the first command.pub(crate) fn select_capabilities( caps: &gix_transport::client::Capabilities, updates: &[Update], opts: &Options<'_>, agent: Option<&str>,) -> Result<BString, Error> { // TODO(boltless): support `report-status-v2` let mut out = BString::from("report-status"); let mut want = |name: &'static str| { if caps.contains(name) { out.push(b' '); out.push_str(name); true } else { false } };
want("ofs-delta"); want("side-band-64k");
if opts.quiet && !want("quiet") { return Err(Error::UnsupportedCapability("quiet")); } if opts.atomic && !want("atomic") { return Err(Error::UnsupportedCapability("atomic")); } if !opts.push_options.is_empty() && !want("push-options") { return Err(Error::UnsupportedCapability("push-options")); } if let Some(agent) = agent.filter(|_| caps.contains("agent")) { out.push_str(" agent="); out.push_str(agent); }
if updates.iter().any(|u| u.new.is_null()) && !caps.contains("delete-refs") { return Err(Error::UnsupportedCapability("delete-refs")); }
Ok(out)}
/// Encode the reference update request that precedes the packfile:////// ```text/// <old> <new> <ref>\0<capabilities> <- capabilities ride the FIRST command only/// <old> <new> <ref> <- bare thereafter/// 0000/// <push-option> <- only when there are options.../// 0000 <- ...and then this second flush is mandatory/// ```pub(crate) fn encode_command_block( updates: &[Update], capabilities: &BStr, push_options: &[BString],) -> Result<Vec<u8>, Error> { use gix_packetline::blocking_io::encode; use std::io::Write;
if updates.is_empty() { return Err(Error::NoUpdates); }
let mut out = Vec::new(); let mut line = Vec::new();
for (index, update) in updates.iter().enumerate() { line.clear(); write!( &mut line, "{} {} {}", update.expected_old.to_hex(), update.new.to_hex(), update.name )?; if index == 0 { line.push(0); line.extend_from_slice(capabilities); } encode::text_to_write(&line, &mut out)?; } encode::flush_to_write(&mut out)?;
if !push_options.is_empty() { for option in push_options { encode::text_to_write(option, &mut out)?; } encode::flush_to_write(&mut out)?; }
Ok(out)}
#[cfg(test)]mod tests { use std::collections::HashMap;
use gix_hash::ObjectId; use gix_object::bstr::ByteSlice;
use crate::ReceivePackAdvertisement;
use super::*;
const A: &str = "1111111111111111111111111111111111111111"; const B: &str = "2222222222222222222222222222222222222222"; const NULL: &str = "0000000000000000000000000000000000000000";
fn oid(hex: &str) -> ObjectId { ObjectId::from_hex(hex.as_bytes()).unwrap() }
/// Frame a payload as `text_to_write` would: 4 hex length digits over payload + `\n`. fn pkt(payload: &str) -> String { format!("{:04x}{payload}\n", payload.len() + 5) }
fn advertised(caps: &str) -> ReceivePackAdvertisement { let (capabilities, _) = gix_transport::client::Capabilities::from_bytes(format!("\0{caps}").as_bytes()) .unwrap(); ReceivePackAdvertisement { server_protocol_version: gix_transport::Protocol::V1, capabilities, remote_refs: HashMap::new(), advertised_haves: Vec::new(), } }
fn opts() -> Options<'static> { Options { atomic: false, push_options: &[], quiet: false, } }
fn update(name: &str, old: &str, new: &str) -> Update { Update { name: gix_ref::FullName::try_from(name).unwrap(), expected_old: oid(old), new: oid(new), } }
#[test] fn capabilities_ride_the_first_command_only() { let block = encode_command_block( &[ update("refs/heads/main", A, B), update("refs/heads/next", A, B), ], "report-status side-band-64k".into(), &[], ) .unwrap(); assert_eq!( block.to_str().unwrap(), format!( "{}{}0000", pkt(&format!( "{A} {B} refs/heads/main\0report-status side-band-64k" )), pkt(&format!("{A} {B} refs/heads/next")) ), "one NUL in the whole block, and exactly one newline per line" ); }
#[test] fn creates_and_deletes_use_the_null_id() { let block = encode_command_block( &[ update("refs/heads/new", NULL, B), update("refs/heads/gone", A, NULL), ], "report-status".into(), &[], ) .unwrap(); assert_eq!( block.to_str().unwrap(), format!( "{}{}0000", pkt(&format!("{NULL} {B} refs/heads/new\0report-status")), pkt(&format!("{A} {NULL} refs/heads/gone")) ) ); }
#[test] fn a_null_id_goes_on_the_wire_at_the_width_the_caller_gave_it() { let block = encode_command_block( &[Update { name: gix_ref::FullName::try_from("refs/heads/gone").unwrap(), expected_old: ObjectId::from_hex(&[b'1'; 64]).unwrap(), new: ObjectId::null(gix_hash::Kind::Sha256), }], "report-status delete-refs".into(), &[], ) .unwrap(); assert!( block.to_str().unwrap().contains(&"0".repeat(64)), "64 zeroes, not 40: {:?}", block.to_str().unwrap() ); }
#[test] fn push_options_add_a_second_flush() { let one = |options: &[BString]| { encode_command_block( &[update("refs/heads/main", A, B)], "report-status".into(), options, ) .unwrap() }; assert!( one(&[]).ends_with(b"0000"), "one flush when there are no options" ); assert_eq!( one(&["ci.skip".into()]).to_str().unwrap(), format!( "{}0000{}0000", pkt(&format!("{A} {B} refs/heads/main\0report-status")), pkt("ci.skip") ) ); }
#[test] fn nothing_to_push_is_an_error() { assert!(matches!( encode_command_block(&[], "report-status".into(), &[]), Err(Error::NoUpdates) )); }
#[test] fn selection_only_asks_for_what_was_advertised() { let adv = advertised("report-status ofs-delta side-band-64k agent=git/2.0"); let caps = select_capabilities( &adv.capabilities, &[update("refs/heads/main", A, B)], &opts(), Some("gix/1.0"), ) .unwrap(); assert_eq!(caps, "report-status ofs-delta side-band-64k agent=gix/1.0");
let bare = advertised("report-status"); let caps = select_capabilities( &bare.capabilities, &[update("refs/heads/main", A, B)], &opts(), Some("gix/1.0"), ) .unwrap(); assert_eq!( caps, "report-status", "no agent, no side-band, no ofs-delta" ); }
#[test] fn opt_in_capabilities_that_were_not_advertised_are_errors() { let adv = advertised("report-status"); let up = [update("refs/heads/main", A, B)];
let atomic = Options { atomic: true, ..opts() }; assert!(matches!( select_capabilities(&adv.capabilities, &up, &atomic, None), Err(Error::UnsupportedCapability("atomic")) ));
let options = [BString::from("ci.skip")]; let with_options = Options { push_options: &options, ..opts() }; assert!(matches!( select_capabilities(&adv.capabilities, &up, &with_options, None), Err(Error::UnsupportedCapability("push-options")) )); }
#[test] fn deleting_needs_delete_refs_but_never_sends_it() { let delete = [update("refs/heads/gone", A, NULL)]; assert!(matches!( select_capabilities( &advertised("report-status").capabilities, &delete, &opts(), None ), Err(Error::UnsupportedCapability("delete-refs")) )); let caps = select_capabilities( &advertised("report-status delete-refs").capabilities, &delete, &opts(), None, ) .unwrap(); assert_eq!( caps, "report-status", "the client does not echo `delete-refs`" ); }}