1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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))
        }
    }
}