use std::path::Path;
use cosmic_pim_caldav::{Conflict, VdirStore};
use crate::error::Result;
pub fn for_collection(root: &Path, collection_id: &str) -> Result<Vec<Conflict>> {
let Some(meta) = crate::provision::open_collection(root, collection_id) else {
return Ok(Vec::new());
};
Ok(VdirStore::open(meta)?.conflicts().to_vec())
}
pub fn all(root: &Path) -> Vec<(String, Conflict)> {
cosmic_pim_core::store::vdir::collections(root)
.into_iter()
.filter_map(|meta| {
let id = meta.id.clone();
let store = VdirStore::open(meta)
.inspect_err(|why| {
tracing::warn!(collection = id, %why, "could not read a collection's conflicts");
})
.ok()?;
Some(
store
.conflicts()
.iter()
.map(|c| (id.clone(), c.clone()))
.collect::<Vec<_>>(),
)
})
.flatten()
.collect()
}
pub fn take_remote(root: &Path, collection_id: &str, href: &str) -> Result<bool> {
let Some(meta) = crate::provision::open_collection(root, collection_id) else {
return Ok(false);
};
Ok(VdirStore::open(meta)?.resolve_conflict_take_remote(href)?)
}
pub fn keep_local(
root: &Path,
collection_id: &str,
href: &str,
merged: Option<&str>,
) -> Result<bool> {
let Some(meta) = crate::provision::open_collection(root, collection_id) else {
return Ok(false);
};
Ok(VdirStore::open(meta)?.resolve_conflict_keep_local(href, merged)?)
}
#[cfg(test)]
mod tests {
use super::*;
use cosmic_pim_caldav::push::PushQueue;
use cosmic_pim_caldav::{CalDavStore, RemoteEvent};
use cosmic_pim_core::model::Rgb;
use cosmic_pim_core::store::vdir;
const HREF: &str = "/cal/a.ics";
const SERVER_V1: &str = "BEGIN:VCALENDAR\r\nX-V:1\r\nEND:VCALENDAR\r\n";
const LOCAL_EDIT: &str = "BEGIN:VCALENDAR\r\nX-V:mine\r\nEND:VCALENDAR\r\n";
const SERVER_V2: &str = "BEGIN:VCALENDAR\r\nX-V:theirs\r\nEND:VCALENDAR\r\n";
fn conflicted() -> (tempfile::TempDir, String) {
let dir = tempfile::tempdir().unwrap();
let meta = vdir::create_collection(dir.path(), "Personal", Rgb(1, 2, 3)).unwrap();
let id = meta.id.clone();
let mut store = VdirStore::open(meta).unwrap();
store.set_remote("/cal/", false).unwrap();
store
.upsert(&RemoteEvent {
href: HREF.into(),
etag: "\"v1\"".into(),
ics: SERVER_V1.into(),
})
.unwrap();
std::fs::write(store.collection().path.join("a.ics"), LOCAL_EDIT).unwrap();
store.queue_put(HREF).unwrap();
let local = store.unpushed_local(HREF).unwrap().unwrap();
store
.record_conflict(&Conflict {
kind: cosmic_pim_caldav::ConflictKind::BothEdited,
href: HREF.into(),
local,
remote: SERVER_V2.into(),
remote_etag: "\"v2\"".into(),
base: None,
})
.unwrap();
(dir, id)
}
fn file(root: &Path, id: &str) -> String {
let meta = crate::provision::open_collection(root, id).unwrap();
std::fs::read_to_string(meta.path.join("a.ics")).unwrap()
}
#[test]
fn a_recorded_conflict_is_readable_by_collection_and_by_root() {
let (dir, id) = conflicted();
let one = for_collection(dir.path(), &id).unwrap();
assert_eq!(one.len(), 1);
assert_eq!(one[0].local, LOCAL_EDIT);
assert_eq!(one[0].remote, SERVER_V2);
let every = all(dir.path());
assert_eq!(every.len(), 1);
assert_eq!(every[0].0, id);
}
#[test]
fn taking_the_remote_version_needs_no_network() {
let (dir, id) = conflicted();
assert!(take_remote(dir.path(), &id, HREF).unwrap());
assert_eq!(file(dir.path(), &id), SERVER_V2);
assert!(for_collection(dir.path(), &id).unwrap().is_empty());
}
#[test]
fn keeping_the_local_version_leaves_a_live_push_behind() {
let (dir, id) = conflicted();
assert!(keep_local(dir.path(), &id, HREF, None).unwrap());
assert_eq!(file(dir.path(), &id), LOCAL_EDIT);
let meta = crate::provision::open_collection(dir.path(), &id).unwrap();
let pending = VdirStore::open(meta).unwrap().pending().unwrap();
assert_eq!(pending.len(), 1);
assert!(!pending[0].blocked, "the resolution left the push parked");
}
#[test]
fn a_merged_text_replaces_both_sides() {
const MERGED: &str = "BEGIN:VCALENDAR\r\nX-V:both\r\nEND:VCALENDAR\r\n";
let (dir, id) = conflicted();
assert!(keep_local(dir.path(), &id, HREF, Some(MERGED)).unwrap());
assert_eq!(file(dir.path(), &id), MERGED);
}
#[test]
fn an_unknown_collection_is_not_an_error() {
let dir = tempfile::tempdir().unwrap();
assert!(for_collection(dir.path(), "nope").unwrap().is_empty());
assert!(!take_remote(dir.path(), "nope", HREF).unwrap());
assert!(!keep_local(dir.path(), "nope", HREF, None).unwrap());
}
#[test]
fn resolving_twice_reports_that_there_was_nothing_left_to_do() {
let (dir, id) = conflicted();
assert!(take_remote(dir.path(), &id, HREF).unwrap());
assert!(!take_remote(dir.path(), &id, HREF).unwrap());
}
}