Subject: bug#60418: [PATCH] Add :vc keyword to use-package

From: Tony Zorman <[email-masked]>
Date: Mon, 01 May 2023 21:43:22 +0200

Hi,

attached are the updated patches; sorry that this took so long.

>From 2b5f99fcaec6c8f985c60ed3ff5e119a7fb58e39 Mon Sep 17 00:00:00 2001
From: Tony Zorman <[email-masked]>
Date: Thu, 29 Dec 2022 11:05:04 +0100
Subject: [PATCH 1/2] Add :vc keyword to use-package

* lisp/use-package/use-package-core.el (use-package-keywords): Add :vc.
(use-package-handler/:load-path): Insert 'load-path' into 'state'.
(use-package-vc-install): Install the package with package-vc.el.
(use-package-handler/:vc): Handler for the :vc keyword.
(use-package-normalize--vc-arg): Normalization for more complex
arguments to 'use-package-normalize/:vc', in order to make them
compatible with the specification of 'package-vc-selected-packages'.
(use-package-normalize/:vc): Normalizer for the :vc keyword.
(use-package): Document :vc.

* lisp/use-package/use-package-ensure.el (use-package-handler/:ensure):
Do not ensure a package when :vc is used in the declaration.

* test/lisp/use-package/use-package-tests.el (use-package-test/:vc-1):
(use-package-test/:vc-2):
(use-package-test/:vc-3):
(use-package-test/:vc-4):
(use-package-test/:vc-5):
(use-package-test-normalize/:vc):
Add tests for :vc.
---
 lisp/use-package/use-package-core.el       | 111 ++++++++++++++++++++-
 lisp/use-package/use-package-ensure.el     |   3 +-
 test/lisp/use-package/use-package-tests.el |  54 ++++++++++
 3 files changed, 165 insertions(+), 3 deletions(-)

diff --git a/lisp/use-package/use-package-core.el b/lisp/use-package/use-package-core.el
index 7ab5bdc276..e7e7ecc710 100644
--- a/lisp/use-package/use-package-core.el
+++ b/lisp/use-package/use-package-core.el
@@ -76,6 +76,7 @@ use-package-keywords
     :functions
     :preface
     :if :when :unless
+    :vc
     :no-require
     :catch
     :after
@@ -1151,7 +1152,8 @@ use-package-normalize/:load-path
     #'use-package-normalize-paths))
 
 (defun use-package-handler/:load-path (name _keyword arg rest state)
-  (let ((body (use-package-process-keywords name rest state)))
+  (let ((body (use-package-process-keywords name rest
+                (plist-put state :load-path arg))))
     (use-package-concat
      (mapcar #'(lambda (path)
                  `(eval-and-compile (add-to-list 'load-path ,path)))
@@ -1577,6 +1579,109 @@ use-package-handler/:config
      (when use-package-compute-statistics
        `((use-package-statistics-gather :config ',name t))))))
 
+;;;; :vc
+
+(defun use-package-vc-install (arg &optional local-path)
+  "Install a package with `package-vc.el'.
+ARG is a list of the form (NAME OPTIONS REVISION), as returned by
+`use-package-normalize--vc-arg'.  If LOCAL-PATH is non-nil, call
+`package-vc-install-from-checkout'; otherwise, indicating a
+remote host, call `package-vc-install' instead."
+  (pcase-let* ((`(,name ,opts ,rev) arg)
+               (spec (if opts (cons name opts) name)))
+    (unless (package-installed-p name)
+      (if local-path
+          (package-vc-install-from-checkout local-path (symbol-name name))
+        (package-vc-install spec rev)))))
+
+(defun use-package-handler/:vc (name _keyword arg rest state)
+  "Generate code to install package NAME, or do so directly.
+When the use-package declaration is part of a byte-compiled file,
+install the package during compilation; otherwise, add it to the
+macro expansion and wait until runtime.  The remaining arguments
+are as follows:
+
+_KEYWORD is ignored.
+
+ARG is the normalized input to the `:vc' keyword, as returned by
+the `use-package-normalize/:vc' function.
+
+REST is a plist of other (following) keywords and their
+arguments, each having already been normalised by the respective
+function.
+
+STATE is a plist of any state that keywords processed before
+`:vc' (see `use-package-keywords') may have accumulated.
+
+Also see the Info node `(use-package) Creating an extension'."
+  (let ((body (use-package-process-keywords name rest state))
+        (local-path (car (plist-get state :load-path))))
+    ;; See `use-package-handler/:ensure' for an explanation.
+    (if (bound-and-true-p byte-compile-current-file)
+        (funcall #'use-package-vc-install arg local-path)        ; compile time
+      (push `(use-package-vc-install ',arg ,local-path) body)))) ; runtime
+
+(defun use-package-normalize--vc-arg (arg)
+  "Normalize possible arguments to the `:vc' keyword.
+ARG is a cons-cell of approximately the form that
+`package-vc-selected-packages' accepts, plus an additional `:rev'
+keyword.  If `:rev' is not given, it defaults to `:last-release'.
+
+Returns a list (NAME SPEC REV), where (NAME . SPEC) is compliant
+with `package-vc-selected-packages' and REV is a (possibly nil,
+indicating the latest commit) revision."
+  (cl-flet* ((mk-string (s)
+	       (if (and s (stringp s)) s (symbol-name s)))
+             (mk-sym (s)
+               (if (and s (stringp s)) (intern s) s))
+	     (normalize (k v)
+               (pcase k
+                 (:rev (cond ((or (eq v :last-release) (not v)) :last-release)
+                             ((eq v :newest) nil)
+                             (t (mk-string v))))
+                 (:vc-backend (mk-sym v))
+                 (_ (mk-string v)))))
+    (pcase-let ((valid-kws '(:url :branch :lisp-dir :main-file :vc-backend :rev))
+                (`(,name . ,opts) arg))
+      (if (stringp opts)                ; (NAME . VERSION-STRING) ?
+          (list name opts)
+        ;; Error handling
+        (cl-loop for (k _) on opts by #'cddr
+                 if (not (member k valid-kws))
+                 do (use-package-error
+                     (format "Keyword :vc received unknown argument: %s. Supported keywords are: %s"
+                             k valid-kws)))
+        ;; Actual normalization
+        (list name
+              (cl-loop for (k v) on opts by #'cddr
+                       if (not (eq k :rev))
+                       nconc (list k (normalize k v)))
+              (normalize :rev (plist-get opts :rev)))))))
+
+(defun use-package-normalize/:vc (name _keyword args)
+  "Normalize possible arguments to the `:vc' keyword.
+NAME is the name of the `use-package' declaration, _KEYWORD is
+ignored, and ARGS it a list of arguments given to the `:vc'
+keyword, the cdr of which is ignored.
+
+See `use-package-normalize--vc-arg' for most of the actual
+normalization work.  Also see the Info
+node `(use-package) Creating an extension'."
+  (let ((arg (car args)))
+    (pcase arg
+      ((or 'nil 't) (list name))                 ; guess name
+      ((pred symbolp) (list arg))                ; use this name
+      ((pred stringp) (list name arg))           ; version string + guess name
+      ((pred plistp)                             ; plist + guess name
+       (use-package-normalize--vc-arg (cons name arg)))
+      (`(,(pred symbolp) . ,(or (pred plistp)    ; plist/version string + name
+                                (pred stringp)))
+       (use-package-normalize--vc-arg arg))
+      (_ (use-package-error "Unrecognised argument to :vc.\
+ The keyword wants an argument of nil, t, a name of a package,\
+ or a cons-cell as accepted by `package-vc-selected-packages', where \
+ the accepted plist is augmented by a `:rev' keyword.")))))
+
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;
 ;;; The main macro
@@ -1666,7 +1771,9 @@ use-package
                  (compare with `custom-set-variables').
 :custom-face     Call `custom-set-faces' with each face definition.
 :ensure          Loads the package using package.el if necessary.
-:pin             Pin the package to an archive."
+:pin             Pin the package to an archive.
+:vc              Install the package directly from a version control system
+                 (using `package-vc.el')."
   (declare (indent defun))
   (unless (memq :disabled args)
     (macroexp-progn
diff --git a/lisp/use-package/use-package-ensure.el b/lisp/use-package/use-package-ensure.el
index e0ea982594..395a0bbda0 100644
--- a/lisp/use-package/use-package-ensure.el
+++ b/lisp/use-package/use-package-ensure.el
@@ -182,7 +182,8 @@ use-package-ensure-elpa
 
 ;;;###autoload
 (defun use-package-handler/:ensure (name _keyword ensure rest state)
-  (let* ((body (use-package-process-keywords name rest state)))
+  (let* ((body (use-package-process-keywords name rest state))
+         (ensure (and (not (plist-member rest :vc)) ensure)))
     ;; We want to avoid installing packages when the `use-package' macro is
     ;; being macro-expanded by elisp completion (see `lisp--local-variables'),
     ;; but still install packages when byte-compiling, to avoid requiring
diff --git a/test/lisp/use-package/use-package-tests.el b/test/lisp/use-package/use-package-tests.el
index 6374a0d103..c8c20fc51c 100644
--- a/test/lisp/use-package/use-package-tests.el
+++ b/test/lisp/use-package/use-package-tests.el
@@ -1951,6 +1951,60 @@ bind-key/845
     (should (eq (nth 1 binding) 'ignore))
     (should (eq (nth 2 binding) nil))))
 
+(ert-deftest use-package-test/:vc-1 ()
+  (match-expansion
+   (use-package foo :vc (:url "bar"))
+   '(progn (use-package-vc-install '(foo (:url "bar") :last-release) nil)
+           (require 'foo nil nil))))
+
+(ert-deftest use-package-test/:vc-2 ()
+  (match-expansion
+   (use-package foo
+     :vc (baz . (:url "baz" :vc-backend "Git"
+                 :main-file qux.el :rev "rev-string")))
+   '(progn (use-package-vc-install '(baz
+                                     (:url "baz" :vc-backend Git :main-file "qux.el")
+                                     "rev-string")
+                                   nil)
+           (require 'foo nil nil))))
+
+(ert-deftest use-package-test/:vc-3 ()
+  (match-expansion
+   (use-package foo :vc (bar . "baz"))
+   '(progn (use-package-vc-install '(bar "baz") nil)
+           (require 'foo nil nil))))
+
+(ert-deftest use-package-test/:vc-4 ()
+  (match-expansion
+   (use-package foo :vc (bar . (:url "baz" :rev :newest)))
+   '(progn (use-package-vc-install '(bar (:url "baz") nil) nil)
+           (require 'foo nil nil))))
+
+(ert-deftest use-package-test/:vc-5 ()
+  (let ((load-path? '(pred (apply-partially
+                            #'string=
+                            (expand-file-name "bar" user-emacs-directory)))))
+    (match-expansion
+     (use-package foo :vc other-name :load-path "bar")
+     `(progn (eval-and-compile
+               (add-to-list 'load-path ,load-path?))
+             (use-package-vc-install '(other-name) ,load-path?)
+             (require 'foo nil nil)))))
+
+(ert-deftest use-package-test-normalize/:vc ()
+  (should (equal '(foo "version-string")
+                 (use-package-normalize/:vc 'foo :vc '("version-string"))))
+  (should (equal '(bar "version-string")
+                 (use-package-normalize/:vc 'foo :vc '((bar . "version-string")))))
+  (should (equal '(foo (:url "bar") "baz")
+                 (use-package-normalize/:vc 'foo :vc '((:url "bar" :rev "baz")))))
+  (should (equal '(foo)
+                 (use-package-normalize/:vc 'foo :vc '(t))))
+  (should (equal '(foo)
+                 (use-package-normalize/:vc 'foo :vc nil)))
+  (should (equal '(bar)
+                 (use-package-normalize/:vc 'foo :vc '(bar)))))
+
 ;; Local Variables:
 ;; no-byte-compile: t
 ;; no-update-autoloads: t
-- 
2.40.1

>From fba961176cd6ffb9998413f25956f7f6e044e064 Mon Sep 17 00:00:00 2001
From: Tony Zorman <[email-masked]>
Date: Thu, 29 Dec 2022 12:23:56 +0100
Subject: [PATCH 2/2] ; Document use-package's :vc keyword

* doc/misc/use-package.texi (Installing packages):
(Install package):
Add documentation for :vc and link to the related chapter in the Emacs
manual.

* etc/NEWS: Mention :vc keyword
---
 doc/misc/use-package.texi | 50 +++++++++++++++++++++++++++++++++++++--
 etc/NEWS                  | 13 ++++++++++
 2 files changed, 61 insertions(+), 2 deletions(-)

diff --git a/doc/misc/use-package.texi b/doc/misc/use-package.texi
index 87105c4db0..d75cb67e08 100644
--- a/doc/misc/use-package.texi
+++ b/doc/misc/use-package.texi
@@ -1554,8 +1554,11 @@ Installing packages
 (@pxref{Package Installation,,, emacs, GNU Emacs Manual}).  The
 @code{use-package} macro provides the @code{:ensure} and @code{:pin}
 keywords that interface with that package manager to automatically
-install packages.  This is particularly useful if you use your init
-file on more than one system.
+install packages.  The @code{:vc} keyword may be used to control how
+package sources are downloaded; e.g., from remote hosts
+(@pxref{Fetching Package Sources,,, emacs, GNU Emacs Manual}).  This
+is particularly useful if you use your init file on more than one
+system.
 
 @menu
 * Install package::
@@ -1607,6 +1610,49 @@ Install package
 You can override the above setting for a single package by adding
 @w{@code{:ensure nil}} to its declaration.
 
+@findex :vc
+The @code{:vc} keyword can be used to control how packages are
+downloaded and/or installed. More specifically, it allows one to fetch
+and update packages directly from a version control system. This is
+especially convenient when wanting to install a package that is not on
+any package archive.
+
+The keyword accepts the same arguments as specified in
+@pxref{Fetching Package Sources,,, emacs, GNU Emacs Manual}, except
+that a name need not explicitly be given: it is inferred from the
+declaration.  The accepted property list is augmented by a @code{:rev}
+keyword, which has the same shape as the @code{REV} argument to
+@code{package-vc-install}.  Notably -- even when not specified --
+@code{:rev} defaults to checking out the last release of the package.
+You can use @code{:rev :newest} to check out the latest commit.
+
+For example,
+
+@example
+@group
+(use-package bbdb
+  :vc (:url "https://git.savannah.nongnu.org/git/bbdb.git"
+       :rev :newest))
+@end group
+@end example
+
+would try -- by invoking @code{package-vc-install} -- to install the
+latest commit of the package @code{foo} from the specified remote.
+
+This can also be used for local packages, by combining it with the
+@code{:load-path} (@pxref{Load path}) keyword:
+
+@example
+@group
+;; Use a local copy of BBDB instead of the one from GNU ELPA.
+(use-package bbdb
+  :vc t
+  :load-path "/path/to/bbdb/dir/")
+@end group
+@end example
+
+The above dispatches to @code{package-vc-install-from-checkout}.
+
 @node Pinning packages
 @section Pinning packages using @code{:pin}
 @cindex installing package from specific archive
diff --git a/etc/NEWS b/etc/NEWS
index b989f80f3c..20f6751121 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -322,6 +322,19 @@ instead of:
         and another_expression):
         do_something()
 
+** use-package
+
++++
+*** New ':vc' keyword.
+This keyword enables the user to control how packages are fetched by
+utilising 'package-vc.el'.  By default, it relays its arguments to
+'package-vc-install', but -- when combined with the ':load-path'
+keyword -- it can also call upon 'package-vc-install-from-checkout'
+instead.  If no revision is given via the ':rev' argument, use-package
+falls back to the last release (via 'package-vc-install's
+':last-release' argument).  To check out the last commit, use ':rev
+:newest'.
+
 
 * New Modes and Packages in Emacs 30.1
 
-- 
2.40.1

-- 
Tony Zorman | https://tony-zorman.com/