Message ID | 87mtomzzu1.fsf@yoctocell.xyz |
---|---|
State | Accepted |
Headers | show |
Series | [bug#50359] import: Add 'generic-git' updater. | expand |
Context | Check | Description |
---|---|---|
cbaines/applying patch | fail | View Laminar job |
cbaines/issue | success | View issue |
Hello, This looks very cool! Xinglu Chen <public@yoctocell.xyz> skribis: > From f924dbb835425f6b9a5796918125592870391405 Mon Sep 17 00:00:00 2001 > Message-Id: <f924dbb835425f6b9a5796918125592870391405.1631125652.git.public@yoctocell.xyz> > From: Xinglu Chen <public@yoctocell.xyz> > Date: Fri, 3 Sep 2021 17:50:56 +0200 > Subject: [PATCH] import: Add 'generic-git' updater. > > * guix/import/git.scm: New file. > * doc/guix.texi (Invoking guix refresh): Document it. > * Makefile.am (MODULES): Register it. > * guix/git.scm (ls-remote-refs): New procedure. > > Co-authored-by: Sarah Morgensen <iskarian@mgsn.dev> Overall LGTM; comments below. > diff --git a/doc/guix.texi b/doc/guix.texi > index 36a0c7f5ec..26afb1607a 100644 > --- a/doc/guix.texi > +++ b/doc/guix.texi > @@ -11920,6 +11920,33 @@ the updater for @uref{https://launchpad.net, Launchpad} packages. > @item generic-html > a generic updater that crawls the HTML page where the source tarball of > the package is hosted, when applicable. > +@item generic-git Please add a newline above. > +@lisp > +(package > + (name "foo") > + ;; ... > + (properties > + '((tag-prefix . "^release0-") > + (tag-suffix . "[a-z]?$") > + (tag-version-delimiter . ":")))) > +@end lisp Very nice. Perhaps s/tag-/release-tag-/ for clarity? > +(define* (ls-remote-refs url #:key tags?) > + "Return the list of references advertised at Git repository URL. If TAGS? > +is true, limit to only refs/tags." To remain consistent with existing naming conventions, I’d call it ‘remote-refs’. > + (with-libgit2 > + (call-with-temporary-directory > + (lambda (cache-directory) > + (let* ((repository (repository-init cache-directory)) > + ;; Create an in-memory remote so we don't touch disk. > + (remote (remote-create-anonymous repository url))) Too bad we need to create an empty repo; hopefully it costs next to nothing though. > + (remote-connect remote) > + (remote-disconnect remote) > + (repository-close! repository) > + > + (filter include? (map remote-head-name (remote-ls remote)))))))) Use ‘filter-map’. > +(define* (get-version-mapping tags #:key prefix suffix delim pre-releases?) Please add a docstring and remove ‘get-’ from the name. :-) > + (define (guess-delim) > + (let ((total (length tags)) > + (dots (reduce + 0 (map (cut string-count <> #\.) tags))) > + (dashes (reduce + 0 (map (cut string-count <> #\-) tags))) > + (underscores (reduce + 0 (map (cut string-count <> #\_) tags)))) > + (display (format #t "total: ~d, dots: ~d, dashes ~d, underscores ~d~%" > + total dots dashes underscores)) Leftover? (Also display + format.) Please spell out ‘delimiter’ (info "(guix) Formatting Code"). > + (cond > + ((>= dots (* total 0.35)) ".") > + ((>= dashes (* total 0.8)) "-") > + ((>= underscores (* total 0.8)) "_") > + (else "")))) That’s a fancy heuristic. :-) > + (let ((mapping (fold alist-cons '() (map get-version tags) tags))) > + (stable-sort! (filter car mapping) entry<?))) It’s perhaps clearer written like this: (stable-sort (filter-map (lambda (tag) (let ((version (get-version tag))) (and version (cons version tag)))) tags) entry<?) > +(define* (get-latest-tag url #:key prefix suffix delim pre-releases?) > + "Return the latest tag available from the Git repository at URL." Maybe “the tag corresponding to the latest version”. s/get-latest-tag/latest-tag/ > + (define (pre-release? tag) > + (any (lambda (rx) (regexp-exec (make-regexp rx regexp/icase) tag)) > + %pre-release-words)) Better call ‘make-regexp’ only once; so you could change ‘%pre-release-words’ to be a list of regexp objects instead of a list of strings. > +(define (latest-git-tag-version package tag-prefix tag-suffix > + tag-version-delimiter refresh-pre-releases?) > + "Given a PACKAGE, the TAG-PREFIX, TAG-SUFFIX, TAG-VERSION-DELIMITER, and > +REFRESH-PRE-RELEASES? properties of PACKAGE, returns the latest version of > +PACKAGE." Maybe s/refresh-pre-releases?/accept-pre-preleases?/ Since this procedure takes a package, it probably doesn’t need the other arguments: it can extract them from the package properties, rather than doing it at the call site. > +(define (latest-git-release package) > + "Return the latest release of PACKAGE." > + (let* ((name (package-name package)) > + (properties (package-properties package)) > + (tag-prefix (assq-ref properties 'tag-prefix)) > + (tag-suffix (assq-ref properties 'tag-suffix)) > + (tag-version-delimiter (assq-ref properties 'tag-version-delimiter)) > + (refresh-pre-releases? (assq-ref properties 'refresh-pre-releases?)) > + (old-version (package-version package)) > + (url (git-reference-url (origin-uri (package-source package)))) > + (new-version (latest-git-tag-version package > + tag-prefix > + tag-suffix > + tag-version-delimiter > + refresh-pre-releases?))) > + > + (if new-version > + (upstream-source > + (package name) > + (version new-version) > + (urls (list url))) > + ;; No new release or no tags available. > + #f))) Simply: (and new-version (upstream-source …)). It would have been nice to have tests. I think testing ‘latest-git-release’ should be feasible without too much hassle using the (guix tests git) infrastructure, as is done in tests/git.scm, with a package referring to a locally-created repo using a git-reference with a file:// URL. Could you send an updated patch? Thanks, Ludo’.
On Fri, Sep 10 2021, Ludovic Courtès wrote: > Hello, > > This looks very cool! Thanks for taking a look! It’s still a WIP, but I think it’s getting there. :-) > Xinglu Chen <public@yoctocell.xyz> skribis: > >> From f924dbb835425f6b9a5796918125592870391405 Mon Sep 17 00:00:00 2001 >> Message-Id: <f924dbb835425f6b9a5796918125592870391405.1631125652.git.public@yoctocell.xyz> >> From: Xinglu Chen <public@yoctocell.xyz> >> Date: Fri, 3 Sep 2021 17:50:56 +0200 >> Subject: [PATCH] import: Add 'generic-git' updater. >> >> * guix/import/git.scm: New file. >> * doc/guix.texi (Invoking guix refresh): Document it. >> * Makefile.am (MODULES): Register it. >> * guix/git.scm (ls-remote-refs): New procedure. >> >> Co-authored-by: Sarah Morgensen <iskarian@mgsn.dev> > > Overall LGTM; comments below. > >> diff --git a/doc/guix.texi b/doc/guix.texi >> index 36a0c7f5ec..26afb1607a 100644 >> --- a/doc/guix.texi >> +++ b/doc/guix.texi >> @@ -11920,6 +11920,33 @@ the updater for @uref{https://launchpad.net, Launchpad} packages. >> @item generic-html >> a generic updater that crawls the HTML page where the source tarball of >> the package is hosted, when applicable. >> +@item generic-git > > Please add a newline above. Noted. >> +@lisp >> +(package >> + (name "foo") >> + ;; ... >> + (properties >> + '((tag-prefix . "^release0-") >> + (tag-suffix . "[a-z]?$") >> + (tag-version-delimiter . ":")))) >> +@end lisp > > Very nice. Perhaps s/tag-/release-tag-/ for clarity? Good idea. >> +(define* (ls-remote-refs url #:key tags?) >> + "Return the list of references advertised at Git repository URL. If TAGS? >> +is true, limit to only refs/tags." > > To remain consistent with existing naming conventions, I’d call it > ‘remote-refs’. > >> + (with-libgit2 >> + (call-with-temporary-directory >> + (lambda (cache-directory) >> + (let* ((repository (repository-init cache-directory)) >> + ;; Create an in-memory remote so we don't touch disk. >> + (remote (remote-create-anonymous repository url))) > > Too bad we need to create an empty repo; hopefully it costs next to > nothing though. > >> + (remote-connect remote) >> + (remote-disconnect remote) >> + (repository-close! repository) >> + >> + (filter include? (map remote-head-name (remote-ls remote)))))))) > > Use ‘filter-map’. > >> +(define* (get-version-mapping tags #:key prefix suffix delim pre-releases?) > > Please add a docstring and remove ‘get-’ from the name. :-) > >> + (define (guess-delim) >> + (let ((total (length tags)) >> + (dots (reduce + 0 (map (cut string-count <> #\.) tags))) >> + (dashes (reduce + 0 (map (cut string-count <> #\-) tags))) >> + (underscores (reduce + 0 (map (cut string-count <> #\_) tags)))) >> + (display (format #t "total: ~d, dots: ~d, dashes ~d, underscores ~d~%" >> + total dots dashes underscores)) > > Leftover? (Also display + format.) Yep. :-) > Please spell out ‘delimiter’ (info "(guix) Formatting Code"). > >> + (cond >> + ((>= dots (* total 0.35)) ".") >> + ((>= dashes (* total 0.8)) "-") >> + ((>= underscores (* total 0.8)) "_") >> + (else "")))) > > That’s a fancy heuristic. :-) Yeah, it was suggested by Sarah, and in my testing it seems to work pretty well. :-) >> + (let ((mapping (fold alist-cons '() (map get-version tags) tags))) >> + (stable-sort! (filter car mapping) entry<?))) > > It’s perhaps clearer written like this: > > (stable-sort (filter-map (lambda (tag) > (let ((version (get-version tag))) > (and version (cons version tag)))) > tags) > entry<?) Agreed, I will use your suggested version. >> +(define* (get-latest-tag url #:key prefix suffix delim pre-releases?) >> + "Return the latest tag available from the Git repository at URL." > > Maybe “the tag corresponding to the latest version”. Yeah, as the latest tag might not correspond to a release... > s/get-latest-tag/latest-tag/ > >> + (define (pre-release? tag) >> + (any (lambda (rx) (regexp-exec (make-regexp rx regexp/icase) tag)) >> + %pre-release-words)) > > Better call ‘make-regexp’ only once; so you could change > ‘%pre-release-words’ to be a list of regexp objects instead of a list of > strings. Noted. >> +(define (latest-git-tag-version package tag-prefix tag-suffix >> + tag-version-delimiter refresh-pre-releases?) >> + "Given a PACKAGE, the TAG-PREFIX, TAG-SUFFIX, TAG-VERSION-DELIMITER, and >> +REFRESH-PRE-RELEASES? properties of PACKAGE, returns the latest version of >> +PACKAGE." > > Maybe s/refresh-pre-releases?/accept-pre-preleases?/ ‘accept-pre-releases?’ ;-) > Since this procedure takes a package, it probably doesn’t need the other > arguments: it can extract them from the package properties, rather than > doing it at the call site. Good point. >> +(define (latest-git-release package) >> + "Return the latest release of PACKAGE." >> + (let* ((name (package-name package)) >> + (properties (package-properties package)) >> + (tag-prefix (assq-ref properties 'tag-prefix)) >> + (tag-suffix (assq-ref properties 'tag-suffix)) >> + (tag-version-delimiter (assq-ref properties 'tag-version-delimiter)) >> + (refresh-pre-releases? (assq-ref properties 'refresh-pre-releases?)) >> + (old-version (package-version package)) >> + (url (git-reference-url (origin-uri (package-source package)))) >> + (new-version (latest-git-tag-version package >> + tag-prefix >> + tag-suffix >> + tag-version-delimiter >> + refresh-pre-releases?))) >> + >> + (if new-version >> + (upstream-source >> + (package name) >> + (version new-version) >> + (urls (list url))) >> + ;; No new release or no tags available. >> + #f))) > > Simply: (and new-version (upstream-source …)). > > It would have been nice to have tests. I think testing > ‘latest-git-release’ should be feasible without too much hassle using > the (guix tests git) infrastructure, as is done in tests/git.scm, with a > package referring to a locally-created repo using a git-reference with a > file:// URL. Thanks for the pointers! I will look into it. > Could you send an updated patch? Sure! Thanks for the review! :-)
From f924dbb835425f6b9a5796918125592870391405 Mon Sep 17 00:00:00 2001 Message-Id: <f924dbb835425f6b9a5796918125592870391405.1631125652.git.public@yoctocell.xyz> From: Xinglu Chen <public@yoctocell.xyz> Date: Fri, 3 Sep 2021 17:50:56 +0200 Subject: [PATCH] import: Add 'generic-git' updater. * guix/import/git.scm: New file. * doc/guix.texi (Invoking guix refresh): Document it. * Makefile.am (MODULES): Register it. * guix/git.scm (ls-remote-refs): New procedure. Co-authored-by: Sarah Morgensen <iskarian@mgsn.dev> --- Makefile.am | 1 + doc/guix.texi | 27 ++++++ guix/git.scm | 33 +++++++ guix/import/git.scm | 217 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+) create mode 100644 guix/import/git.scm diff --git a/Makefile.am b/Makefile.am index 3c79760734..c4d3a456b1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -254,6 +254,7 @@ MODULES = \ guix/import/egg.scm \ guix/import/elpa.scm \ guix/import/gem.scm \ + guix/import/git.scm \ guix/import/github.scm \ guix/import/gnome.scm \ guix/import/gnu.scm \ diff --git a/doc/guix.texi b/doc/guix.texi index 36a0c7f5ec..26afb1607a 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -11920,6 +11920,33 @@ the updater for @uref{https://launchpad.net, Launchpad} packages. @item generic-html a generic updater that crawls the HTML page where the source tarball of the package is hosted, when applicable. +@item generic-git +a generic updater for packages hosted on Git repositories. It tries to +be smart about parsing Git tag names, but if it is not able to parse the +tag name and compare tags correctly, users can define the following +properties for a package. + +@itemize +@item @code{tag-prefix}: a regular expression for matching a prefix of +the tag name. + +@item @code{tag-suffix}: a regular expression for matching a suffix of +the tag name. + +@item @code{tag-version-delimiter}: a string used as the delimiter in +the tag name for separating the numbers of the version. +@end itemize + +@lisp +(package + (name "foo") + ;; ... + (properties + '((tag-prefix . "^release0-") + (tag-suffix . "[a-z]?$") + (tag-version-delimiter . ":")))) +@end lisp + @end table For instance, the following command only checks for updates of Emacs diff --git a/guix/git.scm b/guix/git.scm index 9c6f326c36..c5d0d2da8e 100644 --- a/guix/git.scm +++ b/guix/git.scm @@ -56,6 +56,8 @@ commit-difference commit-relation + ls-remote-refs + git-checkout git-checkout? git-checkout-url @@ -556,6 +558,37 @@ objects: 'ancestor (meaning that OLD is an ancestor of NEW), 'descendant, or (if (set-contains? oldest new) 'descendant 'unrelated)))))) + +;; +;;; Remote operations. +;;; + +(define* (ls-remote-refs url #:key tags?) + "Return the list of references advertised at Git repository URL. If TAGS? +is true, limit to only refs/tags." + (define (ref? ref) + ;; Like `git ls-remote --refs', only show actual references. + (and (string-prefix? "refs/" ref) + (not (string-suffix? "^{}" ref)))) + + (define (tag? ref) + (string-prefix? "refs/tags/" ref)) + + (define (include? ref) + (and (ref? ref) + (or (not tags?) (tag? ref)))) + + (with-libgit2 + (call-with-temporary-directory + (lambda (cache-directory) + (let* ((repository (repository-init cache-directory)) + ;; Create an in-memory remote so we don't touch disk. + (remote (remote-create-anonymous repository url))) + (remote-connect remote) + (remote-disconnect remote) + (repository-close! repository) + + (filter include? (map remote-head-name (remote-ls remote)))))))) ;;; diff --git a/guix/import/git.scm b/guix/import/git.scm new file mode 100644 index 0000000000..52c98de197 --- /dev/null +++ b/guix/import/git.scm @@ -0,0 +1,217 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz> +;;; Copyright © 2021 Sarah Morgensen <iskarian@mgsn.dev> +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. + +(define-module (guix import git) + #:use-module (guix build utils) + #:use-module (guix diagnostics) + #:use-module (guix git) + #:use-module (guix git-download) + #:use-module (guix i18n) + #:use-module (guix packages) + #:use-module (guix upstream) + #:use-module (guix utils) + #:use-module (ice-9 format) + #:use-module (ice-9 match) + #:use-module (ice-9 rdelim) + #:use-module (ice-9 regex) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-26) + #:use-module (srfi srfi-34) + #:use-module (srfi srfi-35) + #:export (%generic-git-updater)) + +;;; Commentary: +;;; +;;; This module provides a generic package updater for packages hosted on Git +;;; repositories. +;;; +;;; It tries to be smart about tag names, but if it is not automatically able +;;; to parse the tag names correctly, users can set the `tag-prefix', +;;; `tag-suffix' and `tag-version-delimiter' properties of the package to make +;;; the updater parse the Git tag name correctly. +;;; +;;; Code: + +;;; Errors & warnings + +(define-condition-type &git-no-valid-tags-error &error + git-no-valid-tags-error?) + +(define (git-no-valid-tags-error) + (raise (condition (&message (message "no valid tags found")) + (&git-no-valid-tags-error)))) + +(define-condition-type &git-no-tags-error &error + git-no-tags-error?) + +(define (git-no-tags-error) + (raise (condition (&message (message "no tags were found")) + (&git-no-tags-error)))) + + +;;; Updater + +(define %pre-release-words + '("alpha" "beta" "rc" "dev" "test")) + +(define* (get-version-mapping tags #:key prefix suffix delim pre-releases?) + (define (guess-delim) + (let ((total (length tags)) + (dots (reduce + 0 (map (cut string-count <> #\.) tags))) + (dashes (reduce + 0 (map (cut string-count <> #\-) tags))) + (underscores (reduce + 0 (map (cut string-count <> #\_) tags)))) + (display (format #t "total: ~d, dots: ~d, dashes ~d, underscores ~d~%" + total dots dashes underscores)) + (cond + ((>= dots (* total 0.35)) ".") + ((>= dashes (* total 0.8)) "-") + ((>= underscores (* total 0.8)) "_") + (else "")))) + + (define delim-rx (regexp-quote (or delim (guess-delim)))) + (define suffix-rx + (string-append + (or suffix + (if pre-releases? + (string-append ".*(" (string-join %pre-release-words "|") ").*") + "")) + "$")) + + (define prefix-rx (string-append "^" (or prefix "[^[:digit:]]*"))) + (pk delim-rx) + + (define tag-rx + (string-append "([[:digit:]][^" delim-rx "[:punct:]]*" + "(" delim-rx "[^[:punct:]" delim-rx "]+)" + ;; If there is are no delimiters, it could mean that the + ;; version just contains one number (e.g., "2"), thus, use + ;; "*" instead of "+" to match zero or more numbers. + (if (string=? delim-rx "") "*" "+") + ")" suffix-rx)) + + (define (get-version tag) + (let ((tag-match (regexp-exec (make-regexp tag-rx) tag))) + (pk tag) + (pk tag-match) + (pk tag-rx) + (and tag-match + (regexp-exec (make-regexp prefix-rx) (match:prefix tag-match)) + (regexp-substitute/global + #f delim-rx (match:substring tag-match) + ;; Don't insert "." if there aren't any delimiters in the first + ;; place. + 'pre (if (string=? delim-rx "") "" ".") 'post)))) + + (define (entry<? a b) + (eq? (version-compare (car a) (car b)) '<)) + + (let ((mapping (fold alist-cons '() (map get-version tags) tags))) + (stable-sort! (filter car mapping) entry<?))) + +(define* (get-latest-tag url #:key prefix suffix delim pre-releases?) + "Return the latest tag available from the Git repository at URL." + (define (pre-release? tag) + (any (lambda (rx) (regexp-exec (make-regexp rx regexp/icase) tag)) + %pre-release-words)) + + (let* ((tags (map (cut string-drop <> (string-length "refs/tags/")) + (ls-remote-refs url #:tags? #t))) + (versions->tags + (get-version-mapping (if pre-releases? + tags + (filter (negate pre-release?) tags)) + #:prefix prefix + #:suffix suffix + #:delim delim + #:pre-releases? pre-releases?))) + (display versions->tags) + (newline) + (cond + ((null? tags) + (git-no-tags-error)) + ((null? versions->tags) + (git-no-valid-tags-error)) + (else + (match (last versions->tags) + ((version . tag) + (values version tag))))))) + +(define (latest-git-tag-version package tag-prefix tag-suffix + tag-version-delimiter refresh-pre-releases?) + "Given a PACKAGE, the TAG-PREFIX, TAG-SUFFIX, TAG-VERSION-DELIMITER, and +REFRESH-PRE-RELEASES? properties of PACKAGE, returns the latest version of +PACKAGE." + (guard (c ((or (git-no-tags-error? c) (git-no-valid-tags-error? c)) + (warning (or (package-field-location package 'source) + (package-location package)) + (G_ "~a for ~a~%") + (condition-message c) + (package-name package)) + #f) + ((eq? (exception-kind c) 'git-error) + (warning (or (package-field-location package 'source) + (package-location package)) + (G_ "failed to fetch Git repository for ~a~%") + (package-name package)) + #f)) + (let* ((source (package-source package)) + (url (git-reference-url (origin-uri source)))) + ;;(format #t "~a~%" (package-name package)) + (get-latest-tag url #:prefix tag-prefix #:suffix tag-suffix + #:delim tag-version-delimiter + #:pre-releases? refresh-pre-releases?)))) + +(define (git-package? package) + "Whether the origin of PACKAGE is a Git repostiory." + (match (package-source package) + ((? origin? origin) + (and (eq? (origin-method origin) git-fetch) + (git-reference? (origin-uri origin)))) + (_ #f))) + +(define (latest-git-release package) + "Return the latest release of PACKAGE." + (let* ((name (package-name package)) + (properties (package-properties package)) + (tag-prefix (assq-ref properties 'tag-prefix)) + (tag-suffix (assq-ref properties 'tag-suffix)) + (tag-version-delimiter (assq-ref properties 'tag-version-delimiter)) + (refresh-pre-releases? (assq-ref properties 'refresh-pre-releases?)) + (old-version (package-version package)) + (url (git-reference-url (origin-uri (package-source package)))) + (new-version (latest-git-tag-version package + tag-prefix + tag-suffix + tag-version-delimiter + refresh-pre-releases?))) + + (if new-version + (upstream-source + (package name) + (version new-version) + (urls (list url))) + ;; No new release or no tags available. + #f))) + +(define %generic-git-updater + (upstream-updater + (name 'generic-git) + (description "Updater for packages hosted on Git repositories") + (pred git-package?) + (latest latest-git-release))) -- 2.33.0