Subject: bug#68579: [PATCH v2] Support a local repo as URL in treesit-language-source-alist

From: Konstantin Kharlamov <[email-masked]>
Date: Sat, 20 Jan 2024 14:56:29 +0300

Sometimes people may need to bisect to find specific revision in a
grammar repo. In this case they'd want to point the URL to the local
repo to avoid cloning it on every rebuild. So add support for full
path in treesit-language-source-alist.

* lisp/treesit.el (treesit--install-language-grammar-1): Test if URL
starts with / meaning that the URL is a local path.  Then if it is,
avoid cloning the repo and removing the path on success.
(treesit--git-clone-repo): Factor out the code for cloning to a separate
function.
(treesit--git-checkout-branch): A helper to checkout the revision for
cases where we didn't clone the repo but want it to point the
revision.
---

v2:

1. Regarding discussion about (file-name-absolute-p) and (file-accessible-directory-p),
I figured that if we only test whether the file is accessible, we can lift the
restriction of it being an absolute directory. So now I test for accessibility.

2. I also removed my comment in variable initialization because it's obsolete and no
longer useful.

3. I also couldn't squash the

   (if url-is-path
       (when revision
         (treesit--git-checkout-branch url revision))

  to

   (and url-is-path revision
        (treesit--git-checkout-branch url revision))

  because that would change the semantics: note that there's an `(if` which checks
  whether we're dealing with a local path, and then in "else" branch (which is not in
  the snippet) we clone the repo. Changing the code as requested would result in the
  code cloning the repo when it's a local path but the revision wasn't set.

4. I hope it's not a problem: I made use of the word `path` inside documentation for
`treesit-language-source-alist`. It just seems to sound awkward repeating the word
"directory" twice, and I think it's clear from the context that these are synonyms.

Hopefully I didn't miss anything 😅

(Eli Zaretskii):
   * start sentences with capital letter and put 2 spaces after the dot
   * replace "path" with "dir" and "directory" in code and docs
   * capitalize and unquote arg names in the docs
   * (treesit--git-clone-repo): document REVISION being nil
   * call (expand-file-name) on the URL
   * added a NEWS entry
   * changed (treesit-language-source-alist) docs to shortly mention the workflow of
     building the grammar from a local path

 etc/NEWS        |  8 ++++++++
 lisp/treesit.el | 49 ++++++++++++++++++++++++++++++++++++-------------
 2 files changed, 44 insertions(+), 13 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 735a05f6579..19b06ea4c66 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1832,6 +1832,14 @@ The 'test' parameter is omitted if it is 'eql' (the default), as is
 'data' if empty.  'rehash-size', 'rehash-threshold' and 'size' are
 always omitted, and ignored if present when the object is read back in.
 
++++
+** 'treesit-install-language-grammar' can handle local directory as URL.
+It is now possible to pass a directory of a local repository as URL
+inside 'treesit-language-source-alist', so that calling
+'treesit-install-language-grammar' would avoid cloning the repository.
+It may be useful for example for the purposes of bisecting a
+treesitter grammar.
+
 
 * Changes in Emacs 30.1 on Non-Free Operating Systems
 
diff --git a/lisp/treesit.el b/lisp/treesit.el
index c8b473c7bb8..19dcbe324aa 100644
--- a/lisp/treesit.el
+++ b/lisp/treesit.el
@@ -3410,14 +3410,16 @@ treesit-explore-mode
 ;;; Install & build language grammar
 
 (defvar treesit-language-source-alist nil
-  "Configuration for downloading and installing tree-sitter language grammars.
+  "Configuration for downloading and installing tree-sitter language
+grammars. The grammar can also be built from a local directory if
+URL is an existing local path to the repo.
 
 The value should be an alist where each element has the form
 
     (LANG . (URL REVISION SOURCE-DIR CC C++))
 
 Only LANG and URL are mandatory.  LANG is the language symbol.
-URL is the Git repository URL for the grammar.
+URL is the Git repository URL or directory name for the grammar.
 
 REVISION is the Git tag or branch of the desired version,
 defaulting to the latest default branch.
@@ -3551,6 +3553,26 @@ treesit--call-process-signal
                                  (buffer-string)))
     (erase-buffer)))
 
+(defun treesit--git-checkout-branch (repo-dir revision)
+  "Checkout REVISION in a repo located in REPO-DIR."
+  (treesit--call-process-signal
+   "git" nil t nil "-C" repo-dir "checkout" revision))
+
+(defun treesit--git-clone-repo (url revision workdir)
+  "Clone repo pointed by URL at commit REVISION to WORKDIR.
+
+REVISION may be nil, in which case the cloned repo will be at its
+default branch."
+  (message "Cloning repository")
+  ;; git clone xxx --depth 1 --quiet [-b yyy] workdir
+  (if revision
+      (treesit--call-process-signal
+       "git" nil t nil "clone" url "--depth" "1" "--quiet"
+       "-b" revision workdir)
+    (treesit--call-process-signal
+     "git" nil t nil "clone" url "--depth" "1" "--quiet"
+     workdir)))
+
 (defun treesit--install-language-grammar-1
     (out-dir lang url &optional revision source-dir cc c++)
   "Install and compile a tree-sitter language grammar library.
@@ -3564,8 +3586,12 @@ treesit--install-language-grammar-1
 `treesit-language-source-alist'.  If anything goes wrong, this
 function signals an error."
   (let* ((lang (symbol-name lang))
+         (maybe-repo-dir (expand-file-name url))
+         (url-is-dir (file-accessible-directory-p maybe-repo-dir))
          (default-directory (make-temp-file "treesit-workdir" t))
-         (workdir (expand-file-name "repo"))
+         (workdir (if url-is-dir
+                      maybe-repo-dir
+                    (expand-file-name "repo")))
          (source-dir (expand-file-name (or source-dir "src") workdir))
          (cc (or cc (seq-find #'executable-find '("cc" "gcc" "c99"))
                  ;; If no C compiler found, just use cc and let
@@ -3580,15 +3606,10 @@ treesit--install-language-grammar-1
          (lib-name (concat "libtree-sitter-" lang soext)))
     (unwind-protect
         (with-temp-buffer
-          (message "Cloning repository")
-          ;; git clone xxx --depth 1 --quiet [-b yyy] workdir
-          (if revision
-              (treesit--call-process-signal
-               "git" nil t nil "clone" url "--depth" "1" "--quiet"
-               "-b" revision workdir)
-            (treesit--call-process-signal
-             "git" nil t nil "clone" url "--depth" "1" "--quiet"
-             workdir))
+          (if url-is-dir
+              (when revision
+                (treesit--git-checkout-branch workdir revision))
+            (treesit--git-clone-repo url revision workdir))
           ;; We need to go into the source directory because some
           ;; header files use relative path (#include "../xxx").
           ;; cd "${sourcedir}"
@@ -3635,7 +3656,9 @@ treesit--install-language-grammar-1
             ;; Ignore errors, in case the old version is still used.
             (ignore-errors (delete-file old-fname)))
           (message "Library installed to %s/%s" out-dir lib-name))
-      (when (file-exists-p workdir)
+      ;; Remove workdir if it's not a repo owned by user and we
+      ;; managed to create it in the first place.
+      (when (and (not url-is-dir) (file-exists-p workdir))
         (delete-directory workdir t)))))
 
 ;;; Etc
-- 
2.43.0