gix_refspec/match_group/
validate.rs1use std::collections::BTreeMap;
2
3use bstr::BString;
4
5use crate::{
6 match_group::{match_lhs, Source},
7 RefSpec,
8};
9
10#[derive(Debug)]
12pub struct Error {
13 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#[derive(Debug, PartialEq, Eq)]
41pub enum Issue {
42 Conflict {
46 destination_full_ref_name: BString,
48 sources: Vec<Source>,
50 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#[derive(Debug, PartialEq, Eq, Clone)]
81pub enum Fix {
82 MappingWithPartialDestinationRemoved {
84 name: BString,
86 spec: RefSpec,
88 },
89}
90
91impl match_lhs::Outcome<'_, '_> {
92 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}