aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--projectile.el15
-rw-r--r--test/projectile-test.el26
3 files changed, 36 insertions, 6 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07e7a67..a906264 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@
### Bugs fixed
* [#1211](https://github.com/bbatsov/projectile/issues/1211): Fix file-local `projectile-project-root` overrides being ignored after the first buffer in a directory was visited. The cache used `(default-directory)` as part of the key but `projectile-root-local` reads a buffer-local variable, so two buffers in the same directory with different overrides got the first buffer's answer. Results from `projectile-root-local` are no longer cached.
+* [#1836](https://github.com/bbatsov/projectile/issues/1836): Memoize per-function `nil` results in the project root cache. Previously, when an early entry in `projectile-project-root-functions` returned nil, the nil was indistinguishable from a missing cache entry, so the function was re-run on every `projectile-project-root` call. With many entries this could cost several full directory walks per call. A `'none` sentinel is now stored for unsuccessful entries.
* [#1508](https://github.com/bbatsov/projectile/issues/1508): Fix dirconfig parser silently treating lines as ignore patterns when the `+`/`-`/`!` prefix or the comment character is preceded by whitespace; leading spaces and tabs are now skipped before prefix dispatch.
* Fix `projectile-files-via-ext-command` executing empty string as shell command for non-git VCS sub-projects.
* Fix `projectile-select-files` crashing on filenames with regexp metacharacters by using `string-search` instead of `string-match`.
diff --git a/projectile.el b/projectile.el
index 0530925..d88819f 100644
--- a/projectile.el
+++ b/projectile.el
@@ -1482,17 +1482,22 @@ If DIR is not supplied it's set to the current directory by default."
;; `projectile-root-local' reads a buffer-local variable rather
;; than inspecting DIR, so its result must not be cached - two
;; buffers in the same directory can legitimately disagree.
+ ;; For other functions, both successes and per-function failures
+ ;; (stored as the 'none sentinel) are memoized, so functions
+ ;; earlier in the list that returned nil aren't re-walked on
+ ;; every call.
(seq-some
(lambda (func)
(if (eq func 'projectile-root-local)
(funcall func dir)
(let* ((cache-key (cons func dir))
(cache-value (gethash cache-key projectile-project-root-cache)))
- (if (and cache-value (file-exists-p cache-value))
- cache-value
- (let ((value (funcall func (file-truename dir))))
- (puthash cache-key value projectile-project-root-cache)
- value)))))
+ (cond
+ ((eq cache-value 'none) nil)
+ ((and cache-value (file-exists-p cache-value)) cache-value)
+ (t (let ((value (funcall func (file-truename dir))))
+ (puthash cache-key (or value 'none) projectile-project-root-cache)
+ value))))))
projectile-project-root-functions)
;; if we get here, we have failed to find a root by all
;; conventional means, and we assume the failure isn't transient
diff --git a/test/projectile-test.el b/test/projectile-test.el
index 52f24b1..88d1da7 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -1283,7 +1283,31 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
;; Second "buffer": same default-directory, different override.
;; The cache must not return the previous buffer's answer.
(let ((projectile-project-root beta-root))
- (expect (projectile-project-root) :to-equal beta-root)))))))
+ (expect (projectile-project-root) :to-equal beta-root))))))
+
+ (it "caches per-function nil results so earlier misses aren't re-walked (#1836)"
+ (projectile-test-with-sandbox
+ (projectile-test-with-files
+ ("projectA/.git/"
+ "projectA/src/")
+ (let* ((calls 0)
+ (always-nil (lambda (_dir) (cl-incf calls) nil))
+ (projectile-project-root-functions
+ (list always-nil 'projectile-root-bottom-up))
+ (projectile-project-root-files-bottom-up '(".git"))
+ (dir (expand-file-name "projectA/src/"))
+ (projectile-project-root-cache (make-hash-table :test 'equal)))
+ ;; first call: always-nil is invoked, then bottom-up wins
+ (expect (file-truename (projectile-project-root dir))
+ :to-equal (file-truename (expand-file-name "projectA/")))
+ (expect calls :to-equal 1)
+ ;; second call: the cached 'none for always-nil should be honoured
+ (expect (file-truename (projectile-project-root dir))
+ :to-equal (file-truename (expand-file-name "projectA/")))
+ (expect calls :to-equal 1)
+ ;; and the cache holds the 'none sentinel for the failing function
+ (expect (gethash (cons always-nil dir) projectile-project-root-cache)
+ :to-be 'none))))))
(describe "projectile-file-exists-p"
(it "returns t if file exists"