Skip to content

Fixed #1110 -- Do not report symlink create/delete as a move in DirectorySnapshotDiff#1176

Open
apoorvdarshan wants to merge 1 commit into
gorakhargosh:masterfrom
apoorvdarshan:fix-1110-polling-symlink-move
Open

Fixed #1110 -- Do not report symlink create/delete as a move in DirectorySnapshotDiff#1176
apoorvdarshan wants to merge 1 commit into
gorakhargosh:masterfrom
apoorvdarshan:fix-1110-polling-symlink-move

Conversation

@apoorvdarshan

Copy link
Copy Markdown

Summary

Fixes #1110. PollingObserver reported the creation of a new symlink as a FileMovedEvent instead of a FileCreatedEvent, diverging from the native (fsevents/inotify/winapi) observers.

Root cause

DirectorySnapshot builds its inode index with the default stat=os.stat, which follows symlinks. A new symlink therefore reports the same (st_ino, st_dev) as its target. DirectorySnapshotDiff's move detection matches paths by inode, so when data.trigger -> data is created, the created symlink's inode maps back to the still-present data, and the change is emitted as a move data -> data.trigger (with an empty files_created). The symmetric case — removing a symlink whose target remains — was reported as a move onto the target.

Fix

Treat an inode match as a move only when the counterpart path is genuinely new (for the destination) or genuinely gone (for the source):

  • deleted → move: require the inode's new path to not already exist in the reference snapshot with that same inode;
  • created → move: require the inode's old path to no longer exist in the new snapshot with that same inode.

This leaves real moves — including the move-replace case where the destination pre-existed with a different inode (test_move_replace) — unchanged, while symlinks/hard links that merely share a target's inode degrade to create/delete events, matching the native observers and the module's own "represent movement as created/deleted when inode logic breaks" note.

Verification

  • Added test_create_symlink_to_existing_file and test_delete_symlink_to_existing_file (skipped on Windows, where symlinks need privileges). Both fail on master and pass with this change.
  • tests/test_snapshot_diff.py: 15 passed (including the pre-existing test_move_replace / test_move_internal).
  • tests/test_observers_polling.py and tests/test_observers_api.py: 9 passed.
  • ruff check and ruff format --check clean on the changed files.

Disclosure: this change was prepared with AI assistance; I have reviewed and verified it locally against the test suite.

…ove in DirectorySnapshotDiff

A new symlink shares its target's inode because the default stat function
follows symlinks. The move-detection heuristic matched that shared inode and
reported the creation of a symlink (or the removal of one whose target
remains) as a move of the target file, so PollingObserver emitted a spurious
FileMovedEvent instead of the create/delete event the native observers emit.

Only treat an inode match as a move when the counterpart path is genuinely
new (destination) or gone (source), leaving the move-replace behaviour intact.
Added regression tests for both the create and delete symlink cases.
@apoorvdarshan apoorvdarshan force-pushed the fix-1110-polling-symlink-move branch from 35f6744 to a0fcba1 Compare July 6, 2026 09:13
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.

PollingObserver reports new symlink as "FileMovedEvent".

1 participant