blob: 4bfca349ec69606ed55314414fcd6fea78281c26 [file] [log] [blame]
use std::collections::BTreeMap;
use bstr::BString;
use crate::{
match_group::{Outcome, Source},
RefSpec,
};
/// All possible issues found while validating matched mappings.
#[derive(Debug, PartialEq, Eq)]
pub enum Issue {
/// Multiple sources try to write the same destination.
///
/// Note that this issue doesn't take into consideration that these sources might contain the same object behind a reference.
Conflict {
/// The unenforced full name of the reference to be written.
destination_full_ref_name: BString,
/// The list of sources that map to this destination.
sources: Vec<Source>,
/// The list of specs that caused the mapping conflict, each matching the respective one in `sources` to allow both
/// `sources` and `specs` to be zipped together.
specs: Vec<BString>,
},
}
impl std::fmt::Display for Issue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Issue::Conflict {
destination_full_ref_name,
sources,
specs,
} => {
write!(
f,
"Conflicting destination {destination_full_ref_name:?} would be written by {}",
sources
.iter()
.zip(specs.iter())
.map(|(src, spec)| format!("{src} ({spec:?})"))
.collect::<Vec<_>>()
.join(", ")
)
}
}
}
}
/// All possible fixes corrected while validating matched mappings.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Fix {
/// Removed a mapping that contained a partial destination entirely.
MappingWithPartialDestinationRemoved {
/// The destination ref name that was ignored.
name: BString,
/// The spec that defined the mapping
spec: RefSpec,
},
}
/// The error returned [outcome validation][Outcome::validated()].
#[derive(Debug)]
pub struct Error {
/// All issues discovered during validation.
pub issues: Vec<Issue>,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Found {} {} the refspec mapping to be used: \n\t{}",
self.issues.len(),
if self.issues.len() == 1 {
"issue that prevents"
} else {
"issues that prevent"
},
self.issues
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n\t")
)
}
}
impl std::error::Error for Error {}
impl<'spec, 'item> Outcome<'spec, 'item> {
/// Validate all mappings or dissolve them into an error stating the discovered issues.
/// Return `(modified self, issues)` providing a fixed-up set of mappings in `self` with the fixed `issues`
/// provided as part of it.
/// Terminal issues are communicated using the [`Error`] type accordingly.
pub fn validated(mut self) -> Result<(Self, Vec<Fix>), Error> {
let mut sources_by_destinations = BTreeMap::new();
for (dst, (spec_index, src)) in self
.mappings
.iter()
.filter_map(|m| m.rhs.as_ref().map(|dst| (dst.as_ref(), (m.spec_index, &m.lhs))))
{
let sources = sources_by_destinations.entry(dst).or_insert_with(Vec::new);
if !sources.iter().any(|(_, lhs)| lhs == &src) {
sources.push((spec_index, src))
}
}
let mut issues = Vec::new();
for (dst, conflicting_sources) in sources_by_destinations.into_iter().filter(|(_, v)| v.len() > 1) {
issues.push(Issue::Conflict {
destination_full_ref_name: dst.to_owned(),
specs: conflicting_sources
.iter()
.map(|(spec_idx, _)| self.group.specs[*spec_idx].to_bstring())
.collect(),
sources: conflicting_sources.into_iter().map(|(_, src)| src.to_owned()).collect(),
})
}
if !issues.is_empty() {
Err(Error { issues })
} else {
let mut fixed = Vec::new();
let group = &self.group;
self.mappings.retain(|m| match m.rhs.as_ref() {
Some(dst) => {
if dst.starts_with(b"refs/") || dst.as_ref() == "HEAD" {
true
} else {
fixed.push(Fix::MappingWithPartialDestinationRemoved {
name: dst.as_ref().to_owned(),
spec: group.specs[m.spec_index].to_owned(),
});
false
}
}
None => true,
});
Ok((self, fixed))
}
}
}