emacs-31 5fde9732b4b 2/3: Localize cache invalidation to project-try-vc
Dmitry Gutov <[email protected]> Thu, 2 Jul 2026 02:01:32 -0400 (EDT)
| Newsgroups | gmane.emacs.diffs |
|---|---|
| Message-ID | <[email protected]> |
branch: emacs-31 commit 5fde9732b4be6fc2dbc9ba8ae67dad01572ffcad Author: Dmitry Gutov <[email protected]> Commit: Dmitry Gutov <[email protected]> Localize cache invalidation to project-try-vc With other functions only using the cached values or populating when necessary. * lisp/progmodes/project.el: Update commentary (bug#81317). (project--get-cached): Add explicit parameter TIMEOUT, use it. (project-try-vc): Build its value from 'non-essential' and the values of two timeout variables. And pass them on. (project-try-vc--search, project--vc-merge-submodules-p) (project--value-in-dir): Also add TIMEOUT. (project-files, vc-git-project-list-files) (vc-hg-project-list-files, project-ignores, project-buffers) (project-name, project-uniquify-dirname-transform): Remove the binding of 'non-essential' as now redundant for cache duration. * test/lisp/progmodes/project-tests.el (project-try-vc-uses-cache) (project-try-vc-invalidates-cache) (project-name-<vc>-reuses-cache) (project-name-<vc>-obeys-cache-invalidation): New tests. (project-vc-supports-project-in-different-dir) (project-vc-ignores-in-external-directory): Use 'project--clear-cache' as the more reliable option. --- lisp/progmodes/project.el | 78 +++++++++++++++++------------------ test/lisp/progmodes/project-tests.el | 80 +++++++++++++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 43 deletions(-) diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 557d4260b77..9ecc1f910d2 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -84,11 +84,11 @@ ;; This project type can also be used for non-VCS controlled ;; directories, see the variable `project-vc-extra-root-markers'. ;; -;; Some of the methods on this backend cache their computations for time -;; determined either by variable `project-vc-cache-timeout' or +;; Some of the methods on this backend cache their computations. +;; Cache invalidation is done inside the `project-current' call, with +;; duration determined either by variable `project-vc-cache-timeout' or ;; `project-vc-non-essential-cache-timeout', depending on whether the -;; MAYBE-PROMPT argument to `project-current' is non-nil, or the value -;; of `non-essential' when project methods are called. +;; argument MAYBE-PROMPT is non-nil. ;; ;; Utils: ;; @@ -613,27 +613,21 @@ higher numbers, intended for \"background\" things like `project-mode-line' indicators and `project-uniquify-dirname-transform'. It is used when `non-essential' is non-nil.") -(defun project--get-cached (dir key) +(defun project--get-cached (dir key timeout) (let ((cached (vc-file-getprop dir key)) (current-time (float-time))) (when (and (numberp (cdr cached)) ;; Support package upgrade mid-session. - (let* ((project-vc-cache-timeout - (if non-essential - project-vc-non-essential-cache-timeout - project-vc-cache-timeout)) - (timeout + (let* ((timeout (cond - ((numberp project-vc-cache-timeout) - project-vc-cache-timeout) - ((null project-vc-cache-timeout) - nil) - ((listp project-vc-cache-timeout) + ((numberp timeout) + timeout) + ((listp timeout) (cdr (seq-find (lambda (pair) (and (functionp (car pair)) (funcall (car pair) dir))) - project-vc-cache-timeout))) + timeout))) (t nil)))) (or (null timeout) (< (- current-time (cdr cached)) timeout)))) @@ -658,15 +652,18 @@ It is used when `non-essential' is non-nil.") The value is cached, and depending on whether MAYBE-PROMPT was non-nil in the `project-current' call, the timeout is determined by `project-vc-cache-timeout' or `project-vc-non-essential-cache-timeout'." - (let ((cached (project--get-cached dir 'project-vc))) + (let* ((timeout (if non-essential + project-vc-non-essential-cache-timeout + project-vc-cache-timeout)) + (cached (project--get-cached dir 'project-vc timeout))) (if (eq cached 'none) nil (or cached - (let ((res (project-try-vc--search dir))) + (let ((res (project-try-vc--search dir timeout))) (project--set-cached dir 'project-vc (or res 'none)) res))))) -(defun project-try-vc--search (dir) +(defun project-try-vc--search (dir timeout) (let* ((backend-markers (delete nil @@ -679,7 +676,7 @@ in the `project-current' call, the timeout is determined by (mapconcat (lambda (m) (format "\\(%s\\)" (wildcard-to-regexp m))) (append backend-markers - (project--value-in-dir 'project-vc-extra-root-markers dir)) + (project--value-in-dir 'project-vc-extra-root-markers dir timeout)) "\\|") "\\'")) (locate-dominating-stop-dir-regexp @@ -704,7 +701,7 @@ in the `project-current' call, the timeout is determined by (while (and root (eq backend 'Git) - (project--vc-merge-submodules-p root) + (project--vc-merge-submodules-p root timeout) (project--submodule-p root)) (let* ((parent (file-name-directory (directory-file-name root)))) (setq root (vc-call-backend 'Git 'root parent)))) @@ -715,7 +712,7 @@ in the `project-current' call, the timeout is determined by (let* ((project-vc-extra-root-markers nil) ;; Avoid submodules scan. (enable-dir-local-variables nil) - (parent (project-try-vc--search root))) + (parent (project-try-vc--search root timeout))) (and parent (setq backend (nth 1 parent))))) (setq project (list 'vc backend root)) project))) @@ -764,7 +761,7 @@ in the `project-current' call, the timeout is determined by (cl-defmethod project-files ((project (head vc)) &optional dirs) (mapcan (lambda (dir) - (let ((ignores (project--value-in-dir 'project-vc-ignores dir)) + (let ((ignores (project--value-in-dir 'project-vc-ignores dir nil)) (backend (project-vc--backend project dir))) (if backend (vc-call-backend backend 'project-list-files dir ignores) @@ -792,7 +789,8 @@ in the `project-current' call, the timeout is determined by (vc-git-use-literal-pathspecs nil) (include-untracked (project--value-in-dir 'project-vc-include-untracked - dir)) + dir + nil)) (submodules (project--git-submodules)) (gitver (vc-git--program-version)) (dedup (and (version<= "2.31" gitver) '("--deduplicate"))) @@ -844,7 +842,7 @@ in the `project-current' call, the timeout is determined by (with-output-to-string (apply #'vc-git-command standard-output 0 nil "ls-files" args)) "\0" t)))) - (when (project--vc-merge-submodules-p default-directory) + (when (project--vc-merge-submodules-p default-directory nil) ;; Unfortunately, 'ls-files --recurse-submodules' conflicts with '-o'. (let ((sub-files (mapcar @@ -869,7 +867,8 @@ in the `project-current' call, the timeout is determined by (let* ((default-directory (expand-file-name (file-name-as-directory dir))) (include-untracked (project--value-in-dir 'project-vc-include-untracked - dir)) + dir + nil)) (args (list (concat "-mcard" (and include-untracked "u")) "--no-status" "-0")) @@ -889,10 +888,11 @@ in the `project-current' call, the timeout is determined by files))) files))) -(defun project--vc-merge-submodules-p (dir) +(defun project--vc-merge-submodules-p (dir timeout) (project--value-in-dir 'project-vc-merge-submodules - dir)) + dir + timeout)) (defun project--git-submodules () ;; 'git submodule foreach' is much slower. @@ -909,7 +909,7 @@ in the `project-current' call, the timeout is determined by (cl-defmethod project-ignores ((project (head vc)) dir) (project--vc-ignores dir (project-vc--backend project dir) - (project--value-in-dir 'project-vc-ignores dir))) + (project--value-in-dir 'project-vc-ignores dir nil))) (defun project--vc-ignores (dir backend extra-ignores) (require 'vc) ; Can be removed when we require Emacs 31.1. @@ -966,12 +966,14 @@ DIRS must contain directory names." ;; Sidestep the issue of expanded/abbreviated file names here. (cl-set-difference files dirs :test #'file-in-directory-p)) -(defun project--value-in-dir (var dir) +(defun project--value-in-dir (var dir timeout) + "Look up variable VAR's value in DIR, with cache duration TIMEOUT. +If TIMEOUT is nil, the cache is not invalidated." (alist-get var (and enable-dir-local-variables - (let ((cached (project--get-cached dir 'project-vc-dir-locals))) + (let ((cached (project--get-cached dir 'project-vc-dir-locals timeout))) (if (eq cached 'none) nil (or cached @@ -990,7 +992,7 @@ DIRS must contain directory names." (cl-defmethod project-buffers ((project (head vc))) (let* ((root (expand-file-name (file-name-as-directory (project-root project)))) - (modules (unless (or (project--vc-merge-submodules-p root) + (modules (unless (or (project--vc-merge-submodules-p root nil) (condition-case nil (project--submodule-p root) (file-missing nil))) @@ -1008,12 +1010,8 @@ DIRS must contain directory names." (nreverse bufs))) (cl-defmethod project-name ((project (head vc))) - "Returns the name of this VC-aware type PROJECT. - -The value is cached, and depending on whether `non-essential' is nil, -the timeout is determined by `project-vc-cache-timeout' or -`project-vc-non-essential-cache-timeout'." - (or (project--value-in-dir 'project-vc-name (project-root project)) + "Returns the name of this VC-aware type PROJECT." + (or (project--value-in-dir 'project-vc-name (project-root project) nil) (cl-call-next-method))) @@ -2735,8 +2733,7 @@ slash-separated components from `project-name' will be appended to the buffer's directory name when buffers from two different projects would otherwise have the same name." (if-let* ((proj (project-current nil dirname))) - (let ((root (project-root proj)) - (non-essential t)) + (let ((root (project-root proj))) (expand-file-name (file-name-concat (file-name-directory root) @@ -2782,7 +2779,6 @@ value is `non-remote', show the project name only for local files." ;; 'last-coding-system-used' when reading the project name ;; from .dir-locals.el also enables flyspell-mode (bug#66825). (when-let* ((last-coding-system-used last-coding-system-used) - (non-essential t) (project (project-current)) (project-name (project-name project))) (concat diff --git a/test/lisp/progmodes/project-tests.el b/test/lisp/progmodes/project-tests.el index 29aaaa1e502..f17ea46b411 100644 --- a/test/lisp/progmodes/project-tests.el +++ b/test/lisp/progmodes/project-tests.el @@ -150,7 +150,7 @@ When `project-ignores' includes a name matching project dir." "Check that it picks up dir-locals settings from somewhere else." (skip-unless (eq (vc-responsible-backend default-directory) 'Git)) (let* ((dir (ert-resource-directory)) - (_ (vc-file-clearprops dir)) + (_ (project--clear-cache)) (project-vc-extra-root-markers '(".dir-locals.el")) (project (project-current nil dir))) (should-not (null project)) @@ -181,7 +181,7 @@ When `project-ignores' includes a name matching project dir." "Check that it applies project-vc-ignores when DIR is external to root." (skip-unless (eq (vc-responsible-backend default-directory) 'Git)) (let* ((dir (ert-resource-directory)) - (_ (vc-file-clearprops dir)) + (_ (project--clear-cache)) ;; Do not detect VC backend. (project-vc-backend-markers-alist nil) (project-vc-extra-root-markers '("configure.ac")) @@ -259,4 +259,80 @@ When `project-ignores' includes a name matching project dir." (should (equal (sort (mapcar #'xref-item-summary matches) #'string<) '("((nil . ((project-vc-ignores . (\"etc\")))))" "etc")))))) +(ert-deftest project-try-vc-uses-cache () + "Check that it reuses the value that's already cached." + (skip-unless (eq (vc-responsible-backend default-directory) 'Git)) + ;; Prepare + (let* ((dir (file-name-directory project-tests--this-file)) + (_ (project--clear-cache)) + (project-vc-extra-root-markers '("files-x-tests.*")) + (project (project-current nil dir))) + (should (nth 1 project)) + (should (string-match-p "/test/lisp/\\'" (project-root project))) + (let* ((project-vc-extra-root-markers nil) + (project-vc-non-essential-cache-timeout 0.1) + (project-cached (project-current nil dir))) + (should (equal project project-cached))))) + +(ert-deftest project-try-vc-invalidates-cache () + "Check that it invalidates the cached value that's too old." + "Check that one can add wildcard entries." + (skip-unless (eq (vc-responsible-backend default-directory) 'Git)) + ;; Prepare + (let* ((dir (file-name-directory project-tests--this-file)) + (_ (project--clear-cache)) + (project-vc-extra-root-markers '("files-x-tests.*")) + (project (project-current nil dir))) + (should (nth 1 project)) + (should (string-match-p "/test/lisp/\\'" (project-root project))) + (let* ((project-vc-extra-root-markers nil) + (project-vc-non-essential-cache-timeout 0.0) + (project-fresh (project-current nil dir))) + (should (file-equal-p + (project-root project-fresh) + (expand-file-name "../../../" dir)))))) + +(ert-deftest project-name-<vc>-reuses-cache () + "Check that it reuses the cached value." + (skip-unless (eq (vc-responsible-backend default-directory) 'Git)) + (project--clear-cache) + (ert-with-temp-directory dir + (write-region "((nil . ((project-vc-name . \"barbaz\"))))" + nil + (expand-file-name ".dir-locals.el" dir)) + (write-region "" nil (expand-file-name "project-marker" dir)) + (let* ((project-vc-extra-root-markers '("project-marker")) + (project (project-current nil dir))) + (should (equal (project-name project) + "barbaz")) + (let* ((project-vc-extra-root-markers nil) + (project-vc-non-essential-cache-timeout 0)) + ;; No change, even if the corresponding cache expired. + (should (equal (project-name project) + "barbaz")))))) + +(ert-deftest project-name-<vc>-obeys-cache-invalidation () + "Check that project-name cache obeys invalidation in project-try-vc." + (skip-unless (eq (vc-responsible-backend default-directory) 'Git)) + (project--clear-cache) + (ert-with-temp-directory dir + (write-region "((nil . ((project-vc-name . \"barbaz\"))))" + nil + (expand-file-name ".dir-locals.el" dir)) + (write-region "" nil (expand-file-name "project-marker" dir)) + (let* ((project-vc-extra-root-markers '("project-marker")) + (project (project-current nil dir))) + (should (equal (project-name project) + "barbaz")) + (delete-file (expand-file-name ".dir-locals.el" dir)) + (let* ((project-vc-non-essential-cache-timeout 0) + (project-fresh (project-current nil dir))) + ;; Same project root. + (should (equal project project-fresh)) + ;; But the name is refreshed. + (should (not (equal (project-name project) "barbaz"))) + (should (equal (project-name project) + (file-name-nondirectory + (directory-file-name dir)))))))) + ;;; project-tests.el ends here