#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RemoteBefore {
Absent,
Known(String),
Unknown,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PorcelainUpdate {
pub from: String,
pub to: String,
pub before: RemoteBefore,
pub after: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PorcelainBlock {
pub shown: String,
pub updates: Vec<PorcelainUpdate>,
}
const SET_UPSTREAM_PREFIX: &str = "Would set upstream of ";
fn parse_summary(summary: &str) -> (RemoteBefore, Option<String>) {
let token: &str = summary.split(' ').next().unwrap_or_default();
if token == "[new" {
return (RemoteBefore::Absent, None);
}
let range: Option<(&str, &str)> = token.split_once("...").or_else(|| {
return token.split_once("..");
});
return match range {
Some((old, new)) if !old.is_empty() && !new.is_empty() => {
(RemoteBefore::Known(old.to_owned()), Some(new.to_owned()))
}
Some(_) | None => (RemoteBefore::Unknown, None),
};
}
pub fn parse_porcelain(output: &str) -> Result<Vec<PorcelainBlock>, String> {
let mut blocks: Vec<PorcelainBlock> = Vec::new();
for line in output.lines() {
if let Some(shown) = line.strip_prefix("To ") {
blocks.push(PorcelainBlock { shown: shown.to_owned(), updates: Vec::new() });
continue;
}
if line == "Done" || line.is_empty() || line.starts_with(SET_UPSTREAM_PREFIX) {
continue;
}
let mut fields = line.splitn(3, '\t');
let (Some(flag), Some(spec), summary) = (fields.next(), fields.next(), fields.next().unwrap_or_default()) else {
return Err(format!("Malformed porcelain line: {line}"));
};
let Some((from, to)) = spec.rsplit_once(':') else {
return Err(format!("Malformed porcelain ref pair: {line}"));
};
let Some(block) = blocks.last_mut() else {
return Err(format!("Porcelain line before any destination: {line}"));
};
if flag == "=" {
continue;
}
let (before, after) = parse_summary(summary);
block.updates.push(PorcelainUpdate { from: from.to_owned(), to: to.to_owned(), before, after });
}
return Ok(blocks);
}
#[cfg(test)]
#[path = "manual_push_porcelain_tests.rs"]
mod tests;