use bstr::{BString, ByteSlice};
use crate::advertisement::chomp;
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefStatus {
pub name: BString,
pub accepted: bool,
pub reason: Option<String>,
pub options: Vec<(String, Option<String>)>,
}
impl RefStatus {
pub fn ok(name: impl Into<BString>) -> Self {
RefStatus {
name: name.into(),
accepted: true,
reason: None,
options: Vec::new(),
}
}
pub fn rejected(name: impl Into<BString>, reason: impl Into<String>) -> Self {
RefStatus {
name: name.into(),
accepted: false,
reason: Some(reason.into()),
options: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PushReport {
pub unpack: String,
pub refs: Vec<RefStatus>,
pub progress: Vec<String>,
pub remote_errors: Vec<String>,
pub unreported: Vec<BString>,
}
impl PushReport {
pub fn is_ok(&self) -> bool {
self.unpack == "ok"
&& self.unreported.is_empty()
&& self.remote_errors.is_empty()
&& self.refs.iter().all(|r| r.accepted)
}
pub fn reconcile(&mut self, commanded: &[BString]) {
self.unreported = commanded
.iter()
.filter(|name| !self.refs.iter().any(|r| &r.name == *name))
.cloned()
.collect();
for status in &mut self.refs {
if !commanded.iter().any(|name| name == &status.name) && status.accepted {
status.accepted = false;
status.reason = Some(
"the remote reported on a reference this push did not command".to_string(),
);
}
}
}
pub fn rejected(&self) -> impl Iterator<Item = &RefStatus> {
self.refs.iter().filter(|r| !r.accepted)
}
pub fn remote_lines(&self) -> impl Iterator<Item = &str> {
self.progress
.iter()
.chain(self.remote_errors.iter())
.map(|l| l.trim_end_matches(['\n', '\r']))
.filter(|l| !l.is_empty())
}
pub fn failure_summary(&self) -> Option<String> {
if self.is_ok() {
return None;
}
let mut parts = Vec::new();
if self.unpack != "ok" {
parts.push(format!("unpack: {}", self.unpack));
}
for r in self.rejected() {
parts.push(format!(
"{}: {}",
r.name,
r.reason.as_deref().unwrap_or("rejected")
));
}
for name in &self.unreported {
parts.push(format!("{name}: remote failed to report status"));
}
for e in self.remote_lines() {
parts.push(format!("remote: {e}"));
}
Some(parts.join("; "))
}
}
pub fn parse(lines: &[Vec<u8>]) -> Result<PushReport> {
let mut report = PushReport::default();
let mut saw_unpack = false;
for line in lines {
let raw = chomp(line);
let text = std::str::from_utf8(raw)
.map_err(|_| Error::protocol(format!("non-UTF-8 report line: {:?}", raw.as_bstr())))?;
if let Some(msg) = text.strip_prefix("ERR ") {
return Err(Error::Remote(msg.to_string()));
} else if let Some(status) = text.strip_prefix("unpack ") {
report.unpack = status.to_string();
saw_unpack = true;
} else if let Some(name) = text.strip_prefix("ok ") {
report.refs.push(RefStatus::ok(name));
} else if let Some(rest) = text.strip_prefix("ng ") {
let (name, reason) = rest.split_once(' ').unwrap_or((rest, ""));
report.refs.push(RefStatus::rejected(name, reason));
} else if let Some(rest) = text.strip_prefix("option ") {
let (key, value) = match rest.split_once(' ') {
Some((k, v)) => (k.to_string(), Some(v.to_string())),
None => (rest.to_string(), None),
};
match report.refs.last_mut() {
Some(r) => r.options.push((key, value)),
None => {
return Err(Error::protocol(format!(
"report-status-v2 sent `option {rest}` before any ref status"
)))
}
}
} else if text.is_empty() {
continue;
} else {
return Err(Error::protocol(format!(
"unrecognised report-status line: {text:?}"
)));
}
}
if !saw_unpack && !lines.is_empty() {
return Err(Error::protocol(
"the remote reported ref statuses without an `unpack` line",
));
}
Ok(report)
}
pub fn lines(unpack: std::result::Result<(), &str>, statuses: &[RefStatus]) -> Vec<Vec<u8>> {
let mut out = Vec::with_capacity(statuses.len() + 1);
out.push(match unpack {
Ok(()) => b"unpack ok".to_vec(),
Err(reason) => format!("unpack {}", one_line(reason)).into_bytes(),
});
for s in statuses {
let mut line = if s.accepted {
b"ok ".to_vec()
} else {
b"ng ".to_vec()
};
line.extend_from_slice(s.name.as_slice());
if !s.accepted {
line.push(b' ');
line.extend_from_slice(one_line(s.reason.as_deref().unwrap_or("rejected")).as_bytes());
}
out.push(line);
for (key, value) in &s.options {
let mut opt = format!("option {key}");
if let Some(v) = value {
opt.push(' ');
opt.push_str(&one_line(v));
}
out.push(opt.into_bytes());
}
}
out
}
pub fn one_line(reason: &str) -> String {
reason.replace(['\n', '\r'], " ")
}