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 @@ -4,6 +4,7 @@

### New features

* [#1984](https://github.com/bbatsov/projectile/pull/1984): Add `projectile-vcs-markers` to make VCS detection order configurable.
* [#1964](https://github.com/bbatsov/projectile/issues/1964): Implement `project-name` and `project-buffers` methods for the `project.el` integration, so that code using `project.el` APIs returns correct results for Projectile-managed projects.
* [#1837](https://github.com/bbatsov/projectile/issues/1837): Add `eat` project terminal commands with keybindings `x x` and `x 4 x`.
* Add keybinding `A` (in the projectile command map) and a menu entry for `projectile-add-known-project`.
Expand Down
33 changes: 33 additions & 0 deletions doc/modules/ROOT/pages/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,39 @@ uses the project root directory name, but you can provide your own:
NOTE: If the variable `projectile-project-name` is set (e.g. via
`.dir-locals.el`), it takes precedence over the function.

== VCS detection order

Projectile automatically detects which version control system a project uses
by checking for marker directories (`.git`, `.hg`, `.jj`, etc.). The variable
`projectile-vcs-markers` controls the detection order — earlier entries take
priority:

[source,elisp]
----
;; Default value — git is detected before jj
(setq projectile-vcs-markers
'((".git" . git)
(".hg" . hg)
(".fslckout" . fossil)
("_FOSSIL_" . fossil)
(".bzr" . bzr)
("_darcs" . darcs)
(".pijul" . pijul)
(".sl" . sapling)
(".jj" . jj)
(".svn" . svn)))
----

This is useful for colocated repositories where multiple VCS markers are
present. For example, https://martinvonz.github.io/jj/[Jujutsu] colocated
repos contain both `.jj` and `.git`. To detect them as `jj` instead of `git`:

[source,elisp]
----
;; Prefer jj over git for colocated repos
(push '(".jj" . jj) projectile-vcs-markers)
----

== Dirty projects

`projectile-browse-dirty-projects` (kbd:[s-p V]) shows projects with
Expand Down
49 changes: 27 additions & 22 deletions projectile.el
Original file line number Diff line number Diff line change
Expand Up @@ -3925,38 +3925,43 @@ it acts on the current project.
Expands wildcards using `file-expand-wildcards' before checking."
(file-expand-wildcards (projectile-expand-root file dir)))

(defcustom projectile-vcs-markers
'((".git" . git)
(".hg" . hg)
(".fslckout" . fossil)
("_FOSSIL_" . fossil)
(".bzr" . bzr)
("_darcs" . darcs)
(".pijul" . pijul)
(".svn" . svn)
(".sl" . sapling)
(".jj" . jj))
"Ordered alist mapping VCS marker filenames to VCS symbols.
Earlier entries take priority over later ones. To prefer jj over
git in colocated repos, move the \".jj\" entry before \".git\".
Keys should be kept in sync with `projectile-project-root-files-bottom-up'."
:group 'projectile
:type '(alist :key-type string :value-type symbol)
:package-version '(projectile . "2.10.0"))

(defun projectile-project-vcs (&optional project-root)
"Determine the VCS used by the project if any.
PROJECT-ROOT is the targeted directory. If nil, use
the variable `projectile-project-root'."
(or project-root (setq project-root (projectile-acquire-root)))
(cond
(or
;; first we check for a VCS marker in the project root itself
((projectile-file-exists-p (expand-file-name ".git" project-root)) 'git)
((projectile-file-exists-p (expand-file-name ".hg" project-root)) 'hg)
((projectile-file-exists-p (expand-file-name ".fslckout" project-root)) 'fossil)
((projectile-file-exists-p (expand-file-name "_FOSSIL_" project-root)) 'fossil)
((projectile-file-exists-p (expand-file-name ".bzr" project-root)) 'bzr)
((projectile-file-exists-p (expand-file-name "_darcs" project-root)) 'darcs)
((projectile-file-exists-p (expand-file-name ".pijul" project-root)) 'pijul)
((projectile-file-exists-p (expand-file-name ".svn" project-root)) 'svn)
((projectile-file-exists-p (expand-file-name ".sl" project-root)) 'sapling)
((projectile-file-exists-p (expand-file-name ".jj" project-root)) 'jj)
(cl-loop for (marker . vcs) in projectile-vcs-markers
when (projectile-file-exists-p (expand-file-name marker project-root))
return vcs)
;; then we check if there's a VCS marker up the directory tree
;; that covers the case when a project is part of a multi-project repository
;; in those cases you can still use the VCS to get a list of files for
;; the project in question
((projectile-locate-dominating-file project-root ".git") 'git)
((projectile-locate-dominating-file project-root ".hg") 'hg)
((projectile-locate-dominating-file project-root ".fslckout") 'fossil)
((projectile-locate-dominating-file project-root "_FOSSIL_") 'fossil)
((projectile-locate-dominating-file project-root ".bzr") 'bzr)
((projectile-locate-dominating-file project-root "_darcs") 'darcs)
((projectile-locate-dominating-file project-root ".pijul") 'pijul)
((projectile-locate-dominating-file project-root ".svn") 'svn)
((projectile-locate-dominating-file project-root ".sl") 'sapling)
((projectile-locate-dominating-file project-root ".jj") 'jj)
(t 'none)))
(cl-loop for (marker . vcs) in projectile-vcs-markers
when (projectile-locate-dominating-file project-root marker)
return vcs)
'none))

(defun projectile--test-name-for-impl-name (impl-file-path)
"Determine the name of the test file for IMPL-FILE-PATH.
Expand Down
28 changes: 28 additions & 0 deletions test/projectile-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -2553,6 +2553,34 @@ projectile-process-current-project-buffers-current to have similar behaviour"
(projectile-project-buffer-p (current-buffer) project-root cache)
(expect 'file-truename :to-have-been-called-times 1)))))

(describe "projectile-project-vcs"
(it "returns git for a repo with only .git"
(projectile-test-with-sandbox
(projectile-test-with-files
(".git/")
(expect (projectile-project-vcs default-directory) :to-equal 'git))))
(it "returns jj for a repo with only .jj"
(projectile-test-with-sandbox
(projectile-test-with-files
(".jj/")
(expect (projectile-project-vcs default-directory) :to-equal 'jj))))
(it "defaults to git before jj for colocated repos"
(projectile-test-with-sandbox
(projectile-test-with-files
(".git/" ".jj/")
(expect (projectile-project-vcs default-directory) :to-equal 'git))))
(it "respects custom marker order: jj before git"
(projectile-test-with-sandbox
(projectile-test-with-files
(".git/" ".jj/")
(let ((projectile-vcs-markers '((".jj" . jj) (".git" . git))))
(expect (projectile-project-vcs default-directory) :to-equal 'jj)))))
(it "returns none when no VCS marker is present"
(let* ((tmpdir (make-temp-file "projectile-test-" t))
(default-directory tmpdir))
(unwind-protect
(expect (projectile-project-vcs default-directory) :to-equal 'none)
(delete-directory tmpdir t)))))
;; A bunch of tests that make sure Projectile commands handle
;; gracefully the case of being run outside of a project.
(assert-friendly-error-when-no-project projectile-project-info)
Expand Down