Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions notify/src/fsevent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion notify/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading