use std::collections::BTreeMap;
use std::collections::btree_map::Entry;
use serde::{Deserialize, Serialize};
use crate::model::names::RelativePath;
use crate::model::verdict::ApplyConflictKind;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum PathApplyOutcome {
CleanExact,
CleanDrifted { line_delta: i64 },
Unassessed { reason: UnassessedReason },
Conflict { kind: ApplyConflictKind },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum UnassessedReason {
BinaryFile,
TargetNotText,
}
impl PathApplyOutcome {
#[must_use]
pub fn severity(&self) -> u8 {
match self {
Self::CleanExact => 0,
Self::CleanDrifted { .. } => 1,
Self::Unassessed { .. } => 2,
Self::Conflict { .. } => 3,
}
}
#[must_use]
pub fn is_conflict(&self) -> bool {
matches!(self, Self::Conflict { .. })
}
#[must_use]
pub fn merge(self, other: Self) -> Self {
match (self, other) {
(Self::CleanDrifted { line_delta: a }, Self::CleanDrifted { line_delta: b }) => {
Self::CleanDrifted {
line_delta: if (b.abs(), b) > (a.abs(), a) { b } else { a },
}
}
(Self::Unassessed { reason: a }, Self::Unassessed { reason: b }) => {
Self::Unassessed { reason: a.max(b) }
}
(Self::Conflict { kind: a }, Self::Conflict { kind: b }) => {
Self::Conflict { kind: a.max(b) }
}
(a, b) => {
if b.severity() > a.severity() {
b
} else {
a
}
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ApplyForecast {
pub paths: BTreeMap<RelativePath, PathApplyOutcome>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ApplySummary {
pub clean: usize,
pub drifted: usize,
pub unassessed: usize,
pub conflicted: usize,
}
impl ApplyForecast {
pub fn record(&mut self, path: RelativePath, outcome: PathApplyOutcome) {
match self.paths.entry(path) {
Entry::Vacant(slot) => {
slot.insert(outcome);
}
Entry::Occupied(mut slot) => {
let merged = slot.get().clone().merge(outcome);
slot.insert(merged);
}
}
}
#[must_use]
pub fn has_conflict(&self) -> bool {
self.paths.values().any(PathApplyOutcome::is_conflict)
}
pub fn conflicts(&self) -> impl Iterator<Item = (&RelativePath, ApplyConflictKind)> {
self.paths.iter().filter_map(|(p, o)| match o {
PathApplyOutcome::Conflict { kind } => Some((p, *kind)),
_ => None,
})
}
#[must_use]
pub fn summary(&self) -> ApplySummary {
let mut s = ApplySummary::default();
for outcome in self.paths.values() {
match outcome {
PathApplyOutcome::CleanExact => s.clean += 1,
PathApplyOutcome::CleanDrifted { .. } => s.drifted += 1,
PathApplyOutcome::Unassessed { .. } => s.unassessed += 1,
PathApplyOutcome::Conflict { .. } => s.conflicted += 1,
}
}
s
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
}