backhopper_core/model/
apply.rs1use std::collections::BTreeMap;
10use std::collections::btree_map::Entry;
11
12use serde::{Deserialize, Serialize};
13
14use crate::model::names::RelativePath;
15use crate::model::verdict::ApplyConflictKind;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22#[serde(tag = "outcome", rename_all = "snake_case")]
23pub enum PathApplyOutcome {
24 CleanExact,
26 CleanDrifted { line_delta: i64 },
29 Unassessed { reason: UnassessedReason },
31 Conflict { kind: ApplyConflictKind },
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
39#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
40#[serde(rename_all = "snake_case")]
41pub enum UnassessedReason {
42 BinaryFile,
44 TargetNotText,
46}
47
48impl PathApplyOutcome {
49 #[must_use]
52 pub fn severity(&self) -> u8 {
53 match self {
54 Self::CleanExact => 0,
55 Self::CleanDrifted { .. } => 1,
56 Self::Unassessed { .. } => 2,
57 Self::Conflict { .. } => 3,
58 }
59 }
60
61 #[must_use]
62 pub fn is_conflict(&self) -> bool {
63 matches!(self, Self::Conflict { .. })
64 }
65
66 #[must_use]
71 pub fn merge(self, other: Self) -> Self {
72 match (self, other) {
73 (Self::CleanDrifted { line_delta: a }, Self::CleanDrifted { line_delta: b }) => {
74 Self::CleanDrifted {
75 line_delta: if (b.abs(), b) > (a.abs(), a) { b } else { a },
76 }
77 }
78 (Self::Unassessed { reason: a }, Self::Unassessed { reason: b }) => {
79 Self::Unassessed { reason: a.max(b) }
80 }
81 (Self::Conflict { kind: a }, Self::Conflict { kind: b }) => {
82 Self::Conflict { kind: a.max(b) }
83 }
84 (a, b) => {
85 if b.severity() > a.severity() {
86 b
87 } else {
88 a
89 }
90 }
91 }
92 }
93}
94
95#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
99#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
100pub struct ApplyForecast {
101 pub paths: BTreeMap<RelativePath, PathApplyOutcome>,
104}
105
106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
108pub struct ApplySummary {
109 pub clean: usize,
110 pub drifted: usize,
111 pub unassessed: usize,
112 pub conflicted: usize,
113}
114
115impl ApplyForecast {
116 pub fn record(&mut self, path: RelativePath, outcome: PathApplyOutcome) {
119 match self.paths.entry(path) {
120 Entry::Vacant(slot) => {
121 slot.insert(outcome);
122 }
123 Entry::Occupied(mut slot) => {
124 let merged = slot.get().clone().merge(outcome);
125 slot.insert(merged);
126 }
127 }
128 }
129
130 #[must_use]
131 pub fn has_conflict(&self) -> bool {
132 self.paths.values().any(PathApplyOutcome::is_conflict)
133 }
134
135 pub fn conflicts(&self) -> impl Iterator<Item = (&RelativePath, ApplyConflictKind)> {
137 self.paths.iter().filter_map(|(p, o)| match o {
138 PathApplyOutcome::Conflict { kind } => Some((p, *kind)),
139 _ => None,
140 })
141 }
142
143 #[must_use]
144 pub fn summary(&self) -> ApplySummary {
145 let mut s = ApplySummary::default();
146 for outcome in self.paths.values() {
147 match outcome {
148 PathApplyOutcome::CleanExact => s.clean += 1,
149 PathApplyOutcome::CleanDrifted { .. } => s.drifted += 1,
150 PathApplyOutcome::Unassessed { .. } => s.unassessed += 1,
151 PathApplyOutcome::Conflict { .. } => s.conflicted += 1,
152 }
153 }
154 s
155 }
156
157 #[must_use]
158 pub fn is_empty(&self) -> bool {
159 self.paths.is_empty()
160 }
161}