diff options
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | doc/modules/ROOT/pages/configuration.adoc | 74 | ||||
| -rw-r--r-- | projectile.el | 35 | ||||
| -rw-r--r-- | test/projectile-test.el | 73 |
4 files changed, 170 insertions, 15 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e16240..eca17ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Changes +* Document the `hybrid` indexing method in the manual and add a feature matrix showing which Projectile knobs (dirconfig, global ignores/unignores, sort order, default caching) apply under `native`/`hybrid`/`alien`. +* `projectile-dir-files-alien` now accepts an optional `vcs` argument so the dispatcher can thread through the already-resolved VCS instead of recomputing it. Existing single-argument callers are unaffected. +* `projectile-index-directory` (native indexing) now relies on `directory-files-no-dot-files-regexp` to filter out `.` and `..` at the C level instead of walking past them in Elisp. * `projectile-project-root-cache` now keys entries on cons cells (`(FUNC . DIR)` for per-function results, `('none . DIR)` for the overall failure marker) instead of formatted strings. This is internal state, but third-party code that reaches into the cache directly will need to update. * `projectile-parse-dirconfig-file' now returns a `projectile-dirconfig' struct (with `keep', `ignore', `ensure', and `prefixless-ignore' slots) instead of a positional 3-tuple. External callers should use the accessors (`projectile-dirconfig-keep' etc.) rather than `car'/`cadr'/`caddr'. * Soft-deprecate prefix-less ignore entries in `.projectile'. Lines without a `+'/`-'/`!' prefix are still treated as ignore patterns for backward compatibility, but a one-time warning is now shown for each project that uses them. Set `projectile-warn-on-prefixless-dirconfig-lines' to nil to silence. diff --git a/doc/modules/ROOT/pages/configuration.adoc b/doc/modules/ROOT/pages/configuration.adoc index 4427bd2..1492633 100644 --- a/doc/modules/ROOT/pages/configuration.adoc +++ b/doc/modules/ROOT/pages/configuration.adoc @@ -146,6 +146,80 @@ You can also customize the path to the `fd` executable and the arguments passed (setq projectile-git-fd-args "-H -0 -E .git -tf --strip-cwd-prefix -c never") ---- +=== Hybrid indexing + +The `hybrid` method runs the same external command as `alien` and then +post-processes the result with Projectile's filtering rules. In other +words, it's `alien` plus a second pass that applies: + +* `.projectile` (dirconfig) ignore/keep/ensure entries +* `projectile-globally-ignored-files`, + `projectile-globally-ignored-directories`, + `projectile-globally-ignored-file-suffixes`, and + `projectile-global-ignore-file-patterns` +* `projectile-globally-unignored-files` and + `projectile-globally-unignored-directories` +* The configured sort order from `projectile-sort-order` + +This is the right choice when your VCS lists more files than you want +Projectile to surface (e.g. you want to drop `*.elc` even though they're +checked in), or when you rely on `.projectile` to keep/ignore subtrees +inside an otherwise large repository. It's slower than `alien` because +of the extra pass, but it remains substantially faster than `native` for +big projects since the file list itself is still produced by the +external tool. + +=== Indexing method comparison + +Not all Projectile features apply to every indexing method. The table +below summarises which configuration knobs take effect under which mode: + +[cols="3,1,1,1", options="header"] +|=== +| Feature | `native` | `hybrid` | `alien` + +| Project file listing speed +| Slow (Elisp) +| Fast +| Fastest + +| Honors `.projectile` (dirconfig) `+`/`-`/`!` rules +| Yes +| Yes +| No + +| Honors `projectile-globally-ignored-files` / `-directories` / `-file-suffixes` / `projectile-global-ignore-file-patterns` +| Yes +| Yes +| No (rely on the VCS / `fd` / `find` ignores instead) + +| Honors `projectile-globally-unignored-*` +| Yes +| Yes +| No + +| Sorts via `projectile-sort-order` +| Yes +| Yes +| No (returned in the external tool's order) + +| File caching enabled by default +| Yes +| No +| No + +| Works on Windows out of the box +| Yes +| Requires Unix utilities +| Requires Unix utilities +|=== + +NOTE: When `alien` is in use and a non-empty `.projectile` file is +present, Projectile emits a one-time warning so the silent bypass +doesn't catch you off guard. Switch to `hybrid` (or `native`) if you +need those rules applied; set +`projectile-warn-when-dirconfig-is-ignored` to nil to silence. + == Sorting You can choose how Projectile sorts files by customizing `projectile-sort-order`. diff --git a/projectile.el b/projectile.el index bcdf1a3..638642e 100644 --- a/projectile.el +++ b/projectile.el @@ -1599,13 +1599,14 @@ Files are returned as relative paths to DIRECTORY." (gethash directory projectile-projects-cache)))) ;; cache disabled or cache miss (or files-list - (let ((vcs (projectile-project-vcs directory))) - (pcase projectile-indexing-method - ('native (projectile-dir-files-native directory)) - ;; use external tools to get the project files - ('hybrid (projectile-adjust-files directory vcs (projectile-dir-files-alien directory))) - ('alien (projectile-dir-files-alien directory)) - (_ (user-error "Unsupported indexing method `%S'" projectile-indexing-method))))))) + (pcase projectile-indexing-method + ('native (projectile-dir-files-native directory)) + ;; use external tools to get the project files + ('hybrid (let ((vcs (projectile-project-vcs directory))) + (projectile-adjust-files directory vcs + (projectile-dir-files-alien directory vcs)))) + ('alien (projectile-dir-files-alien directory)) + (_ (user-error "Unsupported indexing method `%S'" projectile-indexing-method)))))) ;;; Native Project Indexing ;; @@ -1637,8 +1638,7 @@ IGNORED-DIRECTORIES may optionally be provided." (mapcar (lambda (f) (let ((local-f (file-name-nondirectory (directory-file-name f)))) - (unless (or (and patterns (projectile-ignored-rel-p f directory patterns)) - (member local-f '("." ".."))) + (unless (and patterns (projectile-ignored-rel-p f directory patterns)) (progress-reporter-update progress-reporter) (if (file-directory-p f) (unless (projectile-ignored-directory-p @@ -1651,17 +1651,24 @@ IGNORED-DIRECTORIES may optionally be provided." (list f)))))) ;; Use ignore-errors to skip unreadable directories (e.g. ;; .Spotlight-V100 on macOS) instead of aborting the entire - ;; indexing operation. - (ignore-errors (directory-files directory t)))))) + ;; indexing operation. `directory-files-no-dot-files-regexp' + ;; filters out . and .. at the C level so we don't have to + ;; do it again in the loop. + (ignore-errors + (directory-files directory t directory-files-no-dot-files-regexp)))))) ;;; Alien Project Indexing ;; ;; This corresponds to `projectile-indexing-method' being set to hybrid or alien. ;; The only difference between the two methods is that alien doesn't do ;; any post-processing of the files obtained via the external command. -(defun projectile-dir-files-alien (directory) - "Get the files for DIRECTORY using external tools." - (let ((vcs (projectile-project-vcs directory))) +(defun projectile-dir-files-alien (directory &optional vcs) + "Get the files for DIRECTORY using external tools. +VCS, when supplied, must be the project's VCS as returned by +`projectile-project-vcs'. It is computed from DIRECTORY when +omitted; callers that already resolved the VCS can pass it in to +avoid the redundant work." + (let ((vcs (or vcs (projectile-project-vcs directory)))) (cond ((eq vcs 'git) (let* ((files (nconc (projectile-files-via-ext-command directory (projectile-get-ext-command vcs)) diff --git a/test/projectile-test.el b/test/projectile-test.el index 4d56d4d..a546a8f 100644 --- a/test/projectile-test.el +++ b/test/projectile-test.el @@ -919,7 +919,78 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'. (delete-file "deleted.txt") (let ((files (projectile-dir-files-alien default-directory))) (expect files :to-contain "existing.txt") - (expect files :not :to-contain "deleted.txt"))))))) + (expect files :not :to-contain "deleted.txt")))))) + (it "uses the VCS argument when supplied without recomputing it" + (spy-on 'projectile-project-vcs) + (spy-on 'projectile-files-via-ext-command :and-return-value '("a")) + (spy-on 'projectile-get-sub-projects-files :and-return-value nil) + (spy-on 'projectile-git-deleted-files :and-return-value nil) + (let ((projectile-git-use-fd nil) + (projectile-fd-executable nil)) + (projectile-dir-files-alien "/my/root/" 'git)) + (expect 'projectile-project-vcs :not :to-have-been-called))) + +(describe "hybrid indexing" + (it "applies projectile-globally-ignored-file-suffixes on top of the alien result" + (spy-on 'projectile-files-via-ext-command :and-return-value + '("foo.el" "build/foo.elc" "README")) + (spy-on 'projectile-get-sub-projects-files :and-return-value nil) + (spy-on 'projectile-git-deleted-files :and-return-value nil) + (spy-on 'projectile-project-vcs :and-return-value 'git) + (spy-on 'projectile-project-root :and-return-value "/my/root/") + (spy-on 'file-directory-p :and-call-fake + (lambda (filename) (equal filename "/my/root/"))) + (spy-on 'projectile-parse-dirconfig-file :and-return-value nil) + (let ((projectile-indexing-method 'hybrid) + (projectile-enable-caching nil) + (projectile-globally-ignored-file-suffixes '(".elc")) + (projectile-globally-ignored-files nil) + (projectile-globally-ignored-directories nil) + (projectile-globally-unignored-files nil) + (projectile-globally-unignored-directories nil) + (projectile-git-use-fd nil) + (projectile-fd-executable nil)) + (let ((files (projectile-dir-files "/my/root/"))) + (expect files :to-contain "foo.el") + (expect files :to-contain "README") + (expect files :not :to-contain "build/foo.elc")))) + (it "honors dirconfig ignore patterns on top of the alien result" + (projectile-test-with-sandbox + (projectile-test-with-files + ("project/" + "project/keep.txt" + "project/drop.txt" + "project/.projectile") + (let ((root (file-truename (expand-file-name "project/")))) + (with-temp-file (expand-file-name ".projectile" root) + (insert "-/drop.txt\n")) + (spy-on 'projectile-project-root :and-return-value root) + (spy-on 'projectile-project-vcs :and-return-value 'git) + (spy-on 'projectile-files-via-ext-command :and-return-value + '("keep.txt" "drop.txt")) + (spy-on 'projectile-get-sub-projects-files :and-return-value nil) + (spy-on 'projectile-git-deleted-files :and-return-value nil) + (let ((projectile-indexing-method 'hybrid) + (projectile-enable-caching nil) + (projectile-git-use-fd nil) + (projectile-fd-executable nil)) + (let ((files (projectile-dir-files root))) + (expect files :to-contain "keep.txt") + (expect files :not :to-contain "drop.txt"))))))) + (it "passes the resolved VCS to projectile-dir-files-alien" + (spy-on 'projectile-project-vcs :and-return-value 'git) + (spy-on 'projectile-dir-files-alien :and-return-value '("a")) + (spy-on 'projectile-adjust-files :and-call-fake (lambda (_p _v files) files)) + (spy-on 'file-directory-p :and-call-fake + (lambda (filename) (equal filename "/my/root/"))) + (let ((projectile-indexing-method 'hybrid) + (projectile-enable-caching nil)) + (projectile-dir-files "/my/root/")) + ;; vcs is resolved once by the dispatcher and threaded through; the + ;; redundant call inside projectile-dir-files-alien is gone. + (expect 'projectile-project-vcs :to-have-been-called-times 1) + (expect 'projectile-dir-files-alien + :to-have-been-called-with "/my/root/" 'git))) (describe "projectile-project-dirs" (it "includes intermediate directories that contain only subdirectories" |
