backhopper_core/compat/
routing.rs1use std::collections::BTreeSet;
10use std::mem;
11use std::path::{Path, PathBuf};
12
13use crate::config::Project;
14use crate::model::names::ProjectName;
15use crate::model::verdict::{InapplicableReason, PinVerdict, SeriesVerdict, Verdict};
16
17impl SeriesVerdict {
18 pub fn from_routed(results: Vec<RoutedPinVerdict>) -> Self {
21 Self::from_results(
22 results
23 .into_iter()
24 .map(RoutedPinVerdict::into_inner)
25 .collect(),
26 )
27 }
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
32pub struct PathRouting {
33 pub any_in_scope: bool,
34 pub out_of_scope_owners: BTreeSet<ProjectName>,
35 pub untracked_count: usize,
36}
37
38impl PathRouting {
39 pub fn first_out_of_scope_owner(&self) -> Option<&ProjectName> {
40 self.out_of_scope_owners.first()
41 }
42
43 pub fn has_any_attribution(&self) -> bool {
44 !self.out_of_scope_owners.is_empty() || self.untracked_count > 0
45 }
46}
47
48pub fn project_owns_path(project: &Project, path: &Path) -> bool {
51 project_owns_candidate(project, &path.to_string_lossy())
52}
53
54fn project_owns_candidate(project: &Project, candidate: &str) -> bool {
55 if project.owns_path(Path::new(candidate)) {
56 return true;
57 }
58 if !project.is_self()
59 && let Some(rest) = candidate.strip_prefix("deps/")
60 && let Some(rest) = rest.strip_prefix(project.name.as_str())
61 && rest.starts_with('/')
62 {
63 return true;
64 }
65 project.scan_paths.is_empty() && project.app_roots.is_empty()
66}
67
68pub fn classify_paths_for_pin(
69 touched: &[PathBuf],
70 pin_project: &Project,
71 siblings: &[&Project],
72) -> PathRouting {
73 let mut routing = PathRouting::default();
74 for p in touched {
75 let candidate = p.to_string_lossy();
76 if project_owns_candidate(pin_project, &candidate) {
77 routing.any_in_scope = true;
78 continue;
79 }
80 let owner = siblings
81 .iter()
82 .find(|s| s.name != pin_project.name && project_owns_candidate(s, &candidate))
83 .map(|s| s.name.clone());
84 match owner {
85 Some(name) => {
86 routing.out_of_scope_owners.insert(name);
87 }
88 None => routing.untracked_count += 1,
89 }
90 }
91 routing
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct RoutedPinVerdict(PinVerdict);
99
100impl RoutedPinVerdict {
101 #[must_use]
102 pub fn into_inner(self) -> PinVerdict {
103 self.0
104 }
105}
106
107#[must_use]
112pub fn route_pin_verdict(mut pv: PinVerdict, routing: &PathRouting) -> RoutedPinVerdict {
113 if routing.any_in_scope || !routing.has_any_attribution() {
114 return RoutedPinVerdict(pv);
115 }
116 let inapplicable_reason = |pv: &PinVerdict| {
117 pv.touched.inapplicable_reason().unwrap_or_else(|| {
118 match routing.first_out_of_scope_owner() {
119 Some(name) => InapplicableReason::OutOfScopeFor {
120 project: name.clone(),
121 },
122 None => InapplicableReason::Untracked,
123 }
124 })
125 };
126 let verdict = mem::replace(&mut pv.verdict, Verdict::Compatible);
127 pv.verdict = match verdict {
128 Verdict::Inapplicable { .. } => verdict,
129 Verdict::Compatible => {
130 if pv.tracked_refs > 0 {
131 Verdict::Compatible
132 } else {
133 Verdict::Inapplicable {
134 reason: inapplicable_reason(&pv),
135 }
136 }
137 }
138 Verdict::RequiresAdaptation { reasons } | Verdict::Incompatible { reasons } => {
139 let surviving: Vec<_> = reasons
140 .into_iter()
141 .filter(|r| !r.is_path_scoped())
142 .collect();
143 if surviving.is_empty() && pv.tracked_refs == 0 {
144 Verdict::Inapplicable {
145 reason: inapplicable_reason(&pv),
146 }
147 } else {
148 Verdict::from_reasons(surviving)
149 }
150 }
151 };
152 RoutedPinVerdict(pv)
153}