diff --git a/CHANGELOG.md b/CHANGELOG.md index 33df6becf..851f13971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ ### Bugs fixed +- [#2196](https://github.com/bbatsov/projectile/pull/2196): With the default in-memory cache, a project with a `.projectile` no longer throws its cache away and re-indexes on every `projectile-find-file` ([#2193](https://github.com/bbatsov/projectile/issues/2193)). - [#2195](https://github.com/bbatsov/projectile/pull/2195): `projectile-dispatch` can be extended with `transient-insert-suffix`/`transient-append-suffix` as soon as `transient` is loaded, instead of failing with "Not a transient prefix command" until the menu had been opened once ([#2194](https://github.com/bbatsov/projectile/discussions/2194)). - [#2191](https://github.com/bbatsov/projectile/pull/2191): The test suite passes under native compilation with an empty eln cache, as in distro package builds, instead of failing two dirconfig specs with `native-lisp-load-failed` ([#2100](https://github.com/bbatsov/projectile/issues/2100)). - [#2190](https://github.com/bbatsov/projectile/pull/2190): Alien indexing no longer hands git exclude pathspecs to an `fd` whose `projectile-git-fd-args` lacks `--strip-cwd-prefix`, which made it fail with `No valid search paths given` ([#2187](https://github.com/bbatsov/projectile/issues/2187)). diff --git a/projectile.el b/projectile.el index 1c4d2509c..11abeb772 100644 --- a/projectile.el +++ b/projectile.el @@ -2588,10 +2588,28 @@ is tracked in `projectile--current-project'." (run-hook-with-args 'projectile-project-changed-functions project-root previous)))))) +(defun projectile--dirconfig-newer-than-cache-p (project-root) + "Return non-nil if PROJECT-ROOT's dirconfig changed after its files were cached. +When the files are cached in memory, what counts is when that cache was +filled; otherwise it's the persistent cache file, if there is one. With +no cache at all there is nothing to be stale." + (let ((dirconfig (projectile-dirconfig-file project-root)) + (cache-time (gethash project-root projectile-projects-cache-time))) + (if cache-time + ;; The cache time is in whole seconds, so an edit in the second the + ;; cache was filled counts as newer: re-indexing once too often + ;; beats serving a file list that ignores the edit. + (when-let* ((attributes (file-attributes dirconfig))) + (time-less-p cache-time (file-attribute-modification-time attributes))) + (let ((cache-file (projectile-project-cache-file project-root))) + (and (file-exists-p cache-file) + (file-newer-than-file-p dirconfig cache-file)))))) + (defun projectile-maybe-invalidate-cache (force) "Invalidate if FORCE or project's dirconfig newer than cache." - (when (or force (file-newer-than-file-p (projectile-dirconfig-file) - (projectile-project-cache-file))) + (when (or force + (when-let* ((project-root (projectile-project-root))) + (projectile--dirconfig-newer-than-cache-p project-root))) (projectile-invalidate-cache nil))) diff --git a/test/projectile-root-test.el b/test/projectile-root-test.el index 1b1687c15..e80d9a777 100644 --- a/test/projectile-root-test.el +++ b/test/projectile-root-test.el @@ -56,8 +56,54 @@ (expect (projectile-maybe-invalidate-cache t) :to-be-truthy)) (it "should invalidate cache if dirconfig is newer than cache" (spy-on 'projectile-invalidate-cache :and-return-value t) - (spy-on 'file-newer-than-file-p :and-return-value t) - (expect (projectile-maybe-invalidate-cache nil) :to-be-truthy))) + (spy-on 'projectile--dirconfig-newer-than-cache-p :and-return-value t) + (expect (projectile-maybe-invalidate-cache nil) :to-be-truthy)) + (it "leaves an in-memory cache alone when the dirconfig predates it (#2193)" + (projectile-test-with-stub-root "project" (".projectile") + (let ((root (projectile-project-root))) + (set-file-times (projectile-dirconfig-file root) (seconds-to-time 1000)) + (puthash root 2000 projectile-projects-cache-time) + (spy-on 'projectile-invalidate-cache) + (projectile-maybe-invalidate-cache nil) + (expect 'projectile-invalidate-cache :not :to-have-been-called))))) + +(describe "projectile--dirconfig-newer-than-cache-p" + :var (root) + (it "keeps an in-memory cache filled after the dirconfig was written" + (projectile-test-with-stub-root "project" (".projectile") + (setq root (projectile-project-root)) + (set-file-times (projectile-dirconfig-file root) (seconds-to-time 1000)) + (puthash root 2000 projectile-projects-cache-time) + ;; No cache file: the in-memory cache never writes one. + (expect (file-exists-p (projectile-project-cache-file root)) :to-be nil) + (expect (projectile--dirconfig-newer-than-cache-p root) :to-be nil))) + + (it "detects a dirconfig edited after the in-memory cache was filled" + (projectile-test-with-stub-root "project" (".projectile") + (setq root (projectile-project-root)) + (set-file-times (projectile-dirconfig-file root) (seconds-to-time 3000)) + (puthash root 2000 projectile-projects-cache-time) + (expect (projectile--dirconfig-newer-than-cache-p root) :to-be-truthy))) + + (it "falls back to the cache file when nothing is cached in memory" + (projectile-test-with-stub-root "project" (".projectile" ".projectile-cache.eld") + (setq root (projectile-project-root)) + (set-file-times (projectile-project-cache-file root) (seconds-to-time 2000)) + (set-file-times (projectile-dirconfig-file root) (seconds-to-time 3000)) + (expect (projectile--dirconfig-newer-than-cache-p root) :to-be-truthy) + (set-file-times (projectile-dirconfig-file root) (seconds-to-time 1000)) + (expect (projectile--dirconfig-newer-than-cache-p root) :to-be nil))) + + (it "has nothing to invalidate when there is no cache at all" + (projectile-test-with-stub-root "project" (".projectile") + (setq root (projectile-project-root)) + (expect (projectile--dirconfig-newer-than-cache-p root) :to-be nil))) + + (it "has nothing to invalidate when there is no dirconfig" + (projectile-test-with-stub-root "project" ("a.el") + (setq root (projectile-project-root)) + (puthash root 2000 projectile-projects-cache-time) + (expect (projectile--dirconfig-newer-than-cache-p root) :to-be nil)))) (describe "projectile-root-top-down" (it "identifies the root directory of a project by top-down search"