use std::collections::HashSet;
use bstr::{BString, ByteSlice};
use gix_hash::{Kind, ObjectId};
use crate::capabilities;
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PushCommand {
pub name: BString,
pub old: ObjectId,
pub new: ObjectId,
}
impl PushCommand {
pub fn update(name: impl Into<BString>, old: ObjectId, new: ObjectId) -> Self {
PushCommand {
name: name.into(),
old,
new,
}
}
pub fn create(name: impl Into<BString>, new: ObjectId) -> Self {
PushCommand {
name: name.into(),
old: ObjectId::null(new.kind()),
new,
}
}
pub fn delete(name: impl Into<BString>, old: ObjectId) -> Self {
PushCommand {
name: name.into(),
new: ObjectId::null(old.kind()),
old,
}
}
pub fn is_delete(&self) -> bool {
self.new.is_null()
}
pub fn is_create(&self) -> bool {
self.old.is_null()
}
pub fn line(&self) -> Vec<u8> {
let mut out = format!("{} {} ", self.old.to_hex(), self.new.to_hex()).into_bytes();
out.extend_from_slice(self.name.as_slice());
out
}
}
pub const SHALLOW_PREFIX: &[u8] = b"shallow ";
pub fn parse_line(line: &[u8], hash_kind: Kind) -> Result<PushCommand> {
let mut parts = line.splitn(3, |&b| b == b' ');
let (Some(old), Some(new), Some(name)) = (parts.next(), parts.next(), parts.next()) else {
return Err(Error::protocol(format!(
"{:?} is not an `<old> <new> <ref>` command",
line.as_bstr()
)));
};
let parse = |hex: &[u8]| -> Result<ObjectId> {
let id = ObjectId::from_hex(hex).map_err(|err| {
Error::protocol(format!("{:?} is not an object id: {err}", hex.as_bstr()))
})?;
if id.kind() != hash_kind {
return Err(Error::ObjectFormat {
ours: crate::advertisement::object_format_name(hash_kind).unwrap_or("unsupported"),
theirs: crate::advertisement::object_format_name(id.kind())
.unwrap_or("unsupported"),
});
}
Ok(id)
};
let (old, new) = (parse(old)?, parse(new)?);
if old.is_null() && new.is_null() {
return Err(Error::protocol(format!(
"{:?} has a null old AND a null new oid, which is neither a create, \
an update nor a delete",
name.as_bstr()
)));
}
Ok(PushCommand {
name: name.into(),
old,
new,
})
}
pub fn lines(commands: &[PushCommand], caps: &[String]) -> Result<Vec<Vec<u8>>> {
if commands.is_empty() {
return Err(Error::invalid(
"a push with no commands has nothing to say; send nothing instead",
));
}
let mut seen: HashSet<&[u8]> = HashSet::new();
for c in commands {
if !seen.insert(c.name.as_slice()) {
return Err(Error::invalid(format!(
"the command list names {:?} twice; the remote would apply one of \
them and the client could not say which",
c.name.as_bstr()
)));
}
if c.is_delete() && c.is_create() {
return Err(Error::invalid(format!(
"{:?} has a null old AND a null new oid, which is neither a create, \
an update nor a delete",
c.name.as_bstr()
)));
}
if c.name.contains(&b'\n') || c.name.contains(&0) {
return Err(Error::invalid(format!(
"the reference name {:?} contains a newline or a NUL, either of \
which would be read as the end of the line",
c.name.as_bstr()
)));
}
}
let mut out = Vec::with_capacity(commands.len());
for (i, c) in commands.iter().enumerate() {
let mut line = c.line();
if i == 0 {
capabilities::attach(&mut line, caps);
}
out.push(line);
}
Ok(out)
}
pub fn push_option_lines(options: &[String]) -> Result<Vec<Vec<u8>>> {
options
.iter()
.map(|opt| {
if opt.contains('\n') {
return Err(Error::invalid(format!(
"push option {opt:?} contains a newline, which the wire cannot carry"
)));
}
Ok(opt.as_bytes().to_vec())
})
.collect()
}