use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use roaring::RoaringBitmap;
use super::overlay::EditKind;
pub struct PendingEdits {
inner: Mutex<PendingState>,
}
struct PendingState {
uncommitted: Vec<super::overlay::FileEdit>,
}
impl Default for PendingEdits {
fn default() -> Self {
Self::new()
}
}
impl PendingEdits {
pub fn new() -> Self {
PendingEdits {
inner: Mutex::new(PendingState {
uncommitted: Vec::new(),
}),
}
}
pub fn notify_change(&self, path: &Path) {
let mut state = self.inner.lock().unwrap_or_else(|p| p.into_inner());
state.uncommitted.push(super::overlay::FileEdit {
path: path.to_path_buf(),
kind: EditKind::Changed,
});
}
pub fn notify_delete(&self, path: &Path) {
let mut state = self.inner.lock().unwrap_or_else(|p| p.into_inner());
state.uncommitted.push(super::overlay::FileEdit {
path: path.to_path_buf(),
kind: EditKind::Deleted,
});
}
pub fn take_for_commit(&self) -> TakeResult {
let mut state = self.inner.lock().unwrap_or_else(|p| p.into_inner());
let mut newly_changed: HashSet<PathBuf> = HashSet::new();
let mut newly_deleted: HashSet<PathBuf> = HashSet::new();
for edit in state.uncommitted.drain(..) {
match edit.kind {
EditKind::Changed => {
newly_deleted.remove(&edit.path);
newly_changed.insert(edit.path);
}
EditKind::Deleted => {
newly_changed.remove(&edit.path);
newly_deleted.insert(edit.path);
}
}
}
TakeResult {
newly_changed,
newly_deleted,
}
}
pub fn reset(&self) {
let mut state = self.inner.lock().unwrap_or_else(|p| p.into_inner());
state.uncommitted.clear();
}
pub fn has_uncommitted(&self) -> bool {
let state = self.inner.lock().unwrap_or_else(|p| p.into_inner());
!state.uncommitted.is_empty()
}
pub fn uncommitted_count(&self) -> usize {
let state = self.inner.lock().unwrap_or_else(|p| p.into_inner());
state.uncommitted.len()
}
}
pub struct TakeResult {
pub newly_changed: HashSet<PathBuf>,
pub newly_deleted: HashSet<PathBuf>,
}
pub fn compute_delete_set(
base_path_doc_ids: &HashMap<PathBuf, Vec<u32>>,
modified_paths: &HashSet<PathBuf>,
deleted_paths: &HashSet<PathBuf>,
prev: &RoaringBitmap,
) -> RoaringBitmap {
let mut delete_set = prev.clone();
for path in modified_paths.iter().chain(deleted_paths.iter()) {
if let Some(doc_ids) = base_path_doc_ids.get(path) {
for &doc_id in doc_ids {
delete_set.insert(doc_id);
}
}
}
delete_set
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reset_clears_uncommitted() {
let pe = PendingEdits::new();
pe.notify_change(Path::new("a.rs"));
pe.notify_change(Path::new("b.rs"));
pe.reset();
assert_eq!(pe.uncommitted_count(), 0, "reset() must clear uncommitted");
}
#[test]
fn take_for_commit_after_reset_returns_empty() {
let pe = PendingEdits::new();
pe.notify_change(Path::new("a.rs"));
pe.reset();
let result = pe.take_for_commit();
assert!(result.newly_changed.is_empty());
assert!(result.newly_deleted.is_empty());
}
#[test]
fn compute_delete_set_is_incremental() {
let mut base: HashMap<PathBuf, Vec<u32>> = HashMap::new();
base.insert(PathBuf::from("a.rs"), vec![1]);
base.insert(PathBuf::from("b.rs"), vec![2]);
base.insert(PathBuf::from("c.rs"), vec![3]);
let prev = RoaringBitmap::new();
let changed: HashSet<PathBuf> = [PathBuf::from("a.rs")].into();
let deleted: HashSet<PathBuf> = HashSet::new();
let ds1 = compute_delete_set(&base, &changed, &deleted, &prev);
assert!(ds1.contains(1));
assert!(!ds1.contains(2));
let changed2: HashSet<PathBuf> = HashSet::new();
let deleted2: HashSet<PathBuf> = [PathBuf::from("b.rs")].into();
let ds2 = compute_delete_set(&base, &changed2, &deleted2, &ds1);
assert!(ds2.contains(1), "a.rs entry must persist");
assert!(ds2.contains(2), "b.rs entry must be added");
assert!(!ds2.contains(3));
}
}