use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::identity::Id;
use crate::index::Collision;
use super::event_id::*;
use super::paths::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileEntry {
pub path: PathBuf,
pub id: Option<Id>,
pub hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
pub id: String,
pub path: PathBuf,
pub created: String,
pub trigger: String,
pub label: Option<String>,
pub parent: Option<String>,
pub files: Vec<FileEntry>,
}
impl Event {
fn manifest(&self) -> BTreeMap<&Path, (&Option<Id>, &str)> {
manifest_of(&self.files)
}
pub fn diff(&self, previous: &Event) -> (usize, usize) {
let (mine, theirs) = (self.manifest(), previous.manifest());
let changed = mine
.iter()
.filter(|(path, (_, hash))| theirs.get(*path).is_none_or(|(_, old)| old != hash))
.count();
let removed = theirs.keys().filter(|p| !mine.contains_key(*p)).count();
(changed, removed)
}
pub fn describe(&self) -> String {
match &self.label {
Some(label) => format!("{} ({label})", display_stamp(&self.id)),
None => display_stamp(&self.id),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Captured {
Written {
id: String,
files: usize,
blobs: usize,
diff: Option<(usize, usize)>,
},
Unchanged {
id: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Scope {
Whole,
Paths(Vec<PathBuf>),
Id(Id),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Disposition {
Create,
Overwrite,
Unchanged,
CaseOnly,
NoBytes,
Remove,
}
impl Disposition {
pub(super) fn rank(self) -> u8 {
match self {
Disposition::Create => 0,
Disposition::Overwrite => 1,
Disposition::CaseOnly => 2,
Disposition::Unchanged => 3,
Disposition::NoBytes => 4,
Disposition::Remove => 5,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RestoreOp {
pub path: PathBuf,
pub disposition: Disposition,
pub hash: Option<String>,
pub id: Option<Id>,
pub rename_from: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Conflict {
pub path: PathBuf,
pub collision: Collision,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RestorePlan {
pub event: String,
pub ops: Vec<RestoreOp>,
pub conflicts: Vec<Conflict>,
}
impl RestorePlan {
pub fn count(&self, disposition: Disposition) -> usize {
self.ops
.iter()
.filter(|op| op.disposition == disposition)
.count()
}
pub fn removals(&self) -> impl Iterator<Item = &Path> {
self.ops
.iter()
.filter(|op| op.disposition == Disposition::Remove)
.map(|op| op.path.as_path())
}
pub fn is_noop(&self) -> bool {
!self.ops.iter().any(|op| {
matches!(
op.disposition,
Disposition::Create
| Disposition::Overwrite
| Disposition::CaseOnly
| Disposition::Remove
)
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Retention {
Keep(usize),
Before(String),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Pruned {
pub events: Vec<String>,
pub blobs: Vec<PathBuf>,
pub bytes: u64,
pub keeping: usize,
}
impl Pruned {
pub fn is_empty(&self) -> bool {
self.events.is_empty() && self.blobs.is_empty()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Forgotten {
pub hashes: Vec<String>,
pub blobs: Vec<PathBuf>,
pub bytes: u64,
pub shared: Vec<String>,
}
impl Forgotten {
pub fn is_empty(&self) -> bool {
self.hashes.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Presence {
At {
path: PathBuf,
id: Option<Id>,
hash: String,
},
Gone,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Version {
pub event: String,
pub created: String,
pub label: Option<String>,
pub state: Presence,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Latest {
pub id: String,
pub created: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Summary {
pub store_exists: bool,
pub events: usize,
pub latest: Option<Latest>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Subject {
Id(Id),
Path(PathBuf),
}
#[cfg(test)]
mod tests {
use super::super::TRIGGER_MANUAL;
use super::super::support::entry;
use super::*;
#[test]
fn diff_counts_changed_and_removed_against_the_previous_manifest() {
let previous = Event {
id: "p".into(),
path: PathBuf::new(),
created: "2026-07-30T00:00:00Z".into(),
trigger: TRIGGER_MANUAL.into(),
label: None,
parent: None,
files: vec![entry("a.md", b"a"), entry("gone.md", b"g")],
};
let current = Event {
files: vec![entry("a.md", b"CHANGED"), entry("new.md", b"n")],
..previous.clone()
};
assert_eq!(current.diff(&previous), (2, 1));
}
}