gix_refspec/match_group/
validate.rs

1use std::collections::BTreeMap;
2
3use bstr::BString;
4
5use crate::{
6    match_group::{match_lhs, Source},
7    RefSpec,
8};
9
10/// The error returned [outcome validation](match_lhs::Outcome::validated()).
11#[derive(Debug)]
12pub struct Error {
13    /// All issues discovered during validation.
14    pub issues: Vec<Issue>,
15}
16
17impl std::fmt::Display for Error {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        write!(
20            f,
21            "Found {} {} the refspec mapping to be used: \n\t{}",
22            self.issues.len(),
23            if self.issues.len() == 1 {
24                "issue that prevents"
25            } else {
26                "issues that prevent"
27            },
28            self.issues
29                .iter()
30                .map(ToString::to_string)
31                .collect::<Vec<_>>()
32                .join("\n\t")
33        )
34    }
35}
36
37impl std::error::Error for Error {}
38
39/// All possible issues found while validating matched mappings.
40#[derive(Debug, PartialEq, Eq)]
41pub enum Issue {
42    /// Multiple sources try to write the same destination.
43    ///
44    /// Note that this issue doesn't take into consideration that these sources might contain the same object behind a reference.
45    Conflict {
46        /// The unenforced full name of the reference to be written.
47        destination_full_ref_name: BString,
48        /// The list of sources that map to this destination.
49        sources: Vec<Source>,
50        /// The list of specs that caused the mapping conflict, each matching the respective one in `sources` to allow both
51        /// `sources` and `specs` to be zipped together.
52        specs: Vec<BString>,
53    },
54}
55
56impl std::fmt::Display for Issue {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            Issue::Conflict {
60                destination_full_ref_name,
61                sources,
62                specs,
63            } => {
64                write!(
65                    f,
66                    "Conflicting destination {destination_full_ref_name:?} would be written by {}",
67                    sources
68                        .iter()
69                        .zip(specs.iter())
70                        .map(|(src, spec)| format!("{src} ({spec:?})"))
71                        .collect::<Vec<_>>()
72                        .join(", ")
73                )
74            }
75        }
76    }
77}
78
79/// All possible fixes corrected while validating matched mappings.
80#[derive(Debug, PartialEq, Eq, Clone)]
81pub enum Fix {
82    /// Removed a mapping that contained a partial destination entirely.
83    MappingWithPartialDestinationRemoved {
84        /// The destination ref name that was ignored.
85        name: BString,
86        /// The spec that defined the mapping
87        spec: RefSpec,
88    },
89}
90
91impl match_lhs::Outcome<'_, '_> {
92    /// Validate all mappings or dissolve them into an error stating the discovered issues.
93    /// Return `(modified self, issues)` providing a fixed-up set of mappings in `self` with the fixed `issues`
94    /// provided as part of it.
95    /// Terminal issues are communicated using the [`Error`] type accordingly.
96    pub fn validated(mut self) -> Result<(Self, Vec<Fix>), Error> {
97        let mut sources_by_destinations = BTreeMap::new();
98        for (dst, (spec_index, src)) in self
99            .mappings
100            .iter()
101            .filter_map(|m| m.rhs.as_ref().map(|dst| (dst.as_ref(), (m.spec_index, &m.lhs))))
102        {
103            let sources = sources_by_destinations.entry(dst).or_insert_with(Vec::new);
104            if !sources.iter().any(|(_, lhs)| lhs == &src) {
105                sources.push((spec_index, src));
106            }
107        }
108        let mut issues = Vec::new();
109        for (dst, conflicting_sources) in sources_by_destinations.into_iter().filter(|(_, v)| v.len() > 1) {
110            issues.push(Issue::Conflict {
111                destination_full_ref_name: dst.to_owned(),
112                specs: conflicting_sources
113                    .iter()
114                    .map(|(spec_idx, _)| self.group.specs[*spec_idx].to_bstring())
115                    .collect(),
116                sources: conflicting_sources
117                    .into_iter()
118                    .map(|(_, src)| src.clone().into_owned())
119                    .collect(),
120            });
121        }
122        if !issues.is_empty() {
123            Err(Error { issues })
124        } else {
125            let mut fixed = Vec::new();
126            let group = &self.group;
127            self.mappings.retain(|m| match m.rhs.as_ref() {
128                Some(dst) => {
129                    if dst.starts_with(b"refs/") || dst.as_ref() == "HEAD" {
130                        true
131                    } else {
132                        fixed.push(Fix::MappingWithPartialDestinationRemoved {
133                            name: dst.as_ref().to_owned(),
134                            spec: group.specs[m.spec_index].to_owned(),
135                        });
136                        false
137                    }
138                }
139                None => true,
140            });
141            Ok((self, fixed))
142        }
143    }
144}