diff --git a/src/backend/KQueue.zig b/src/backend/KQueue.zig index 8f3e717..104154f 100644 --- a/src/backend/KQueue.zig +++ b/src/backend/KQueue.zig @@ -8,7 +8,7 @@ pub const watches_recursively = false; pub const detects_file_modifications = true; pub const emits_close_events = false; pub const emits_rename_for_files = false; -pub const emits_rename_for_dirs = true; +pub const emits_rename_for_dirs = false; pub const emits_subtree_created_on_movein = true; handler: *Handler, @@ -175,7 +175,7 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { // skipped by scan_dir's snapshots.contains() check. self.remove_watch(allocator, dir_path); } else if (ev.fflags & NOTE_RENAME != 0) { - self.handler.change(dir_path, EventType.renamed, .dir) catch |e| { + self.handler.change(dir_path, EventType.deleted, .dir) catch |e| { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); return; }; diff --git a/src/backend/KQueueDir.zig b/src/backend/KQueueDir.zig index ca95d63..fbd08d4 100644 --- a/src/backend/KQueueDir.zig +++ b/src/backend/KQueueDir.zig @@ -8,7 +8,7 @@ pub const watches_recursively = false; pub const detects_file_modifications = false; pub const emits_close_events = false; pub const emits_rename_for_files = false; -pub const emits_rename_for_dirs = true; +pub const emits_rename_for_dirs = false; pub const emits_subtree_created_on_movein = true; pub const WatchEntry = struct { fd: std.posix.fd_t, is_file: bool }; @@ -136,7 +136,7 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { return; }; } else if (ev.fflags & NOTE_RENAME != 0) { - self.handler.change(watch_path, EventType.renamed, .file) catch |e| { + self.handler.change(watch_path, EventType.deleted, .file) catch |e| { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); return; }; @@ -156,7 +156,7 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { // skipped by scan_dir's snapshots.contains() check. self.remove_watch(allocator, watch_path); } else if (ev.fflags & NOTE_RENAME != 0) { - self.handler.change(watch_path, EventType.renamed, .dir) catch |e| { + self.handler.change(watch_path, EventType.deleted, .dir) catch |e| { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); return; }; diff --git a/src/main.zig b/src/main.zig index 9508587..1d8a096 100644 --- a/src/main.zig +++ b/src/main.zig @@ -52,14 +52,12 @@ const CliHandler = struct { .modified => .blue, .closed => .bright_black, .deleted => .red, - .renamed => .magenta, }; const event_label = switch (event_type) { .created => "create ", .modified => "modify ", .closed => "close ", .deleted => "delete ", - .renamed => "rename ", }; self.tty.setColor(&w.interface, color) catch return error.HandlerFailed; w.interface.writeAll(event_label) catch return error.HandlerFailed; @@ -163,8 +161,8 @@ fn usage(out: std.fs.File) !void { \\ create a file or directory was created \\ modify a file was modified \\ close a file was closed after writing (Linux only) - \\ delete a file or directory was deleted - \\ rename a file or directory was renamed + \\ delete a file or directory was deleted or moved out + \\ rename a file or directory was renamed (Linux/Windows only, src -> dst) \\ \\Type column: file, dir, or ? (unknown) \\ diff --git a/src/nightwatch_test.zig b/src/nightwatch_test.zig index 6ad7c11..79e64c9 100644 --- a/src/nightwatch_test.zig +++ b/src/nightwatch_test.zig @@ -400,21 +400,16 @@ fn testRenameFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { try drainEvents(Watcher, &watcher); if (comptime Watcher.emits_rename_for_files) { - // INotify delivers a paired atomic rename callback; FSEvents/Windows - // deliver individual .renamed change events per path. - const has_rename = th.hasRename(src_path, dst_path) or - th.hasChange(src_path, .renamed, .file); - try std.testing.expect(has_rename); + // INotify/Windows: paired atomic rename callback. + try std.testing.expect(th.hasRename(src_path, dst_path)); } else { - // KQueue/KQueueDir: file rename appears as delete + create. + // kqueue/FSEvents: file rename appears as deleted + created. try std.testing.expect(th.hasChange(src_path, .deleted, .file)); try std.testing.expect(th.hasChange(dst_path, .created, .file)); } } fn testRenameDir(comptime Watcher: type, allocator: std.mem.Allocator) !void { - if (comptime !Watcher.emits_rename_for_dirs) return error.SkipZigTest; - const TH = MakeTestHandler(Watcher); const tmp = try makeTempDir(allocator); @@ -440,13 +435,13 @@ fn testRenameDir(comptime Watcher: type, allocator: std.mem.Allocator) !void { try std.fs.renameAbsolute(src_path, dst_path); try drainEvents(Watcher, &watcher); - // All backends with emits_rename_for_dirs=true deliver at least a rename - // event for the source path. INotify delivers a paired rename callback; - // KQueue/KQueueDir deliver change(.renamed, .dir) for the old path only; - // FSEvents/Windows deliver change(.renamed, .dir) for both paths. - const has_rename = th.hasRename(src_path, dst_path) or - th.hasChange(src_path, .renamed, .dir); - try std.testing.expect(has_rename); + if (comptime Watcher.emits_rename_for_dirs) { + // INotify/Windows: paired rename callback. + try std.testing.expect(th.hasRename(src_path, dst_path)); + } else { + // kqueue/FSEvents: old path appears as deleted. + try std.testing.expect(th.hasChange(src_path, .deleted, .dir)); + } } fn testUnwatchedDir(comptime Watcher: type, allocator: std.mem.Allocator) !void { @@ -590,8 +585,7 @@ fn testRenameOrder(comptime Watcher: type, allocator: std.mem.Allocator) !void { const src_idx = th.indexOfAnyPath(src_path) orelse return error.MissingSrcEvent; - const dst_idx = th.indexOfChange(dst_path, .renamed, .file) orelse - th.indexOfChange(dst_path, .created, .file) orelse + const dst_idx = th.indexOfChange(dst_path, .created, .file) orelse return error.MissingDstEvent; try std.testing.expect(src_idx < dst_idx); @@ -835,7 +829,7 @@ test "renaming a file is reported correctly per-platform" { } } -test "renaming a directory emits a rename event" { +test "renaming a directory emits a deleted event for the old path" { inline for (comptime std.enums.values(nw.Variant)) |variant| { try testRenameDir(nw.Create(variant), std.testing.allocator); } diff --git a/src/types.zig b/src/types.zig index cf5695b..0f46724 100644 --- a/src/types.zig +++ b/src/types.zig @@ -12,38 +12,12 @@ pub const EventType = enum { /// Only delivered by INotfiy (Linux) and only if the file was opened /// for writing. closed, - /// A file or directory was deleted. + /// A file or directory was deleted or moved out of a watched tree. + /// + /// Also emitted for the old path when a file or directory is renamed, + /// on all backends except INotify and Windows which can pair the old + /// and new paths into a single atomic `rename` callback instead. deleted, - /// A file or directory was renamed or moved. - /// - /// Delivery varies by backend: - /// - /// - **INotify**: all watches share a single inotify file descriptor, so - /// moves are paired by cookie across all watched roots. Renames between - /// two watched directories - even separate watch roots on the same - /// watcher instance - are delivered as a single atomic `rename` - /// callback. A move out of all watched paths appears as `deleted`; a - /// move in from an unwatched path appears as `created`. - /// - /// - **Windows**: renames within a single watched root are delivered as a - /// single atomic `rename` callback. However, each root uses an - /// independent `ReadDirectoryChangesW` handle with no shared cookie, so - /// a move between two separately watched roots cannot be paired: it - /// appears as `deleted` on the source side and `created` on the - /// destination side. - /// - /// - **kqueue / kqueuedir**: when a watched *directory* is itself - /// renamed, a `renamed` change event is emitted for the old directory - /// path (the new path is not known). Renames of *files inside* a - /// watched directory are detected indirectly via directory-level - /// `NOTE_WRITE` events and appear as `deleted` + `created`. - /// - /// - **FSEvents**: renames are normalized to `deleted` (old path) and - /// `created` (new path) via an existence check at event time. The two - /// sides are not paired. Move-in and move-out therefore appear as - /// `created` and `deleted` respectively, consistent with all other - /// backends. - renamed, }; /// Whether the affected filesystem object is a file, directory, or unknown.