use std::collections::BTreeMap;
pub(crate) struct Refspec {
pub(crate) negative: bool,
pub(crate) source: String,
pub(crate) destination: Option<String>,
pub(crate) pattern: bool,
}
pub(crate) fn parse_refspec(text: &str) -> Option<Refspec> {
if let Some(source) = text.strip_prefix('^') {
let negative_ok = source.starts_with("refs/") && !source.contains(':') && star_count(source) <= 1;
if negative_ok {
return Some(Refspec { negative: true, source: source.to_owned(), destination: None, pattern: star_count(source) == 1 });
}
return None;
}
let spec = text.strip_prefix('+').unwrap_or(text);
let (source, destination) = match spec.split_once(':') {
Some((source, destination)) => (source, Some(destination)),
None => (spec, None),
};
if !source.starts_with("refs/") || side_outside_refs(destination) {
return None;
}
let pattern = star_count(source) == 1;
let sides_agree = star_count(source) == destination.map_or(star_count(source), star_count);
if star_count(source) > 1 || !sides_agree {
return None;
}
return Some(Refspec { negative: false, source: source.to_owned(), destination: destination.map(str::to_owned), pattern });
}
pub(crate) fn star_count(side: &str) -> usize {
return side.matches('*').count();
}
pub(crate) fn side_outside_refs(destination: Option<&str>) -> bool {
return match destination {
Some(side) => !side.starts_with("refs/"),
None => false,
};
}
pub(crate) fn capture<'a>(pattern: &str, name: &'a str) -> Option<&'a str> {
let (prefix, suffix) = pattern.split_once('*')?;
let middle = name.strip_prefix(prefix)?.strip_suffix(suffix)?;
return Some(middle);
}
pub(crate) fn excludes(negative: &Refspec, name: &str) -> bool {
if negative.pattern {
return capture(&negative.source, name).is_some();
}
return negative.source == name;
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum Judged {
Targets(Vec<String>),
Refused(String),
Unjudged(String),
}
pub(crate) fn judge(branch_ref: &str, remote: &str, refspecs: &[String], local_refs: &[String]) -> Judged {
let mut items: Vec<Refspec> = Vec::new();
for text in refspecs {
match parse_refspec(text) {
Some(item) => items.push(item),
None => return Judged::Unjudged(text.clone()),
}
}
let mut taken: BTreeMap<String, String> = BTreeMap::new();
for item in &items {
if item.negative || item.pattern {
continue;
}
if !local_refs.contains(&item.source) {
return Judged::Refused(format!(
"`remote.{remote}.push` names `{}`, which is not a ref here, and Git refuses to push anything while it is not.",
item.source
));
}
let destination: String = match item.destination.clone() {
Some(side) => side,
None => item.source.clone(),
};
if let Some(earlier) = taken.insert(destination.clone(), item.source.clone()) {
return Judged::Refused(format!(
"`remote.{remote}.push` sends both `{earlier}` and `{}` to `{destination}`, and Git refuses that (\"receives from more than one src\").",
item.source
));
}
}
let mut sorted: Vec<&String> = local_refs.iter().collect();
sorted.sort();
for name in sorted {
if let Some(destination) = mapped_destination(&items, name)
&& !taken.contains_key(&destination)
{
taken.insert(destination, name.clone());
}
}
for negative in &items {
if !negative.negative {
continue;
}
let mut kept: BTreeMap<String, String> = BTreeMap::new();
for (destination, source) in taken {
if !excludes(negative, &destination) {
kept.insert(destination, source);
}
}
taken = kept;
}
let mut targets: Vec<String> = Vec::new();
for (destination, source) in taken {
if source == branch_ref {
targets.push(destination);
}
}
return Judged::Targets(targets);
}
pub(crate) fn mapped_destination(items: &[Refspec], name: &str) -> Option<String> {
for item in items {
if item.negative || !item.pattern {
continue;
}
let Some(middle) = capture(&item.source, name) else {
continue;
};
let side: &str = item.destination.as_deref().unwrap_or(item.source.as_str());
return Some(side.replacen('*', middle, 1));
}
return None;
}