branch: elpa/projectile
commit 5c5dd5cc7316e85b4d63e66372bf78f3bd428084
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
Add per-language manifests to projectile-project-root-files-bottom-up
The default value contained only VC markers (.git, .hg, etc.), so in
any polyglot or monorepo layout where the language subproject sits in
a subdirectory of an enclosing .git root, projectile-root-bottom-up
returned the outer .git root. This was surprising for tools that ask
projectile-project-root which subproject a buffer belongs to (e.g.
CIDER trying to find the deps.edn for cider-jack-in).
The registered project types already populate projectile-project-root-files
(the top-down list) via projectile-register-project-type's :project-file
keyword, but the top-down function is never reached when bottom-up
short-circuits on .git.
Add a curated set of per-language manifests (deps.edn, Cargo.toml,
pyproject.toml, pom.xml, package.json, etc.) directly to the bottom-up
list. The bottom-most match wins, so the nearer subproject root beats
the outer .git. Markers that legitimately appear at multiple levels
of one project (Makefile, application.yml, requirements.txt, manage.py,
gradlew) are intentionally not in the list and stay top-down only, as
do wildcard markers (?*.csproj, ?*.sln, ?*.nimble) which the
file-exists-p path in projectile-root-bottom-up does not expand.
.projectile precedence is unchanged: projectile-root-marked sits ahead
of projectile-root-bottom-up in projectile-project-root-functions and
runs against a fixed (list projectile-dirconfig-file), independent of
projectile-project-root-files-bottom-up.
Update the projects.adoc explanation of the two lists to match, and
add a regression test using the default list to confirm a
monorepo/.git + clj/deps.edn layout resolves to clj/ rather than the
outer root.
---
CHANGELOG.md | 1 +
doc/modules/ROOT/pages/projects.adoc | 34 +++++++++++++---------
projectile.el | 55 +++++++++++++++++++++++++++++++++++-
test/projectile-test.el | 16 ++++++++++-
4 files changed, 90 insertions(+), 16 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac8a16ed1b..44d20b9178 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Changes
+* `projectile-project-root-files-bottom-up` now ships with the common
per-language project manifests (`deps.edn`, `project.clj`, `Cargo.toml`,
`pyproject.toml`, `pom.xml`, `package.json`, etc.) alongside the VC markers. In
a polyglot or monorepo layout where the language manifest sits in a
subdirectory of an enclosing `.git` root, the closer subproject now wins.
Markers that legitimately appear at multiple levels of a single project
(Makefile, application.yml, requirements.txt, manage.py [...]
* `projectile-get-immediate-sub-projects` skips the `git submodule foreach`
shell-out for git projects with no `.gitmodules` file anywhere up the parent
chain. Hot path for monorepos that index the project root often.
* `projectile-discover-projects-in-directory` now uses
`directory-files-no-dot-files-regexp` to skip `.` and `..` at the C level
instead of doing the post-filter in Elisp - matches the indexing walker.
* Document the anchored vs `*`-prefixed semantics of
`projectile-globally-ignored-directories`, the `find` fallback's lack of common
directory exclusions when `fd` isn't available, and how `fd`/`git ls-files`
handle deleted-but-unstaged files differently.
diff --git a/doc/modules/ROOT/pages/projects.adoc
b/doc/modules/ROOT/pages/projects.adoc
index d14092ba41..e617d89213 100644
--- a/doc/modules/ROOT/pages/projects.adoc
+++ b/doc/modules/ROOT/pages/projects.adoc
@@ -754,19 +754,25 @@ precedence with respect to project detection. Let's
examine the defaults:
discovery logic.
* `projectile-root-bottom-up` will start looking for a project marker
- file/folder(e.g. `.projectile`, `.hg`, `.git`) from the current folder
+ file/folder (e.g. `.projectile`, `.hg`, `.git`, `deps.edn`,
+ `Cargo.toml`, `pyproject.toml`) from the current folder
(a.k.a. `default-directory` in Emacs lingo) up the directory tree. It will
- return the first match it discovers. The assumption is pretty simple - the
- root marker appear only once, at the root folder of a project. If a root
- marker appears in several nested folders (e.g. you've got nested git
projects),
- the bottom-most (closest to the current dir) match has precedence. You can
- customize the root markers recognized by this function via
- `projectile-project-root-files-bottom-up`
+ return the first match it discovers. The default list (configurable via
+ `projectile-project-root-files-bottom-up`) contains VCS markers and the
+ language-specific project manifests registered via
+ `projectile-register-project-type`. The bottom-most (closest to the
+ current dir) match wins, so in a polyglot or monorepo layout where a
+ `.git` sits at the top and a language manifest lives in a subdirectory,
+ the language subdirectory wins.
* `projectile-root-top-down` is similar, but it will return the top-most
- (farthest from the current directory) match. It's configurable via
- `projectile-project-root-files` and all project manifest markers like
- `pom.xml`, `Gemfile`, `project.clj`, etc go there.
+ (farthest from the current directory) match. It is configurable via
+ `projectile-project-root-files`, which is also populated automatically
+ from `projectile-register-project-type`. It is only consulted when no
+ bottom-up match was found, so today it primarily covers projects whose
+ marker is a wildcard pattern (e.g. `?*.csproj`, `?*.sln`), since
+ `projectile-root-bottom-up` matches via `file-exists-p` and does not
+ expand globs.
* `projectile-root-top-down-recurring` will look for project markers that can
appear at every level of a project (e.g. `Makefile` or `.svn`) and will
return
@@ -774,10 +780,10 @@ precedence with respect to project detection. Let's
examine the defaults:
NOTE: `projectile-root-top-down` only matches *regular files* — directories
with names listed in `projectile-project-root-files` are skipped. This is
-why the default list contains files like `configure.ac` or `TAGS` rather than
-VCS directories. `projectile-root-bottom-up` matches both files and
-directories, so VCS markers like `.git` (a directory in normal repos, a file
-in worktrees and submodules) belong on the bottom-up list.
+why the default seed list contains files like `configure.ac` or `TAGS`
+rather than VCS directories. `projectile-root-bottom-up` matches both
+files and directories, so VCS markers like `.git` (a directory in normal
+repos, a file in worktrees and submodules) belong on the bottom-up list.
The default ordering should work well for most people, but depending on the
structure of your project you might want to tweak it.
diff --git a/projectile.el b/projectile.el
index b6008f4ac8..d37f43444b 100644
--- a/projectile.el
+++ b/projectile.el
@@ -353,9 +353,62 @@ See `projectile-register-project-type'."
".pijul" ; Pijul VCS root dir
".sl" ; Sapling VCS root dir
".jj" ; Jujutsu VCS root dir
+ ;; Per-language project manifests. Including these alongside VC
+ ;; markers means that in a polyglot or monorepo layout (e.g. a
+ ;; `.git' at the repo root with a `deps.edn' in a language
+ ;; subdirectory), the inner project marker wins for tools that ask
+ ;; `projectile-project-root' which subproject the buffer belongs to.
+ ;; Markers that legitimately appear at multiple levels of a single
+ ;; project (Makefile, application.yml, requirements.txt, manage.py,
+ ;; gradlew) are intentionally left in the top-down list instead.
+ "Cargo.toml" ; Rust
+ "Package.swift" ; Swift
+ "build.zig.zon" ; Zig
+ "dune-project" ; OCaml
+ "Project.toml" ; Julia
+ "elm.json" ; Elm
+ "pubspec.yaml" ; Dart
+ "info.rkt" ; Racket
+ "stack.yaml" ; Haskell
+ "DESCRIPTION" ; R
+ "Eldev" ; Emacs (Eldev)
+ "Eask" ; Emacs (Eask)
+ "Cask" ; Emacs (Cask)
+ "shard.yml" ; Crystal
+ "Gemfile" ; Ruby
+ "deps.edn" ; Clojure CLI
+ "build.boot" ; Boot
+ "project.clj" ; Leiningen
+ "build.mill" ; Mill
+ "build.sc" ; Mill (legacy)
+ "build.sbt" ; sbt
+ "build.gradle" ; Gradle
+ "pom.xml" ; Maven
+ "pyproject.toml" ; Python (PEP 518)
+ "poetry.lock" ; Python (Poetry)
+ "Pipfile" ; Python (pipenv)
+ "tox.ini" ; Python (tox)
+ "setup.py" ; Python (setuptools)
+ "angular.json" ; Angular
+ "package.json" ; npm/yarn/pnpm
+ "Gruntfile.js" ; Grunt
+ "gulpfile.js" ; Gulp
+ "Taskfile.yml" ; go-task
+ "composer.json" ; PHP
+ "rebar.config" ; Erlang (rebar3)
+ "mix.exs" ; Elixir
+ "CMakeLists.txt" ; CMake
+ "WORKSPACE" ; Bazel
+ "flake.nix" ; Nix flake
+ "default.nix" ; Nix
+ "meson.build" ; Meson
+ "SConstruct" ; SCons
+ "xmake.lua" ; xmake
+ "debian/control" ; Debian source package
)
"A list of files considered to mark the root of a project.
-The bottommost (parentmost) match has precedence."
+The bottommost (parentmost) match has precedence -- so a project
+manifest in a subdirectory wins over an enclosing VC root."
:group 'projectile
:type '(repeat string))
diff --git a/test/projectile-test.el b/test/projectile-test.el
index 12e7894f53..139f856407 100644
--- a/test/projectile-test.el
+++ b/test/projectile-test.el
@@ -1632,7 +1632,21 @@ by `projectile-files-via-ext-command')."
"worktree/src/file.txt")
(expect (projectile-root-bottom-up "worktree/src/" '(".git"))
:to-equal
- (expand-file-name "worktree/"))))))
+ (expand-file-name "worktree/")))))
+ (it "prefers a nearer project manifest over an outer VC root via the default
list"
+ ;; Regression: in a polyglot/monorepo layout (`.git' at the top, a
+ ;; language manifest deeper down),
`projectile-project-root-files-bottom-up'
+ ;; ships the common manifests, so the closer subproject root wins.
+ (projectile-test-with-sandbox
+ (projectile-test-with-files
+ ("monorepo/.git/"
+ "monorepo/clj/deps.edn"
+ "monorepo/clj/src/foo.clj")
+ (expect (projectile-root-bottom-up
+ "monorepo/clj/src/"
+ projectile-project-root-files-bottom-up)
+ :to-equal
+ (expand-file-name "monorepo/clj/"))))))
(describe "projectile-root-marked"
(it "finds the closest dirconfig-file bottom-up"