diff --git a/CHANGELOG.md b/CHANGELOG.md index 654862eae..61c2ffdf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/doc/modules/ROOT/pages/configuration.adoc b/doc/modules/ROOT/pages/configuration.adoc index 4427bd247..acec9f6ba 100644 --- a/doc/modules/ROOT/pages/configuration.adoc +++ b/doc/modules/ROOT/pages/configuration.adoc @@ -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 diff --git a/projectile.el b/projectile.el index 0584f6629..d42d0f78a 100644 --- a/projectile.el +++ b/projectile.el @@ -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. diff --git a/test/projectile-test.el b/test/projectile-test.el index 3b67734cc..f2be3c65b 100644 --- a/test/projectile-test.el +++ b/test/projectile-test.el @@ -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)