dirsql 0.4.38__tar.gz → 0.4.40__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. {dirsql-0.4.38 → dirsql-0.4.40}/Cargo.lock +2 -2
  2. {dirsql-0.4.38 → dirsql-0.4.40}/PKG-INFO +1 -1
  3. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/Cargo.toml +1 -1
  4. dirsql-0.4.40/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +7 -0
  5. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/testing-conventions.toml +0 -3
  6. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/Cargo.toml +1 -1
  7. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/lib.rs +247 -0
  8. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/scanner.rs +30 -6
  9. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/watcher.rs +73 -0
  10. {dirsql-0.4.38 → dirsql-0.4.40}/Cargo.toml +0 -0
  11. {dirsql-0.4.38 → dirsql-0.4.40}/README.md +0 -0
  12. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/__init__.py +0 -0
  13. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/_async.py +0 -0
  14. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/_dirsql.pyi +0 -0
  15. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/__init__.py +0 -0
  16. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/config_paths_from_argv.py +0 -0
  17. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/discover_plugins/__init__.py +0 -0
  18. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  19. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  20. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  21. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  22. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  23. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/main.py +0 -0
  24. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/cli/resolve_config_extensions.py +0 -0
  25. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/has_bare_name.py +0 -0
  26. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/is_bare_name.py +0 -0
  27. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/load_extension_entries.py +0 -0
  28. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/platform_patterns.py +0 -0
  29. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/py.typed +0 -0
  30. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/resolve_config_extensions.py +0 -0
  31. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/resolve_configs_extension_specs.py +0 -0
  32. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/resolve_entries.py +0 -0
  33. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/resolve_extension.py +0 -0
  34. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/resolve_package.py +0 -0
  35. {dirsql-0.4.38 → dirsql-0.4.40}/dirsql/toml_module.py +0 -0
  36. {dirsql-0.4.38 → dirsql-0.4.40}/docs/.claude/CLAUDE.md +0 -0
  37. {dirsql-0.4.38 → dirsql-0.4.40}/docs/.vitepress/config.ts +0 -0
  38. {dirsql-0.4.38 → dirsql-0.4.40}/docs/.vitepress/theme/index.ts +0 -0
  39. {dirsql-0.4.38 → dirsql-0.4.40}/docs/.vitepress/theme/lang.ts +0 -0
  40. {dirsql-0.4.38 → dirsql-0.4.40}/docs/AGENTS.md +0 -0
  41. {dirsql-0.4.38 → dirsql-0.4.40}/docs/explanation.md +0 -0
  42. {dirsql-0.4.38 → dirsql-0.4.40}/docs/getting-started.md +0 -0
  43. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/columns-from-paths.md +0 -0
  44. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/define-tables.md +0 -0
  45. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/embed.md +0 -0
  46. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/extract-from-contents.md +0 -0
  47. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/load-extension.md +0 -0
  48. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/parse-files-into-columns.md +0 -0
  49. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/persist.md +0 -0
  50. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/query-json.md +0 -0
  51. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/query-without-config.md +0 -0
  52. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/react-to-changes.md +0 -0
  53. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/search-by-meaning.md +0 -0
  54. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/search-indexes.md +0 -0
  55. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/skip-files.md +0 -0
  56. {dirsql-0.4.38 → dirsql-0.4.40}/docs/howto/write-a-plugin.md +0 -0
  57. {dirsql-0.4.38 → dirsql-0.4.40}/docs/index.md +0 -0
  58. {dirsql-0.4.38 → dirsql-0.4.40}/docs/package.json +0 -0
  59. {dirsql-0.4.38 → dirsql-0.4.40}/docs/plugins.md +0 -0
  60. {dirsql-0.4.38 → dirsql-0.4.40}/docs/pnpm-lock.yaml +0 -0
  61. {dirsql-0.4.38 → dirsql-0.4.40}/docs/pnpm-workspace.yaml +0 -0
  62. {dirsql-0.4.38 → dirsql-0.4.40}/docs/reference/cli.md +0 -0
  63. {dirsql-0.4.38 → dirsql-0.4.40}/docs/reference/columns.md +0 -0
  64. {dirsql-0.4.38 → dirsql-0.4.40}/docs/reference/config.md +0 -0
  65. {dirsql-0.4.38 → dirsql-0.4.40}/docs/reference/hooks.md +0 -0
  66. {dirsql-0.4.38 → dirsql-0.4.40}/docs/reference/http-api.md +0 -0
  67. {dirsql-0.4.38 → dirsql-0.4.40}/docs/reference/path-tables.md +0 -0
  68. {dirsql-0.4.38 → dirsql-0.4.40}/docs/reference/sdk.md +0 -0
  69. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/CHANGELOG.md +0 -0
  70. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/MIGRATIONS.md +0 -0
  71. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/README.md +0 -0
  72. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  73. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  74. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  75. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  76. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  77. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  78. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  79. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  80. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  81. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  82. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
  83. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
  84. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
  85. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
  86. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
  87. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
  88. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
  89. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
  90. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
  91. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
  92. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
  93. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
  94. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
  95. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/changelog.d/README.md +0 -0
  96. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/conftest.py +0 -0
  97. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/.claude/CLAUDE.md +0 -0
  98. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/.vitepress/config.ts +0 -0
  99. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  100. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  101. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/AGENTS.md +0 -0
  102. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/explanation.md +0 -0
  103. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/getting-started.md +0 -0
  104. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/columns-from-paths.md +0 -0
  105. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/define-tables.md +0 -0
  106. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/embed.md +0 -0
  107. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/extract-from-contents.md +0 -0
  108. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/load-extension.md +0 -0
  109. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  110. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/persist.md +0 -0
  111. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/query-json.md +0 -0
  112. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/query-without-config.md +0 -0
  113. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/react-to-changes.md +0 -0
  114. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/search-by-meaning.md +0 -0
  115. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/search-indexes.md +0 -0
  116. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/skip-files.md +0 -0
  117. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/howto/write-a-plugin.md +0 -0
  118. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/index.md +0 -0
  119. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/package.json +0 -0
  120. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/plugins.md +0 -0
  121. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/pnpm-lock.yaml +0 -0
  122. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/pnpm-workspace.yaml +0 -0
  123. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/reference/cli.md +0 -0
  124. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/reference/columns.md +0 -0
  125. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/reference/config.md +0 -0
  126. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/reference/hooks.md +0 -0
  127. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/reference/http-api.md +0 -0
  128. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/reference/path-tables.md +0 -0
  129. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/docs/reference/sdk.md +0 -0
  130. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
  131. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
  132. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
  133. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
  134. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
  135. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
  136. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
  137. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
  138. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
  139. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
  140. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
  141. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
  142. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
  143. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
  144. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
  145. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
  146. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
  147. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
  148. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
  149. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  150. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  151. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  152. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  153. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  154. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  155. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  156. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  157. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
  158. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
  159. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/migrations.d/README.md +0 -0
  160. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/src/lib.rs +0 -0
  161. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/tests/__init__.py +0 -0
  162. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/tests/conftest.py +0 -0
  163. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/tests/e2e/__init__.py +0 -0
  164. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  165. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  166. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/tests/integration/__init__.py +0 -0
  167. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/tests/integration/binding/__init__.py +0 -0
  168. {dirsql-0.4.38 → dirsql-0.4.40}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  169. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/CHANGELOG.md +0 -0
  170. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/MIGRATIONS.md +0 -0
  171. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/README.md +0 -0
  172. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/benches/db_bench.rs +0 -0
  173. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/benches/differ_bench.rs +0 -0
  174. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/benches/matcher_bench.rs +0 -0
  175. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/benches/scanner_bench.rs +0 -0
  176. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/explanation.md +0 -0
  177. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/getting-started.md +0 -0
  178. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  179. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/define-tables.md +0 -0
  180. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/embed.md +0 -0
  181. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  182. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/load-extension.md +0 -0
  183. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  184. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/persist.md +0 -0
  185. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/query-json.md +0 -0
  186. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/query-without-config.md +0 -0
  187. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/react-to-changes.md +0 -0
  188. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  189. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/search-indexes.md +0 -0
  190. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/skip-files.md +0 -0
  191. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  192. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/index.md +0 -0
  193. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/plugins.md +0 -0
  194. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/reference/cli.md +0 -0
  195. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/reference/columns.md +0 -0
  196. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/reference/config.md +0 -0
  197. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/reference/hooks.md +0 -0
  198. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/reference/http-api.md +0 -0
  199. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/reference/path-tables.md +0 -0
  200. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/docs/reference/sdk.md +0 -0
  201. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/bin/dirsql.rs +0 -0
  202. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/cli/execute.rs +0 -0
  203. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/cli/init.rs +0 -0
  204. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/cli/mod.rs +0 -0
  205. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/cli/repl.rs +0 -0
  206. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/cli/router.rs +0 -0
  207. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/cli/run.rs +0 -0
  208. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/cli/serialize.rs +0 -0
  209. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/cli/server.rs +0 -0
  210. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/cli/table.rs +0 -0
  211. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/command.rs +0 -0
  212. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/config.rs +0 -0
  213. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/db.rs +0 -0
  214. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/default_config.toml +0 -0
  215. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/differ.rs +0 -0
  216. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/functions.rs +0 -0
  217. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/infer.rs +0 -0
  218. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/matcher.rs +0 -0
  219. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/parsed_cache.rs +0 -0
  220. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/parsed_vtab.rs +0 -0
  221. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/path_table.rs +0 -0
  222. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/persist.rs +0 -0
  223. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/progress.rs +0 -0
  224. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/sql_literal.rs +0 -0
  225. {dirsql-0.4.38 → dirsql-0.4.40}/packages/rust/src/vtab.rs +0 -0
  226. {dirsql-0.4.38 → dirsql-0.4.40}/pyproject.toml +0 -0
@@ -540,7 +540,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
540
540
 
541
541
  [[package]]
542
542
  name = "dirsql"
543
- version = "0.4.38"
543
+ version = "0.4.40"
544
544
  dependencies = [
545
545
  "assert_cmd",
546
546
  "axum",
@@ -583,7 +583,7 @@ dependencies = [
583
583
 
584
584
  [[package]]
585
585
  name = "dirsql-py-ext"
586
- version = "0.4.38"
586
+ version = "0.4.40"
587
587
  dependencies = [
588
588
  "dirsql",
589
589
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.38
3
+ Version: 0.4.40
4
4
  Requires-Dist: tomli>=2 ; python_full_version < '3.11'
5
5
  Requires-Dist: bin-shim>=0.1
6
6
  Summary: Ephemeral SQL index over a local directory
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
4
4
  # pypi/maturin handler can rewrite it via `write-version` before
5
5
  # `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
6
6
  # and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
7
- version = "0.4.38"
7
+ version = "0.4.40"
8
8
  # abi3 note: the `abi3-py310` pyo3 feature (below) builds ONE stable-ABI
9
9
  # `cp310-abi3` wheel per platform that loads on every CPython >= 3.10
10
10
  # (matching `pyproject.toml`'s `requires-python = ">=3.10"`), instead of
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1788980718,
4
+ "exit_code": 0,
5
+ "commit": "f1d452531516e25204fbc4a2bdc3233d036a00f5",
6
+ "branch": "claude/1096-watch-moved-dir"
7
+ }
@@ -9,9 +9,6 @@
9
9
  [e2e]
10
10
  extra_scope = ["packages/rust/src", "packages/python/src"]
11
11
 
12
- [python]
13
- one_function_per_file = { max_lines = 12 }
14
-
15
12
  # Same floor as the root file, which this lane no longer reads.
16
13
  [python.coverage]
17
14
  fail_under = 100
@@ -6,7 +6,7 @@ description = "Ephemeral SQL index over a local directory"
6
6
  # version stays for the binding crates (publish = false). The literal
7
7
  # intentionally lags the published version: putitoutthere rewrites it to
8
8
  # the planned release version at build time and never commits it back.
9
- version = "0.4.38"
9
+ version = "0.4.40"
10
10
  edition.workspace = true
11
11
  # Literal `license` (not `license.workspace = true`) because
12
12
  # putitoutthere's preflight check reads `[package].license` directly
@@ -538,6 +538,18 @@ impl DirSQL {
538
538
  return Vec::new();
539
539
  }
540
540
 
541
+ // A directory that appears whole (`mkdir`, or a populated directory
542
+ // renamed into the tree) arrives as one event naming the directory and
543
+ // none for the files already inside it, so the subtree is walked here.
544
+ // Only on creation: a metadata change on a directory says nothing
545
+ // about its files, and walking on every one would re-run `on_file`
546
+ // across the subtree for a `chmod`.
547
+ if matches!(event, FileEvent::Created(_))
548
+ && self.inner.fs.is_dir(&abs_path).unwrap_or(false)
549
+ {
550
+ return self.index_subtree(&abs_path);
551
+ }
552
+
541
553
  // Fan-out: dispatch the event to every table whose glob matches, and
542
554
  // concatenate the resulting row events. Cross-table event order is
543
555
  // unspecified. An `on_file` failure produces an error event for that
@@ -562,6 +574,27 @@ impl DirSQL {
562
574
  events
563
575
  }
564
576
 
577
+ /// Index every file beneath `dir` as if each had its own create event,
578
+ /// using the scanner's walk so the watch path and the initial scan share
579
+ /// one definition of what counts as a row.
580
+ fn index_subtree(&self, dir: &Path) -> Vec<RowEvent> {
581
+ let base = if dir.starts_with(&self.inner.watch_root) {
582
+ &self.inner.watch_root
583
+ } else {
584
+ &self.inner.root
585
+ };
586
+ let mut events = Vec::new();
587
+ for (path, table) in self.inner.fs.scan_subtree(base, dir, &self.inner.matcher) {
588
+ let rel_path = path
589
+ .strip_prefix(base)
590
+ .unwrap_or(&path)
591
+ .to_string_lossy()
592
+ .to_string();
593
+ events.extend(self.handle_upsert(&table, &path, &rel_path));
594
+ }
595
+ events
596
+ }
597
+
565
598
  fn handle_delete(&self, table: &str, rel_path: &str) -> Vec<RowEvent> {
566
599
  // Snapshot the file's rows before deleting them — the Delete events
567
600
  // carry the old-row payloads. Read and delete under one lock
@@ -1571,6 +1604,15 @@ trait FileSystem: Send + Sync {
1571
1604
  fn stat(&self, path: &Path) -> std::io::Result<FileStat>;
1572
1605
  /// Whether `path` is a regular file. `Err(NotFound)` when it doesn't exist.
1573
1606
  fn is_file(&self, path: &Path) -> std::io::Result<bool>;
1607
+ /// Whether `path` is a directory. `Err(NotFound)` when it doesn't exist.
1608
+ fn is_dir(&self, path: &Path) -> std::io::Result<bool>;
1609
+ /// The scanner's walk over the subtree at `dir`, matched relative to `root`.
1610
+ fn scan_subtree(
1611
+ &self,
1612
+ root: &Path,
1613
+ dir: &Path,
1614
+ matcher: &TableMatcher,
1615
+ ) -> Vec<(PathBuf, String)>;
1574
1616
  /// BLAKE3-hash a file's contents.
1575
1617
  fn hash(&self, path: &Path) -> std::io::Result<[u8; 32]>;
1576
1618
  /// Canonicalize the watch root, falling back to the literal path.
@@ -1588,6 +1630,19 @@ impl FileSystem for RealFs {
1588
1630
  std::fs::metadata(path).map(|m| m.is_file())
1589
1631
  }
1590
1632
 
1633
+ fn is_dir(&self, path: &Path) -> std::io::Result<bool> {
1634
+ std::fs::metadata(path).map(|m| m.is_dir())
1635
+ }
1636
+
1637
+ fn scan_subtree(
1638
+ &self,
1639
+ root: &Path,
1640
+ dir: &Path,
1641
+ matcher: &TableMatcher,
1642
+ ) -> Vec<(PathBuf, String)> {
1643
+ scanner::scan_subtree(root, dir, matcher)
1644
+ }
1645
+
1591
1646
  fn hash(&self, path: &Path) -> std::io::Result<[u8; 32]> {
1592
1647
  hash_file(path)
1593
1648
  }
@@ -2213,6 +2268,8 @@ mod internal_tests {
2213
2268
  hashes: StdHashMap<PathBuf, [u8; 32]>,
2214
2269
  canonical_roots: StdHashMap<PathBuf, String>,
2215
2270
  dirs: StdHashSet<PathBuf>,
2271
+ subtrees: StdHashMap<PathBuf, Vec<(PathBuf, String)>>,
2272
+ subtree_walks: Mutex<Vec<PathBuf>>,
2216
2273
  }
2217
2274
 
2218
2275
  impl FakeFs {
@@ -2228,6 +2285,22 @@ mod internal_tests {
2228
2285
  self
2229
2286
  }
2230
2287
 
2288
+ /// Register a directory together with what the scanner's walk of it
2289
+ /// would return; each file is also stat-able as a regular file.
2290
+ fn with_subtree(mut self, dir: impl Into<PathBuf>, found: Vec<(PathBuf, String)>) -> Self {
2291
+ let dir = dir.into();
2292
+ self.dirs.insert(dir.clone());
2293
+ for (path, _) in &found {
2294
+ self.stats.insert(path.clone(), fake_stat());
2295
+ }
2296
+ self.subtrees.insert(dir, found);
2297
+ self
2298
+ }
2299
+
2300
+ fn subtree_walks(&self) -> Vec<PathBuf> {
2301
+ self.subtree_walks.lock().unwrap().clone()
2302
+ }
2303
+
2231
2304
  fn set_hash(&mut self, path: impl Into<PathBuf>, hash: [u8; 32]) {
2232
2305
  self.hashes.insert(path.into(), hash);
2233
2306
  }
@@ -2263,6 +2336,29 @@ mod internal_tests {
2263
2336
  ))
2264
2337
  }
2265
2338
 
2339
+ fn is_dir(&self, path: &Path) -> std::io::Result<bool> {
2340
+ if self.dirs.contains(path) {
2341
+ return Ok(true);
2342
+ }
2343
+ if self.stats.contains_key(path) {
2344
+ return Ok(false);
2345
+ }
2346
+ Err(std::io::Error::new(
2347
+ std::io::ErrorKind::NotFound,
2348
+ "fake: no such file",
2349
+ ))
2350
+ }
2351
+
2352
+ fn scan_subtree(
2353
+ &self,
2354
+ _root: &Path,
2355
+ dir: &Path,
2356
+ _matcher: &TableMatcher,
2357
+ ) -> Vec<(PathBuf, String)> {
2358
+ self.subtree_walks.lock().unwrap().push(dir.to_path_buf());
2359
+ self.subtrees.get(dir).cloned().unwrap_or_default()
2360
+ }
2361
+
2266
2362
  fn hash(&self, path: &Path) -> std::io::Result<[u8; 32]> {
2267
2363
  self.hashes.get(path).copied().ok_or_else(|| {
2268
2364
  std::io::Error::new(std::io::ErrorKind::NotFound, "fake: no such file")
@@ -2517,6 +2613,144 @@ mod internal_tests {
2517
2613
  assert_eq!(events.len(), 1, "non-ignored path must produce one event");
2518
2614
  }
2519
2615
 
2616
+ fn subtree_db(fake: FakeFs, root: &Path, ignore: Vec<&str>) -> DirSQL {
2617
+ DirSQL::with_ignore_and_fs(
2618
+ root,
2619
+ vec![Table::new(
2620
+ "items",
2621
+ "CREATE TABLE items (name TEXT)",
2622
+ "**/*.txt",
2623
+ |path| {
2624
+ vec![Row::from_iter([(
2625
+ "name".to_string(),
2626
+ Value::Text(path.to_string()),
2627
+ )])]
2628
+ },
2629
+ )],
2630
+ ignore,
2631
+ Arc::new(fake),
2632
+ )
2633
+ .unwrap()
2634
+ }
2635
+
2636
+ /// A created directory is indexed by walking it: every file the scanner
2637
+ /// reports beneath it becomes rows, as if each had its own create event.
2638
+ #[test]
2639
+ fn process_file_event_indexes_the_files_beneath_a_created_directory() {
2640
+ let dir = TempDir::new().unwrap();
2641
+ let moved = dir.path().join("moved");
2642
+ let a = moved.join("a.txt");
2643
+ let deep = moved.join("one").join("deep.txt");
2644
+ let fake = FakeFs::default().with_subtree(
2645
+ moved.clone(),
2646
+ vec![(a.clone(), "items".into()), (deep.clone(), "items".into())],
2647
+ );
2648
+ let db = subtree_db(fake, dir.path(), Vec::new());
2649
+
2650
+ let events = db.process_file_event(FileEvent::Created(moved));
2651
+
2652
+ assert_eq!(events.len(), 2, "one insert per file found: {events:?}");
2653
+ assert!(
2654
+ events.iter().all(|e| matches!(e, RowEvent::Insert { .. })),
2655
+ "got: {events:?}"
2656
+ );
2657
+ let paths: Vec<String> = events
2658
+ .iter()
2659
+ .filter_map(|e| match e {
2660
+ RowEvent::Insert { file_path, .. } => Some(file_path.clone()),
2661
+ _ => None,
2662
+ })
2663
+ .collect();
2664
+ assert_eq!(
2665
+ paths,
2666
+ vec!["moved/a.txt".to_string(), "moved/one/deep.txt".to_string()],
2667
+ "file paths are root-relative"
2668
+ );
2669
+ }
2670
+
2671
+ /// The walk is matched against the root the event was stripped with, so
2672
+ /// the rows it writes carry the same relative paths a create event would.
2673
+ #[test]
2674
+ fn process_file_event_walks_a_created_directory_under_the_watch_root() {
2675
+ let root = PathBuf::from("/ws");
2676
+ let moved = root.join("moved");
2677
+ let fake = FakeFs::default()
2678
+ .with_canonical_root(&root, "/ws")
2679
+ .with_subtree(moved.clone(), vec![(moved.join("a.txt"), "items".into())]);
2680
+ let db = subtree_db(fake, &root, Vec::new());
2681
+
2682
+ let events = db.process_file_event(FileEvent::Created(moved.clone()));
2683
+
2684
+ assert!(
2685
+ matches!(&events[..], [RowEvent::Insert { file_path, .. }] if file_path == "moved/a.txt"),
2686
+ "got: {events:?}"
2687
+ );
2688
+ }
2689
+
2690
+ /// Only a *created* directory is walked. A metadata change on one says
2691
+ /// nothing about its files, and walking on every such event would re-run
2692
+ /// `on_file` across the whole subtree for a `chmod`.
2693
+ #[test]
2694
+ fn process_file_event_does_not_walk_a_modified_directory() {
2695
+ let dir = TempDir::new().unwrap();
2696
+ let sub = dir.path().join("sub");
2697
+ let fake =
2698
+ FakeFs::default().with_subtree(sub.clone(), vec![(sub.join("a.txt"), "items".into())]);
2699
+ let fake = Arc::new(fake);
2700
+ let db = DirSQL::with_ignore_and_fs(
2701
+ dir.path(),
2702
+ vec![Table::new(
2703
+ "items",
2704
+ "CREATE TABLE items (name TEXT)",
2705
+ "**/*.txt",
2706
+ |_| vec![],
2707
+ )],
2708
+ Vec::<String>::new(),
2709
+ fake.clone(),
2710
+ )
2711
+ .unwrap();
2712
+
2713
+ let events = db.process_file_event(FileEvent::Modified(sub.clone()));
2714
+
2715
+ assert!(events.is_empty(), "got: {events:?}");
2716
+ assert!(
2717
+ fake.subtree_walks().is_empty(),
2718
+ "a modified directory must not be walked; walked: {:?}",
2719
+ fake.subtree_walks()
2720
+ );
2721
+ }
2722
+
2723
+ /// An ignored directory is never walked, matching the initial scan.
2724
+ #[test]
2725
+ fn process_file_event_does_not_walk_an_ignored_created_directory() {
2726
+ let dir = TempDir::new().unwrap();
2727
+ let skip = dir.path().join("skip");
2728
+ let fake = Arc::new(
2729
+ FakeFs::default()
2730
+ .with_subtree(skip.clone(), vec![(skip.join("a.txt"), "items".into())]),
2731
+ );
2732
+ let db = DirSQL::with_ignore_and_fs(
2733
+ dir.path(),
2734
+ vec![Table::new(
2735
+ "items",
2736
+ "CREATE TABLE items (name TEXT)",
2737
+ "**/*.txt",
2738
+ |_| vec![],
2739
+ )],
2740
+ vec!["skip"],
2741
+ fake.clone(),
2742
+ )
2743
+ .unwrap();
2744
+
2745
+ let events = db.process_file_event(FileEvent::Created(skip));
2746
+
2747
+ assert!(events.is_empty(), "got: {events:?}");
2748
+ assert!(
2749
+ fake.subtree_walks().is_empty(),
2750
+ "ignored directory was walked"
2751
+ );
2752
+ }
2753
+
2520
2754
  /// Building with a **relative** root canonicalizes `watch_root` to an
2521
2755
  /// absolute path while leaving `root` exactly as the caller supplied it,
2522
2756
  /// so `notify` never sees `.`.
@@ -2773,6 +3007,19 @@ mod internal_tests {
2773
3007
  fs.is_file(&missing).is_err(),
2774
3008
  "is_file of a missing path must error"
2775
3009
  );
3010
+ assert!(
3011
+ fs.is_dir(dir.path()).unwrap(),
3012
+ "a directory must report is_dir=true"
3013
+ );
3014
+ assert!(
3015
+ fs.is_dir(&missing).is_err(),
3016
+ "is_dir of a missing path must error"
3017
+ );
3018
+ let matcher = TableMatcher::new(&[("**/*", "files")], &[]).unwrap();
3019
+ assert!(
3020
+ fs.scan_subtree(dir.path(), dir.path(), &matcher).is_empty(),
3021
+ "an empty directory walks to nothing"
3022
+ );
2776
3023
  // A nonexistent path can't canonicalize, so the literal fallback runs.
2777
3024
  assert_eq!(
2778
3025
  fs.canonical_root(&missing),
@@ -34,11 +34,31 @@ pub fn scan_directory_reporting(
34
34
  root: &Path,
35
35
  matcher: &TableMatcher,
36
36
  on_file: &mut dyn FnMut(u64),
37
+ ) -> Vec<(PathBuf, String)> {
38
+ scan_below(root, root, matcher, on_file)
39
+ }
40
+
41
+ /// [`scan_directory`] restricted to the subtree at `dir`, a directory beneath
42
+ /// `root`. Paths are still matched and ignored relative to `root`, so the
43
+ /// result is exactly the slice of a full scan that falls under `dir`.
44
+ ///
45
+ /// This is how the live watcher indexes a directory that appears whole — a
46
+ /// `mkdir` or a rename into the tree — since the OS reports one event for the
47
+ /// directory and none for the files already inside it.
48
+ pub fn scan_subtree(root: &Path, dir: &Path, matcher: &TableMatcher) -> Vec<(PathBuf, String)> {
49
+ scan_below(root, dir, matcher, &mut |_| {})
50
+ }
51
+
52
+ fn scan_below(
53
+ root: &Path,
54
+ start: &Path,
55
+ matcher: &TableMatcher,
56
+ on_file: &mut dyn FnMut(u64),
37
57
  ) -> Vec<(PathBuf, String)> {
38
58
  let mut results = Vec::new();
39
59
  let mut seen: u64 = 0;
40
60
 
41
- for entry in walk(root, matcher, Path::new(""), false) {
61
+ for entry in walk(root, start, matcher, Path::new(""), false) {
42
62
  let path = entry.path();
43
63
 
44
64
  // Match against relative path so globs like "comments/**/*.jsonl" work
@@ -92,7 +112,7 @@ pub fn scan_glob(
92
112
  ) -> Vec<PathBuf> {
93
113
  let mut results = Vec::new();
94
114
 
95
- for entry in walk(root, ignore, ignore_base, gitignore) {
115
+ for entry in walk(root, root, ignore, ignore_base, gitignore) {
96
116
  if !entry.file_type().is_file() {
97
117
  continue;
98
118
  }
@@ -145,6 +165,7 @@ pub(crate) fn parse_gitignore_arg(arg: &str) -> Result<bool, String> {
145
165
  /// entries a `.gitignore` in force ignores are pruned/skipped too.
146
166
  fn walk<'a>(
147
167
  root: &Path,
168
+ start: &Path,
148
169
  ignore: &'a TableMatcher,
149
170
  ignore_base: &'a Path,
150
171
  gitignore: bool,
@@ -154,13 +175,16 @@ fn walk<'a>(
154
175
  // first. `filter_entry` visits a directory before its children and the
155
176
  // children before the next sibling, so a depth-keyed stack tracks scope.
156
177
  let mut frames: Vec<GitignoreFrame> = Vec::new();
157
- WalkDir::new(&root)
178
+ WalkDir::new(start)
158
179
  .into_iter()
159
180
  .filter_entry(move |entry| {
160
181
  let rel_path = entry.path().strip_prefix(&root).unwrap_or(entry.path());
182
+ // Depth below `root`, not below `start`: the reserved-directory
183
+ // rule is about the tree's top level wherever the walk begins.
184
+ let depth = rel_path.components().count();
161
185
  let is_dir = entry.file_type().is_dir();
162
186
  if !should_descend(
163
- entry.depth(),
187
+ depth,
164
188
  is_dir,
165
189
  entry.file_name(),
166
190
  rel_path,
@@ -172,7 +196,7 @@ fn walk<'a>(
172
196
  if !gitignore {
173
197
  return true;
174
198
  }
175
- while frames.last().is_some_and(|f| f.depth >= entry.depth()) {
199
+ while frames.last().is_some_and(|f| f.depth >= depth) {
176
200
  frames.pop();
177
201
  }
178
202
  if is_gitignored(&frames, entry.path(), rel_path, is_dir, ignore_base) {
@@ -180,7 +204,7 @@ fn walk<'a>(
180
204
  }
181
205
  if is_dir && let Some(matcher) = load_gitignore(entry.path()) {
182
206
  frames.push(GitignoreFrame {
183
- depth: entry.depth(),
207
+ depth,
184
208
  dir: rel_path.to_path_buf(),
185
209
  matcher,
186
210
  });
@@ -79,6 +79,13 @@ fn translate_event(event: &Event) -> Vec<FileEvent> {
79
79
  EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
80
80
  Some(FileEvent::Deleted(path.clone()))
81
81
  }
82
+ // inotify follows a matched `From`/`To` pair with a `Both` naming
83
+ // the same two paths; both sides were already translated above.
84
+ EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => None,
85
+ // A rename INTO the tree puts a path here that did not exist a
86
+ // moment ago — a creation, and for a directory one the watcher must
87
+ // walk, since the OS reports nothing for the files already inside.
88
+ EventKind::Modify(ModifyKind::Name(_)) => Some(FileEvent::Created(path.clone())),
82
89
  EventKind::Modify(_) => Some(FileEvent::Modified(path.clone())),
83
90
  EventKind::Remove(_) => Some(FileEvent::Deleted(path.clone())),
84
91
  _ => None,
@@ -135,6 +142,39 @@ fn rename_from_event(paths: Vec<PathBuf>) -> Event {
135
142
  }
136
143
  }
137
144
 
145
+ #[cfg(test)]
146
+ fn rename_to_event(paths: Vec<PathBuf>) -> Event {
147
+ Event {
148
+ kind: EventKind::Modify(notify::event::ModifyKind::Name(
149
+ notify::event::RenameMode::To,
150
+ )),
151
+ paths,
152
+ attrs: Default::default(),
153
+ }
154
+ }
155
+
156
+ #[cfg(test)]
157
+ fn rename_any_event(paths: Vec<PathBuf>) -> Event {
158
+ Event {
159
+ kind: EventKind::Modify(notify::event::ModifyKind::Name(
160
+ notify::event::RenameMode::Any,
161
+ )),
162
+ paths,
163
+ attrs: Default::default(),
164
+ }
165
+ }
166
+
167
+ #[cfg(test)]
168
+ fn rename_both_event(paths: Vec<PathBuf>) -> Event {
169
+ Event {
170
+ kind: EventKind::Modify(notify::event::ModifyKind::Name(
171
+ notify::event::RenameMode::Both,
172
+ )),
173
+ paths,
174
+ attrs: Default::default(),
175
+ }
176
+ }
177
+
138
178
  #[cfg(test)]
139
179
  fn access_event(paths: Vec<PathBuf>) -> Event {
140
180
  Event {
@@ -196,6 +236,39 @@ mod tests {
196
236
  );
197
237
  }
198
238
 
239
+ #[test]
240
+ fn translate_event_maps_rename_to_as_create() {
241
+ // inotify emits `Modify(Name(To))` when a path is renamed INTO the
242
+ // watched tree; it is a creation so a moved-in directory gets walked.
243
+ let results = translate_event(&rename_to_event(vec![PathBuf::from("/tmp/arrived")]));
244
+ assert_eq!(
245
+ results,
246
+ vec![FileEvent::Created(PathBuf::from("/tmp/arrived"))]
247
+ );
248
+ }
249
+
250
+ #[test]
251
+ fn translate_event_maps_rename_any_as_create() {
252
+ // kqueue / FSEvents cannot tell which side of a rename they saw; a
253
+ // creation of a path that turns out not to exist is a harmless no-op.
254
+ let results = translate_event(&rename_any_event(vec![PathBuf::from("/tmp/either")]));
255
+ assert_eq!(
256
+ results,
257
+ vec![FileEvent::Created(PathBuf::from("/tmp/either"))]
258
+ );
259
+ }
260
+
261
+ #[test]
262
+ fn translate_event_ignores_rename_both() {
263
+ // `Both` restates a `From`/`To` pair inotify already delivered; acting
264
+ // on it would index the destination twice.
265
+ let results = translate_event(&rename_both_event(vec![
266
+ PathBuf::from("/tmp/old"),
267
+ PathBuf::from("/tmp/new"),
268
+ ]));
269
+ assert!(results.is_empty());
270
+ }
271
+
199
272
  #[test]
200
273
  fn translate_event_multiple_paths() {
201
274
  let results = translate_event(&create_event(vec![
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes