//! Sync selector: filter which revisions to include in a sync operation. //! //! Used with `--branch` CLI flag to sync only a subset of the DAG. use clawhdf5_onion::format::NO_PARENT; use clawhdf5_onion::writer::OnionFile; use crate::error::SyncOnionError; use crate::packet::OnionLayerPacket; /// Selector controlling which revisions participate in a sync. #[derive(Debug, Clone)] pub enum SyncSelector { /// All revisions on all branches (default). All, /// Only revisions on the named branch. Branch(String), /// Only revisions up to (and including) a specific revision number. UpTo(u64), /// Only the named branch, up to a specific revision. BranchAt(String, u64), } /// Filter a set of packets according to `selector`, keeping only those that /// match. This is applied on the sender side before transmission. pub fn filter_packets( packets: Vec, selector: &SyncSelector, onion: &OnionFile, ) -> Result, SyncOnionError> { match selector { SyncSelector::All => Ok(packets), SyncSelector::Branch(name) => { let branch = onion .branch_by_name(name) .ok_or_else(|| SyncOnionError::BranchNotFound(name.clone()))?; let branch_id = branch.id; Ok(packets .into_iter() .filter(|p| p.branch_id == branch_id) .collect()) } SyncSelector::UpTo(max_rev) => Ok(packets .into_iter() .filter(|p| p.revision <= *max_rev) .collect()), SyncSelector::BranchAt(name, max_rev) => { let branch = onion .branch_by_name(name) .ok_or_else(|| SyncOnionError::BranchNotFound(name.clone()))?; let branch_id = branch.id; Ok(packets .into_iter() .filter(|p| p.branch_id == branch_id && p.revision <= *max_rev) .collect()) } } } /// Compute the remote HEAD for a given selector on the local `OnionFile`. /// /// Returns [`NO_PARENT`] if no revisions match (full sync needed). pub fn remote_head_for_selector(_selector: &SyncSelector, remote_revision_count: u64) -> u64 { // For a simple linear sync, remote HEAD = remote_revision_count - 1 // (or NO_PARENT if empty). if remote_revision_count == 0 { NO_PARENT } else { remote_revision_count - 1 } } // ───────────────────────────────────────────────────────────────────────────── // Tests // ───────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; use crate::differ::diff_revisions; use clawhdf5_onion::format::NO_PARENT; use clawhdf5_onion::writer::OnionFile; use tempfile::NamedTempFile; fn make_onion(n: u8) -> OnionFile { let f = NamedTempFile::new().unwrap(); let h5 = f.path().with_extension("h5"); std::fs::write(&h5, b"\x89HDF\r\n\x1a\n").unwrap(); let mut onion = OnionFile::create(&h5, 4096).unwrap(); for i in 0..n { let mut s = onion.begin_session(None).unwrap(); s.record_page(0, &vec![i; 4096]); onion.commit_session(s, None).unwrap(); } onion } #[test] fn selector_all_passes_everything() { let onion = make_onion(4); let packets = diff_revisions(&onion, NO_PARENT).unwrap(); let filtered = filter_packets(packets, &SyncSelector::All, &onion).unwrap(); assert_eq!(filtered.len(), 4); } #[test] fn selector_up_to() { let onion = make_onion(5); let packets = diff_revisions(&onion, NO_PARENT).unwrap(); let filtered = filter_packets(packets, &SyncSelector::UpTo(2), &onion).unwrap(); assert_eq!(filtered.len(), 3); // revisions 0, 1, 2 } #[test] fn selector_branch_main() { let onion = make_onion(3); let packets = diff_revisions(&onion, NO_PARENT).unwrap(); // All revisions are on main (branch_id = 0) let filtered = filter_packets(packets, &SyncSelector::Branch("main".to_string()), &onion).unwrap(); assert_eq!(filtered.len(), 3); } #[test] fn selector_branch_nonexistent_errors() { let onion = make_onion(2); let packets = diff_revisions(&onion, NO_PARENT).unwrap(); let result = filter_packets( packets, &SyncSelector::Branch("no-such".to_string()), &onion, ); assert!(matches!(result, Err(SyncOnionError::BranchNotFound(_)))); } #[test] fn remote_head_empty() { assert_eq!(remote_head_for_selector(&SyncSelector::All, 0), NO_PARENT); } #[test] fn remote_head_non_empty() { assert_eq!(remote_head_for_selector(&SyncSelector::All, 5), 4); } }