aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBozhidar Batsov <bozhidar@toptal.com>2026-04-26 07:13:42 +0100
committerBozhidar Batsov <bozhidar@toptal.com>2026-04-26 07:13:42 +0100
commit67981d6d93b957edf3359d94f6216837427834bc (patch)
treea425158198f405ee313de72377bf698559406f53
parent183023602f1002f39d6e5c82ca44a317d6b517fe (diff)
Skip cache for projectile-root-local (#1211)
projectile-root-local reads the buffer-local projectile-project-root variable rather than inspecting its DIR argument, so caching its result by (FUNC . DIR) was wrong: two buffers visiting the same directory but with different file-local overrides would share the first buffer's cached answer. Skip the cache for this function specifically - it's a single variable read, no FS work to amortize. Adds a regression test that visits two buffers with different file-local roots from the same default-directory and confirms each sees its own override.
-rw-r--r--CHANGELOG.md1
-rw-r--r--projectile.el21
-rw-r--r--test/projectile-test.el21
3 files changed, 34 insertions, 9 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b64613..07e7a67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,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.
* [#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 bcd8302..0530925 100644
--- a/projectile.el
+++ b/projectile.el
@@ -1478,16 +1478,21 @@ If DIR is not supplied it's set to the current directory by default."
(unless (or is-local is-connected)
'none))
;; if the file is local or we're connected to it via TRAMP, run
- ;; through the project root functions until we find a project dir
+ ;; through the project root functions until we find a project dir.
+ ;; `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.
(seq-some
(lambda (func)
- (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))))
+ (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)))))
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 f099d5f..52f24b1 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -1264,7 +1264,26 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
;; since the failure was transitory, there should be nothing cached
(expect (gethash cache-key projectile-project-root-cache) :to-be nil)
;; and projectile-project-root should still return nil
- (expect (projectile-project-root dir) :to-be nil)))))))
+ (expect (projectile-project-root dir) :to-be nil))))))
+
+ (it "honors a buffer-local projectile-project-root after it changes (#1211)"
+ (projectile-test-with-sandbox
+ (projectile-test-with-files
+ ("alpha/.projectile"
+ "beta/.projectile"
+ "host/notes.org")
+ (let* ((dir (expand-file-name "host/"))
+ (alpha-root (file-truename (expand-file-name "alpha/")))
+ (beta-root (file-truename (expand-file-name "beta/")))
+ (default-directory dir)
+ (projectile-project-root-cache (make-hash-table :test 'equal)))
+ ;; First "buffer": file-local override pointing at alpha.
+ (let ((projectile-project-root alpha-root))
+ (expect (projectile-project-root) :to-equal alpha-root))
+ ;; 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)))))))
(describe "projectile-file-exists-p"
(it "returns t if file exists"