diff --git a/notify/src/fsevent.rs b/notify/src/fsevent.rs index c612b2ec..8d066342 100644 --- a/notify/src/fsevent.rs +++ b/notify/src/fsevent.rs @@ -253,6 +253,17 @@ fn translate_flags(flags: &StreamFlags, precise: bool, root_path_exists: bool) - }); } + // `ITEM_CLONED` can be present alongside other flags (including create/modify/remove). + // Preserve any existing `info` (like "root changed"), but annotate otherwise so downstream + // can detect and filter clone-related events. See https://github.com/notify-rs/notify/issues/465. + if flags.contains(StreamFlags::ITEM_CLONED) { + for ev in &mut evs { + if ev.info().is_none() { + ev.attrs.set_info("is: clone"); + } + } + } + if flags.contains(StreamFlags::OWN_EVENT) { for ev in &mut evs { *ev = std::mem::take(ev).set_process_id(std::process::id()); @@ -853,6 +864,70 @@ mod tests { ); } + #[test] + fn translate_flags_ignores_is_file_only_events() { + assert!(translate_flags(&StreamFlags::IS_FILE, true, false).is_empty()); + assert!( + translate_flags( + &(StreamFlags::IS_FILE | StreamFlags::ITEM_CLONED), + true, + false + ) + .is_empty(), + "type-only clone flags should not produce events" + ); + } + + #[test] + fn translate_flags_sets_clone_info_for_file_events() { + let create = translate_flags( + &(StreamFlags::ITEM_CREATED | StreamFlags::IS_FILE | StreamFlags::ITEM_CLONED), + true, + false, + ); + assert_eq!(create.len(), 1); + assert_eq!(create[0].kind, EventKind::Create(CreateKind::File)); + assert_eq!(create[0].info(), Some("is: clone")); + + let modify = translate_flags( + &(StreamFlags::INODE_META_MOD + | StreamFlags::ITEM_MODIFIED + | StreamFlags::IS_FILE + | StreamFlags::ITEM_CLONED), + true, + false, + ); + assert_eq!(modify.len(), 2); + assert!( + modify + .iter() + .any(|e| matches!(e.kind, EventKind::Modify(ModifyKind::Metadata(_)))) + ); + assert!( + modify + .iter() + .any(|e| matches!(e.kind, EventKind::Modify(ModifyKind::Data(_)))) + ); + assert!( + modify.iter().all(|e| e.info() == Some("is: clone")), + "all events should be annotated as clone-related: {modify:?}" + ); + } + + #[test] + fn translate_flags_does_not_override_existing_info() { + let evs = translate_flags( + &(StreamFlags::ROOT_CHANGED + | StreamFlags::ITEM_REMOVED + | StreamFlags::IS_FILE + | StreamFlags::ITEM_CLONED), + true, + false, + ); + assert_eq!(evs.len(), 1); + assert_eq!(evs[0].info(), Some("root changed")); + } + #[test] fn does_not_crash_with_empty_path() { let mut watcher = FsEventWatcher::new(|_| {}, Config::default()).unwrap(); diff --git a/notify/src/lib.rs b/notify/src/lib.rs index 445e81df..a75b7196 100644 --- a/notify/src/lib.rs +++ b/notify/src/lib.rs @@ -52,7 +52,8 @@ //! //! On APFS, `std::fs::copy` may use copy-on-write cloning (`fclonefileat`/`clonefile`). //! This can update inode metadata on the source file, and FSEvents may report a metadata change -//! for the source path (see [issue #259](https://github.com/notify-rs/notify/issues/259)). +//! for the source path (see [issue #259](https://github.com/notify-rs/notify/issues/259) and +//! [issue #465](https://github.com/notify-rs/notify/issues/465)). //! //! Workarounds are to avoid `std::fs::copy` (use `std::io::copy` or `read`/`write` instead), or //! filter out metadata-only events if they're not relevant (e.g. don't include