Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Changes

* [#1771](https://github.com/bbatsov/projectile/issues/1771): Apply `.projectile` ignore/ensure patterns to file listing (not just grep/ripgrep). Patterns prefixed with `-` are now respected by `projectile-remove-ignored`. Also filter out path traversal patterns (containing `..`) in `projectile-normalise-patterns` for security.
* [#1958](https://github.com/bbatsov/projectile/issues/1958): Exclude `.projectile-cache.eld` from search results (ripgrep/ag/grep) by default.
* [#1957](https://github.com/bbatsov/projectile/pull/1957): Add `:caller` information to calls to `ivy-read` (used by packages like `ivy-rich`).
* [#1947](https://github.com/bbatsov/projectile/issues/1947): `projectile-project-name` should be marked as safe.
Expand Down
48 changes: 42 additions & 6 deletions projectile.el
Original file line number Diff line number Diff line change
Expand Up @@ -1664,14 +1664,41 @@ Only text sent to standard output is taken into account."
"First remove ignored files from FILES, then add back unignored files."
(projectile-add-unignored project vcs (projectile-remove-ignored files)))

(defun projectile--pattern-to-regex (pattern)
"Convert a glob PATTERN to a regular expression using `wildcard-to-regexp'."
(wildcard-to-regexp pattern))

(defun projectile--file-matches-patterns-p (file patterns)
"Check if FILE matches any of glob PATTERNS.
For patterns without directory separators (e.g., \"*.text\"), matches against
the file's basename to apply the pattern recursively across all directories.
For patterns with directory separators (e.g., \"src/*.c\"), matches against
the full relative path."
(cl-some
(lambda (pattern)
(let ((regex (projectile--pattern-to-regex pattern)))
(if (string-match-p "/" pattern)
;; Pattern contains directory separator: match against full path
(string-match-p regex file)
;; Pattern without directory: match against basename only
;; This makes "*.text" match "subdir/file.text"
(string-match-p regex (file-name-nondirectory file)))))
patterns))

(defun projectile-remove-ignored (files)
"Remove ignored files and folders from FILES.

If ignored directory prefixed with '*', then ignore all
directories/subdirectories with matching filename,
otherwise operates relative to project root."
(let ((ignored-files (projectile-ignored-files-rel))
(ignored-dirs (projectile-ignored-directories-rel)))
otherwise operates relative to project root.

Also applies glob patterns from the project's .projectile file,
respecting both ignore patterns (prefixed with -) and ensure
patterns (prefixed with !)."
(let* ((ignored-files (projectile-ignored-files-rel))
(ignored-dirs (projectile-ignored-directories-rel))
(patterns-to-ignore (projectile-patterns-to-ignore))
(patterns-to-ensure (projectile-patterns-to-ensure)))
(cl-remove-if
(lambda (file)
(or (cl-some
Expand All @@ -1694,7 +1721,11 @@ otherwise operates relative to project root."
(cl-some
(lambda (suf)
(string-suffix-p suf file t))
projectile-globally-ignored-file-suffixes)))
projectile-globally-ignored-file-suffixes)
;; Check glob patterns from .projectile file
(and (projectile--file-matches-patterns-p file patterns-to-ignore)
;; Respect ensure patterns (unignore)
(not (projectile--file-matches-patterns-p file patterns-to-ensure)))))
files)))

(defun projectile-keep-ignored-files (project vcs files)
Expand Down Expand Up @@ -1912,8 +1943,13 @@ projectile project root."
paths))))

(defun projectile-normalise-patterns (patterns)
"Remove paths from PATTERNS."
(cl-remove-if (lambda (pat) (string-prefix-p "/" pat)) patterns))
"Remove paths from PATTERNS and filter out unsafe patterns.
Patterns starting with `/' are removed as they are paths, not patterns.
Patterns containing `..' are removed to prevent path traversal attacks."
(cl-remove-if (lambda (pat)
(or (string-prefix-p "/" pat)
(string-match-p "\\.\\." pat)))
patterns))

(defun projectile-make-relative-to-root (files)
"Make FILES relative to the project root."
Expand Down
62 changes: 61 additions & 1 deletion test/projectile-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -450,10 +450,70 @@ Just delegates OPERATION and ARGS for all operations except for`shell-command`'.
(spy-on 'projectile-project-name :and-return-value "project")
(spy-on 'projectile-ignored-files-rel)
(spy-on 'projectile-ignored-directories-rel)
(spy-on 'projectile-patterns-to-ignore :and-return-value nil)
(spy-on 'projectile-patterns-to-ensure :and-return-value nil)
(let* ((file-names '("foo.c" "foo.o" "foo.so" "foo.o.gz" "foo.tar.gz" "foo.tar.GZ"))
(files (mapcar 'projectile-expand-root file-names)))
(let ((projectile-globally-ignored-file-suffixes '(".o" ".so" ".tar.gz")))
(expect (projectile-remove-ignored files) :to-equal (mapcar 'projectile-expand-root '("foo.c" "foo.o.gz")))))))
(expect (projectile-remove-ignored files) :to-equal (mapcar 'projectile-expand-root '("foo.c" "foo.o.gz"))))))
(it "removes files matching glob patterns from .projectile"
(spy-on 'projectile-project-root :and-return-value "/path/to/project/")
(spy-on 'projectile-project-name :and-return-value "project")
(spy-on 'projectile-ignored-files-rel :and-return-value nil)
(spy-on 'projectile-ignored-directories-rel :and-return-value nil)
(spy-on 'projectile-patterns-to-ignore :and-return-value '("*.text" "*.log"))
(spy-on 'projectile-patterns-to-ensure :and-return-value nil)
(let ((projectile-globally-ignored-file-suffixes nil)
(files '("src/main.c" "src/utils.c" "mysubdir/x.txt" "mysubdir/y.text" "debug.log")))
(expect (projectile-remove-ignored files) :to-equal '("src/main.c" "src/utils.c" "mysubdir/x.txt"))))
(it "respects ensure patterns when removing glob-matched files"
(spy-on 'projectile-project-root :and-return-value "/path/to/project/")
(spy-on 'projectile-project-name :and-return-value "project")
(spy-on 'projectile-ignored-files-rel :and-return-value nil)
(spy-on 'projectile-ignored-directories-rel :and-return-value nil)
(spy-on 'projectile-patterns-to-ignore :and-return-value '("*.log"))
(spy-on 'projectile-patterns-to-ensure :and-return-value '("important.log"))
(let ((projectile-globally-ignored-file-suffixes nil)
(files '("src/main.c" "debug.log" "important.log")))
(expect (projectile-remove-ignored files) :to-equal '("src/main.c" "important.log"))))
(it "avoids false positives with simple patterns (no wildcards)"
(spy-on 'projectile-project-root :and-return-value "/path/to/project/")
(spy-on 'projectile-project-name :and-return-value "project")
(spy-on 'projectile-ignored-files-rel :and-return-value nil)
(spy-on 'projectile-ignored-directories-rel :and-return-value nil)
(spy-on 'projectile-patterns-to-ignore :and-return-value '("build"))
(spy-on 'projectile-patterns-to-ensure :and-return-value nil)
(let ((projectile-globally-ignored-file-suffixes nil)
(files '("src/main.c" "mybuild/file.txt" "build" "dir/build")))
(expect (projectile-remove-ignored files) :to-equal '("src/main.c" "mybuild/file.txt"))))
(it "matches patterns with directory separators against full path"
(spy-on 'projectile-project-root :and-return-value "/path/to/project/")
(spy-on 'projectile-project-name :and-return-value "project")
(spy-on 'projectile-ignored-files-rel :and-return-value nil)
(spy-on 'projectile-ignored-directories-rel :and-return-value nil)
(spy-on 'projectile-patterns-to-ignore :and-return-value '("src/*.log"))
(spy-on 'projectile-patterns-to-ensure :and-return-value nil)
(let ((projectile-globally-ignored-file-suffixes nil)
(files '("src/debug.log" "lib/debug.log" "debug.log")))
(expect (projectile-remove-ignored files) :to-equal '("lib/debug.log" "debug.log"))))
(it "handles empty ignore patterns gracefully"
(spy-on 'projectile-project-root :and-return-value "/path/to/project/")
(spy-on 'projectile-project-name :and-return-value "project")
(spy-on 'projectile-ignored-files-rel :and-return-value nil)
(spy-on 'projectile-ignored-directories-rel :and-return-value nil)
(spy-on 'projectile-patterns-to-ignore :and-return-value nil)
(spy-on 'projectile-patterns-to-ensure :and-return-value nil)
(let ((projectile-globally-ignored-file-suffixes nil)
(files '("a.log" "b.txt")))
(expect (projectile-remove-ignored files) :to-equal '("a.log" "b.txt")))))

(describe "projectile-normalise-patterns"
(it "removes patterns starting with /"
(expect (projectile-normalise-patterns '("*.log" "/absolute/path" "*.txt"))
:to-equal '("*.log" "*.txt")))
(it "removes patterns containing .. to prevent path traversal"
(expect (projectile-normalise-patterns '("*.log" "../parent/*.txt" "valid/*.c" "foo/../bar"))
:to-equal '("*.log" "valid/*.c"))))

(describe "projectile-add-unignored"
(it "requires explicitly unignoring files inside ignored paths"
Expand Down