Skip to content

feat: Catch renames during watcher - #767

Open
dmtrKovalenko wants to merge 1 commit into
mainfrom
feat/rename-tolerance
Open

feat: Catch renames during watcher#767
dmtrKovalenko wants to merge 1 commit into
mainfrom
feat/rename-tolerance

Conversation

@dmtrKovalenko

@dmtrKovalenko dmtrKovalenko commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • File watchers now report renames as a single renamed event with both destination and original paths.
    • Rename events are available across Node.js, Bun, Python, C, and shared APIs.
    • File history and frecency data now transfer when files are renamed.
    • Moves across the indexed tree boundary continue to report as creation or removal events.
  • Documentation

    • Updated watcher documentation to describe rename events and the 50 ms debounce window.
  • Bug Fixes

    • Improved rename detection while avoiding false matches for unrelated file changes.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The watcher now detects filesystem renames, emits destination and source paths, transfers frecency history, and exposes rename data through C, Node, Bun, and Python APIs. Tests cover rename pairing, subscriptions, frecency behavior, FFI access, and language integrations.

Changes

Rename-aware file watching

Layer / File(s) Summary
Rename event and frecency contracts
crates/fff-core/src/watcher/watch.rs, crates/fff-core/src/dbs/frecency.rs, crates/fff-core/tests/rename_frecency_test.rs
Adds the Renamed event kind, optional source paths, and transactional frecency history copying.
Rename detection and event propagation
crates/fff-core/src/watcher/background_watcher.rs, crates/fff-core/src/watcher/rescan_tests.rs
Pairs explicit and unique metadata-matched renames, suppresses duplicate removals, transfers frecency history, and tests fallback cases.
C ABI and language bindings
crates/fff-c/*, packages/fff-node/*, packages/fff-bun/*, crates/fff-python/*, packages/shared/fff-api.ts
Adds the source-path accessor and maps rename events to public from or from_path fields. Integration tests and documentation cover in-tree and boundary-crossing moves.

Tooling cleanup

Layer / File(s) Summary
Lint selection and minor cleanup
Makefile, packages/pi-fff/src/index.ts, tests/test_cd_during_post_scan.lua
Uses a PATH-first luacheck selection and applies two behavior-preserving local edits.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding rename handling to the watcher.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rename-tolerance

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/fff-core/src/watcher/background_watcher.rs (1)

688-721: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

One LMDB write transaction per renamed file.

copy_history opens and commits its own write transaction for every entry in renames. A directory move produces one explicit rename pair per file, so a large move turns into thousands of sequential commits on the watcher thread. This blocks event processing for the whole batch.

Consider a batched API, for example copy_history_many(&[(from, to)]), that writes all pairs in one transaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fff-core/src/watcher/background_watcher.rs` around lines 688 - 721,
Update the frecency rename-history handling around shared_frecency and
copy_history to use a batched operation such as copy_history_many for all
renames in one write transaction. Preserve the existing copied-history
semantics, collect only successfully carried destination paths, and retain the
current error and picker-update behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fff-core/src/watcher/rescan_tests.rs`:
- Around line 585-598: Update the file-opening step in the rescan test before
set_modified to use OpenOptions with write access for src/keep.rs instead of
File::open. Preserve the existing timestamp assignment and watcher feed
behavior.

In `@crates/fff-core/src/watcher/watch.rs`:
- Around line 511-519: Ensure rename matching in the subscription loop preserves
destination-path ignore rules: after computing the destination result from
sub.filter_mask(&path_refs, &mut scratch), only merge source-path matches that
remain allowed by that destination result, so an ignored destination produces no
event. Update the matched-mask logic around filter_mask and retain normal source
matching for non-ignored destinations.

---

Nitpick comments:
In `@crates/fff-core/src/watcher/background_watcher.rs`:
- Around line 688-721: Update the frecency rename-history handling around
shared_frecency and copy_history to use a batched operation such as
copy_history_many for all renames in one write transaction. Preserve the
existing copied-history semantics, collect only successfully carried destination
paths, and retain the current error and picker-update behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f344e43-c7d4-4f93-bb43-7157bc362662

📥 Commits

Reviewing files that changed from the base of the PR and between dd87489 and 05619f8.

📒 Files selected for processing (27)
  • Makefile
  • crates/fff-c/include/fff.h
  • crates/fff-c/src/watch.rs
  • crates/fff-c/tests/smoke.c
  • crates/fff-core/src/dbs/frecency.rs
  • crates/fff-core/src/shared.rs
  • crates/fff-core/src/watcher/background_watcher.rs
  • crates/fff-core/src/watcher/rescan_tests.rs
  • crates/fff-core/src/watcher/watch.rs
  • crates/fff-core/tests/rename_frecency_test.rs
  • crates/fff-core/tests/watch_subscription_test.rs
  • crates/fff-python/src/finder.rs
  • crates/fff-python/src/types.rs
  • packages/fff-bun/README.md
  • packages/fff-bun/src/fff-api.ts
  • packages/fff-bun/src/ffi.ts
  • packages/fff-bun/test/watch.test.ts
  • packages/fff-node/README.md
  • packages/fff-node/src/fff-api.ts
  • packages/fff-node/src/ffi.ts
  • packages/fff-node/test/watch.mjs
  • packages/fff-python/README.md
  • packages/fff-python/src/fff/__init__.pyi
  • packages/fff-python/tests/test_watch.py
  • packages/pi-fff/src/index.ts
  • packages/shared/fff-api.ts
  • tests/test_cd_during_post_scan.lua

Comment on lines +585 to +598
let mtime = std::fs::metadata(f.path("src/gone.rs"))
.unwrap()
.modified()
.unwrap();
std::fs::remove_file(f.path("src/gone.rs")).unwrap();
std::fs::File::open(f.path("src/keep.rs"))
.unwrap()
.set_modified(mtime)
.unwrap();

f.feed([
remove_file(f.path("src/gone.rs")),
modify(f.path("src/keep.rs")),
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether this test module is gated to unix and whether Windows CI runs it.
rg -nP --type=rust -B5 'mod rescan_tests' crates/fff-core/src
fd -e yml -e yaml . .github/workflows --exec rg -nl 'windows'

Repository: dmtrKovalenko/fff

Length of output: 653


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- test module and nearby test setup ---'
cat -n crates/fff-core/src/watcher/mod.rs | sed -n '1,30p'
cat -n crates/fff-core/src/watcher/rescan_tests.rs | sed -n '560,610p'

printf '%s\n' '--- Windows and rescan_stats workflow entries ---'
for f in .github/workflows/*.yml .github/workflows/*.yaml; do
  [ -f "$f" ] || continue
  printf '\n### %s\n' "$f"
  rg -n -C 5 'windows|rescan_stats|cargo test|cargo nextest|target' "$f" || true
done

printf '%s\n' '--- all rescan_stats references ---'
rg -n -C 3 'rescan_stats|rescan_tests' .

Repository: dmtrKovalenko/fff

Length of output: 39893


🌐 Web query:

Rust std fs File set_modified Windows FILE_WRITE_ATTRIBUTES File::open read access source

💡 Result:

To set the modification time of a file on Windows using Rust, you must open the file with the appropriate access rights. Using standard read-only access (via File::open) is insufficient because changing file timestamps requires the FILE_WRITE_ATTRIBUTES access right [1]. When using Rust's std::fs::File::set_times (or the nightly set_modified method), the underlying Windows API function is SetFileTime, which requires the file handle to have been opened with at least FILE_WRITE_ATTRIBUTES access [2][3][1]. To achieve this in Rust, you can use OpenOptions combined with the OpenOptionsExt trait available on Windows [4][5]. This allows you to manually specify the dwDesiredAccess flag for the underlying CreateFileW call [4][6]. Example implementation: use std::fs::OpenOptions; use std::os::windows::fs::OpenOptionsExt; use std::fs::FileTimes; use std::time::SystemTime; fn main -> std::io::Result<> { // Define the required access: FILE_WRITE_ATTRIBUTES (0x100) const FILE_WRITE_ATTRIBUTES: u32 = 0x100; let file = OpenOptions::new.access_mode(FILE_WRITE_ATTRIBUTES).open("your_file.txt")?; let new_time = SystemTime::now; let times = FileTimes::new.set_modified(new_time); // This will now succeed because the file handle has FILE_WRITE_ATTRIBUTES file.set_times(times)?; Ok() } In summary: 1. File::open typically uses read access (GENERIC_READ), which does not include FILE_WRITE_ATTRIBUTES [7][1]. 2. You must use OpenOptions::new.access_mode(FILE_WRITE_ATTRIBUTES).open(...) to obtain the necessary handle permissions [4][1]. 3. Once opened with this permission, you can successfully invoke set_times or set_modified [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- Cargo feature and test configuration ---'
rg -n -C 4 'rescan-stats|rescan_stats|default|features' Cargo.toml crates/fff-core/Cargo.toml

printf '%s\n' '--- Fixture file creation and permissions ---'
rg -n -C 5 'struct Fixture|impl Fixture|fn write\(|OpenOptions|set_permissions|readonly' crates/fff-core/src/watcher/rescan_tests.rs crates/fff-core/src

Repository: dmtrKovalenko/fff

Length of output: 15714


Open keep.rs with write access before calling set_modified. Windows CI runs this test, and File::open lacks the access required to change timestamps. Use OpenOptions::new().write(true).open(...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fff-core/src/watcher/rescan_tests.rs` around lines 585 - 598, Update
the file-opening step in the rescan test before set_modified to use OpenOptions
with write access for src/keep.rs instead of File::open. Preserve the existing
timestamp assignment and watcher feed behavior.

Comment on lines 511 to +519
let path_refs: Vec<&str> = paths.iter().map(String::as_str).collect();
let from_refs: Vec<&str> = from_paths.iter().map(String::as_str).collect();
let mut scratch = Vec::new();
let mut deliveries = Vec::with_capacity(state.subs.len());
for sub in &state.subs {
let matched = sub.filter_mask(&path_refs, &mut scratch);
let mut matched = sub.filter_mask(&path_refs, &mut scratch);
if has_renames {
matched |= sub.filter_mask(&from_refs, &mut scratch);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Rename matching bypasses the per-subscription ignore list.

filter_mask applies the ignore rules inside each call. When you OR the two masks, a bit cleared by the destination ignore can be set again by the source path. Example: subscription pattern **, ignore *.log, rename a.txtb.log. The first mask is 0, the second mask sets the bit, and the subscriber receives an event whose path is the ignored b.log. The documented contract says ignored paths produce no events.

Decide the intended rule and enforce it. If the destination must stay ignorable, mask the rename hits with the destination result.

🔧 One possible fix
             for sub in &state.subs {
                 let mut matched = sub.filter_mask(&path_refs, &mut scratch);
                 if has_renames {
-                    matched |= sub.filter_mask(&from_refs, &mut scratch);
+                    // A rename is announced only if the destination itself is
+                    // not excluded by this subscription.
+                    let not_ignored = sub.filter_mask(&path_refs, &mut scratch)
+                        | sub.ignore_free_mask(&path_refs, &mut scratch);
+                    matched |= sub.filter_mask(&from_refs, &mut scratch) & not_ignored;
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let path_refs: Vec<&str> = paths.iter().map(String::as_str).collect();
let from_refs: Vec<&str> = from_paths.iter().map(String::as_str).collect();
let mut scratch = Vec::new();
let mut deliveries = Vec::with_capacity(state.subs.len());
for sub in &state.subs {
let matched = sub.filter_mask(&path_refs, &mut scratch);
let mut matched = sub.filter_mask(&path_refs, &mut scratch);
if has_renames {
matched |= sub.filter_mask(&from_refs, &mut scratch);
}
for sub in &state.subs {
let mut matched = sub.filter_mask(&path_refs, &mut scratch);
if has_renames {
// A rename is announced only if the destination itself is
// not excluded by this subscription.
let not_ignored = sub.filter_mask(&path_refs, &mut scratch)
| sub.ignore_free_mask(&path_refs, &mut scratch);
matched |= sub.filter_mask(&from_refs, &mut scratch) & not_ignored;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fff-core/src/watcher/watch.rs` around lines 511 - 519, Ensure rename
matching in the subscription loop preserves destination-path ignore rules: after
computing the destination result from sub.filter_mask(&path_refs, &mut scratch),
only merge source-path matches that remain allowed by that destination result,
so an ignored destination produces no event. Update the matched-mask logic
around filter_mask and retain normal source matching for non-ignored
destinations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant