channels: Add support for a news file.
* guix/channels.scm (<channel-metadata>)[news-file]: New field. (read-channel-metadata): Set the 'news-file' field. (read-channel-metadata-from-source): Likewise. (<channel-news>, <channel-news-entry>): New record types. (sexp->channel-news-entry, read-channel-news) (channel-news-for-commit): New procedures. * guix/tests/git.scm (populate-git-repository): For 'add', allow CONTENTS to be a procedure. * tests/channels.scm ("channel-news, no news") ("channel-news, one entry"): New tests. * doc/guix.texi (Channels): Document it.
This commit is contained in:
parent
873f6f1334
commit
8ba7fd3cd6
|
@ -3991,6 +3991,68 @@ add a meta-data file @file{.guix-channel} that contains:
|
||||||
(directory "guix"))
|
(directory "guix"))
|
||||||
@end lisp
|
@end lisp
|
||||||
|
|
||||||
|
@cindex news, for channels
|
||||||
|
@subsection Writing Channel News
|
||||||
|
|
||||||
|
Channel authors may occasionally want to communicate to their users
|
||||||
|
information about important changes in the channel. You'd send them all
|
||||||
|
an email, but that's not convenient.
|
||||||
|
|
||||||
|
Instead, channels can provide a @dfn{news file}; when the channel users
|
||||||
|
run @command{guix pull}, that news file is automatically read and
|
||||||
|
@command{guix pull --news} can display the announcements that correspond
|
||||||
|
to the new commits that have been pulled, if any.
|
||||||
|
|
||||||
|
To do that, channel authors must first declare the name of the news file
|
||||||
|
in their @file{.guix-channel} file:
|
||||||
|
|
||||||
|
@lisp
|
||||||
|
(channel
|
||||||
|
(version 0)
|
||||||
|
(news-file "etc/news.txt"))
|
||||||
|
@end lisp
|
||||||
|
|
||||||
|
The news file itself, @file{etc/news.txt} in this example, must look
|
||||||
|
something like this:
|
||||||
|
|
||||||
|
@lisp
|
||||||
|
(channel-news
|
||||||
|
(version 0)
|
||||||
|
(entry (commit "d894ab8e9bfabcefa6c49d9ba2e834dd5a73a300")
|
||||||
|
(title (en "Fixed terrible bug")
|
||||||
|
(fr "Oh la la"))
|
||||||
|
(body (en "@@emph@{Good news@}! It's fixed!")
|
||||||
|
(eo "Certe ĝi pli bone funkcias nun!")))
|
||||||
|
(entry (commit "bdcabe815cd28144a2d2b4bc3c5057b051fa9906")
|
||||||
|
(title (en "Added a great package")
|
||||||
|
(ca "Què vol dir guix?"))
|
||||||
|
(body (en "Don't miss the @@code@{hello@} package!"))))
|
||||||
|
@end lisp
|
||||||
|
|
||||||
|
The file consists of a list of @dfn{news entries}. Each entry is
|
||||||
|
associated with a commit: it describes changes made in this commit,
|
||||||
|
possibly in preceding commits as well. Users see entries only the first
|
||||||
|
time they obtain the commit the entry refers to.
|
||||||
|
|
||||||
|
The @code{title} field should be a one-line summary while @code{body}
|
||||||
|
can be arbitrarily long, and both can contain Texinfo markup
|
||||||
|
(@pxref{Overview,,, texinfo, GNU Texinfo}). Both the title and body are
|
||||||
|
a list of language tag/message tuples, which allows @command{guix pull}
|
||||||
|
to display news in the language that corresponds to the user's locale.
|
||||||
|
|
||||||
|
If you want to translate news using a gettext-based workflow, you can
|
||||||
|
extract translatable strings with @command{xgettext} (@pxref{xgettext
|
||||||
|
Invocation,,, gettext, GNU Gettext Utilities}). For example, assuming
|
||||||
|
you write news entries in English first, the command below creates a PO
|
||||||
|
file containing the strings to translate:
|
||||||
|
|
||||||
|
@example
|
||||||
|
xgettext -o news.po -l scheme -ken etc/news.scm
|
||||||
|
@end example
|
||||||
|
|
||||||
|
To sum up, yes, you could use your channel as a blog. But beware, this
|
||||||
|
is @emph{not quite} what your users might expect.
|
||||||
|
|
||||||
@subsection Replicating Guix
|
@subsection Replicating Guix
|
||||||
|
|
||||||
@cindex pinning, channels
|
@cindex pinning, channels
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
|
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
(define-module (guix channels)
|
(define-module (guix channels)
|
||||||
|
#:use-module (git)
|
||||||
#:use-module (guix git)
|
#:use-module (guix git)
|
||||||
#:use-module (guix records)
|
#:use-module (guix records)
|
||||||
#:use-module (guix gexp)
|
#:use-module (guix gexp)
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
#:use-module (guix derivations)
|
#:use-module (guix derivations)
|
||||||
#:use-module (guix combinators)
|
#:use-module (guix combinators)
|
||||||
#:use-module (guix diagnostics)
|
#:use-module (guix diagnostics)
|
||||||
|
#:use-module (guix sets)
|
||||||
#:use-module (guix store)
|
#:use-module (guix store)
|
||||||
#:use-module (guix i18n)
|
#:use-module (guix i18n)
|
||||||
#:use-module ((guix utils)
|
#:use-module ((guix utils)
|
||||||
|
@ -67,7 +69,14 @@
|
||||||
%channel-profile-hooks
|
%channel-profile-hooks
|
||||||
channel-instances->derivation
|
channel-instances->derivation
|
||||||
|
|
||||||
profile-channels))
|
profile-channels
|
||||||
|
|
||||||
|
channel-news-entry?
|
||||||
|
channel-news-entry-commit
|
||||||
|
channel-news-entry-title
|
||||||
|
channel-news-entry-body
|
||||||
|
|
||||||
|
channel-news-for-commit))
|
||||||
|
|
||||||
;;; Commentary:
|
;;; Commentary:
|
||||||
;;;
|
;;;
|
||||||
|
@ -110,10 +119,11 @@
|
||||||
(checkout channel-instance-checkout))
|
(checkout channel-instance-checkout))
|
||||||
|
|
||||||
(define-record-type <channel-metadata>
|
(define-record-type <channel-metadata>
|
||||||
(channel-metadata directory dependencies)
|
(channel-metadata directory dependencies news-file)
|
||||||
channel-metadata?
|
channel-metadata?
|
||||||
(directory channel-metadata-directory) ;string with leading slash
|
(directory channel-metadata-directory) ;string with leading slash
|
||||||
(dependencies channel-metadata-dependencies)) ;list of <channel>
|
(dependencies channel-metadata-dependencies) ;list of <channel>
|
||||||
|
(news-file channel-metadata-news-file)) ;string | #f
|
||||||
|
|
||||||
(define (channel-reference channel)
|
(define (channel-reference channel)
|
||||||
"Return the \"reference\" for CHANNEL, an sexp suitable for
|
"Return the \"reference\" for CHANNEL, an sexp suitable for
|
||||||
|
@ -129,12 +139,13 @@ if valid metadata could not be read from PORT."
|
||||||
(match (read port)
|
(match (read port)
|
||||||
(('channel ('version 0) properties ...)
|
(('channel ('version 0) properties ...)
|
||||||
(let ((directory (and=> (assoc-ref properties 'directory) first))
|
(let ((directory (and=> (assoc-ref properties 'directory) first))
|
||||||
(dependencies (or (assoc-ref properties 'dependencies) '())))
|
(dependencies (or (assoc-ref properties 'dependencies) '()))
|
||||||
|
(news-file (and=> (assoc-ref properties 'news-file) first)))
|
||||||
(channel-metadata
|
(channel-metadata
|
||||||
(cond ((not directory) "/")
|
(cond ((not directory) "/") ;directory
|
||||||
((string-prefix? "/" directory) directory)
|
((string-prefix? "/" directory) directory)
|
||||||
(else (string-append "/" directory)))
|
(else (string-append "/" directory)))
|
||||||
(map (lambda (item)
|
(map (lambda (item) ;dependencies
|
||||||
(let ((get (lambda* (key #:optional default)
|
(let ((get (lambda* (key #:optional default)
|
||||||
(or (and=> (assoc-ref item key) first) default))))
|
(or (and=> (assoc-ref item key) first) default))))
|
||||||
(and-let* ((name (get 'name))
|
(and-let* ((name (get 'name))
|
||||||
|
@ -145,7 +156,8 @@ if valid metadata could not be read from PORT."
|
||||||
(branch branch)
|
(branch branch)
|
||||||
(url url)
|
(url url)
|
||||||
(commit (get 'commit))))))
|
(commit (get 'commit))))))
|
||||||
dependencies))))
|
dependencies)
|
||||||
|
news-file))) ;news-file
|
||||||
((and ('channel ('version version) _ ...) sexp)
|
((and ('channel ('version version) _ ...) sexp)
|
||||||
(raise (condition
|
(raise (condition
|
||||||
(&message (message "unsupported '.guix-channel' version"))
|
(&message (message "unsupported '.guix-channel' version"))
|
||||||
|
@ -169,7 +181,7 @@ doesn't exist."
|
||||||
read-channel-metadata))
|
read-channel-metadata))
|
||||||
(lambda args
|
(lambda args
|
||||||
(if (= ENOENT (system-error-errno args))
|
(if (= ENOENT (system-error-errno args))
|
||||||
(channel-metadata "/" '())
|
(channel-metadata "/" '() #f)
|
||||||
(apply throw args)))))
|
(apply throw args)))))
|
||||||
|
|
||||||
(define (channel-instance-metadata instance)
|
(define (channel-instance-metadata instance)
|
||||||
|
@ -560,3 +572,98 @@ PROFILE is not a profile created by 'guix pull', return the empty list."
|
||||||
;; Show most recently installed packages last.
|
;; Show most recently installed packages last.
|
||||||
(reverse
|
(reverse
|
||||||
(manifest-entries (profile-manifest profile)))))
|
(manifest-entries (profile-manifest profile)))))
|
||||||
|
|
||||||
|
|
||||||
|
;;;
|
||||||
|
;;; News.
|
||||||
|
;;;
|
||||||
|
|
||||||
|
;; Channel news.
|
||||||
|
(define-record-type <channel-news>
|
||||||
|
(channel-news entries)
|
||||||
|
channel-news?
|
||||||
|
(entries channel-news-entries)) ;list of <channel-news-entry>
|
||||||
|
|
||||||
|
;; News entry, associated with a specific commit of the channel.
|
||||||
|
(define-record-type <channel-news-entry>
|
||||||
|
(channel-news-entry commit title body)
|
||||||
|
channel-news-entry?
|
||||||
|
(commit channel-news-entry-commit) ;hex string
|
||||||
|
(title channel-news-entry-title) ;list of language tag/string pairs
|
||||||
|
(body channel-news-entry-body)) ;list of language tag/string pairs
|
||||||
|
|
||||||
|
(define (sexp->channel-news-entry entry)
|
||||||
|
"Return the <channel-news-entry> record corresponding to ENTRY, an sexp."
|
||||||
|
(define (pair language message)
|
||||||
|
(cons (symbol->string language) message))
|
||||||
|
|
||||||
|
(match entry
|
||||||
|
(('entry ('commit commit)
|
||||||
|
('title ((? symbol? title-tags) (? string? titles)) ...)
|
||||||
|
('body ((? symbol? body-tags) (? string? bodies)) ...)
|
||||||
|
_ ...)
|
||||||
|
(channel-news-entry commit
|
||||||
|
(map pair title-tags titles)
|
||||||
|
(map pair body-tags bodies)))
|
||||||
|
(_
|
||||||
|
(raise (condition
|
||||||
|
(&message (message "invalid channel news entry"))
|
||||||
|
(&error-location
|
||||||
|
(location (source-properties->location
|
||||||
|
(source-properties entry)))))))))
|
||||||
|
|
||||||
|
(define (read-channel-news port)
|
||||||
|
"Read a channel news feed from PORT and return it as a <channel-news>
|
||||||
|
record."
|
||||||
|
(match (false-if-exception (read port))
|
||||||
|
(('channel-news ('version 0) entries ...)
|
||||||
|
(channel-news (map sexp->channel-news-entry entries)))
|
||||||
|
(('channel-news ('version version) _ ...)
|
||||||
|
;; This is an unsupported version from the future. There's nothing wrong
|
||||||
|
;; with that (the user may simply need to upgrade the 'guix' channel to
|
||||||
|
;; be able to read it), so silently ignore it.
|
||||||
|
(channel-news '()))
|
||||||
|
(#f
|
||||||
|
(raise (condition
|
||||||
|
(&message (message "syntactically invalid channel news file")))))
|
||||||
|
(sexp
|
||||||
|
(raise (condition
|
||||||
|
(&message (message "invalid channel news file"))
|
||||||
|
(&error-location
|
||||||
|
(location (source-properties->location
|
||||||
|
(source-properties sexp)))))))))
|
||||||
|
|
||||||
|
(define* (channel-news-for-commit channel new #:optional old)
|
||||||
|
"Return a list of <channel-news-entry> for CHANNEL between commits OLD and
|
||||||
|
NEW. When OLD is omitted or is #f, return all the news entries of CHANNEL."
|
||||||
|
(catch 'git-error
|
||||||
|
(lambda ()
|
||||||
|
(let* ((checkout (update-cached-checkout (channel-url channel)
|
||||||
|
#:ref `(commit . ,new)))
|
||||||
|
(metadata (read-channel-metadata-from-source checkout))
|
||||||
|
(news-file (channel-metadata-news-file metadata))
|
||||||
|
(news-file (and news-file
|
||||||
|
(string-append checkout "/" news-file))))
|
||||||
|
(if (and news-file (file-exists? news-file))
|
||||||
|
(let ((entries (channel-news-entries (call-with-input-file news-file
|
||||||
|
read-channel-news))))
|
||||||
|
(if old
|
||||||
|
(with-repository checkout repository
|
||||||
|
(let* ((new (commit-lookup repository (string->oid new)))
|
||||||
|
(old (commit-lookup repository (string->oid old)))
|
||||||
|
(commits (list->set
|
||||||
|
(map (compose oid->string commit-id)
|
||||||
|
(commit-difference new old)))))
|
||||||
|
(filter (lambda (entry)
|
||||||
|
(set-contains? commits
|
||||||
|
(channel-news-entry-commit entry)))
|
||||||
|
entries)))
|
||||||
|
entries))
|
||||||
|
'())))
|
||||||
|
(lambda (key error . rest)
|
||||||
|
;; If commit NEW or commit OLD cannot be found, then something must be
|
||||||
|
;; wrong (for example, the history of CHANNEL was rewritten and these
|
||||||
|
;; commits no longer exist upstream), so quietly return the empty list.
|
||||||
|
(if (= GIT_ENOTFOUND (git-error-code error))
|
||||||
|
'()
|
||||||
|
(apply throw key error rest)))))
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
(define-module (guix tests git)
|
(define-module (guix tests git)
|
||||||
#:use-module (git)
|
#:use-module (git)
|
||||||
|
#:use-module ((guix git) #:select (with-repository))
|
||||||
#:use-module (guix utils)
|
#:use-module (guix utils)
|
||||||
#:use-module (guix build utils)
|
#:use-module (guix build utils)
|
||||||
#:use-module (ice-9 match)
|
#:use-module (ice-9 match)
|
||||||
|
@ -55,7 +56,11 @@ Return DIRECTORY on success."
|
||||||
(mkdir-p (dirname file))
|
(mkdir-p (dirname file))
|
||||||
(call-with-output-file file
|
(call-with-output-file file
|
||||||
(lambda (port)
|
(lambda (port)
|
||||||
(display contents port)))
|
(display (if (string? contents)
|
||||||
|
contents
|
||||||
|
(with-repository directory repository
|
||||||
|
(contents repository)))
|
||||||
|
port)))
|
||||||
(git "add" file)
|
(git "add" file)
|
||||||
(loop rest)))
|
(loop rest)))
|
||||||
((('commit text) rest ...)
|
((('commit text) rest ...)
|
||||||
|
|
|
@ -28,6 +28,10 @@
|
||||||
#:use-module (guix gexp)
|
#:use-module (guix gexp)
|
||||||
#:use-module ((guix utils)
|
#:use-module ((guix utils)
|
||||||
#:select (error-location? error-location location-line))
|
#:select (error-location? error-location location-line))
|
||||||
|
#:use-module ((guix build utils) #:select (which))
|
||||||
|
#:use-module (git)
|
||||||
|
#:use-module (guix git)
|
||||||
|
#:use-module (guix tests git)
|
||||||
#:use-module (srfi srfi-1)
|
#:use-module (srfi srfi-1)
|
||||||
#:use-module (srfi srfi-26)
|
#:use-module (srfi srfi-26)
|
||||||
#:use-module (srfi srfi-34)
|
#:use-module (srfi srfi-34)
|
||||||
|
@ -246,4 +250,99 @@
|
||||||
(depends? drv3
|
(depends? drv3
|
||||||
(list drv2 drv0) (list))))))))
|
(list drv2 drv0) (list))))))))
|
||||||
|
|
||||||
|
(unless (which (git-command)) (test-skip 1))
|
||||||
|
(test-equal "channel-news, no news"
|
||||||
|
'()
|
||||||
|
(with-temporary-git-repository directory
|
||||||
|
'((add "a.txt" "A")
|
||||||
|
(commit "the commit"))
|
||||||
|
(with-repository directory repository
|
||||||
|
(let ((channel (channel (url (string-append "file://" directory))
|
||||||
|
(name 'foo)))
|
||||||
|
(latest (reference-name->oid repository "HEAD")))
|
||||||
|
(channel-news-for-commit channel (oid->string latest))))))
|
||||||
|
|
||||||
|
(unless (which (git-command)) (test-skip 1))
|
||||||
|
(test-assert "channel-news, one entry"
|
||||||
|
(with-temporary-git-repository directory
|
||||||
|
`((add ".guix-channel"
|
||||||
|
,(object->string
|
||||||
|
'(channel (version 0)
|
||||||
|
(news-file "news.scm"))))
|
||||||
|
(commit "first commit")
|
||||||
|
(add "src/a.txt" "A")
|
||||||
|
(commit "second commit")
|
||||||
|
(add "news.scm"
|
||||||
|
,(lambda (repository)
|
||||||
|
(let ((previous
|
||||||
|
(reference-name->oid repository "HEAD")))
|
||||||
|
(object->string
|
||||||
|
`(channel-news
|
||||||
|
(version 0)
|
||||||
|
(entry (commit ,(oid->string previous))
|
||||||
|
(title (en "New file!")
|
||||||
|
(eo "Nova dosiero!"))
|
||||||
|
(body (en "Yeah, a.txt."))))))))
|
||||||
|
(commit "third commit")
|
||||||
|
(add "src/b.txt" "B")
|
||||||
|
(commit "fourth commit")
|
||||||
|
(add "news.scm"
|
||||||
|
,(lambda (repository)
|
||||||
|
(let ((second
|
||||||
|
(commit-id
|
||||||
|
(find-commit repository "second commit")))
|
||||||
|
(previous
|
||||||
|
(reference-name->oid repository "HEAD")))
|
||||||
|
(object->string
|
||||||
|
`(channel-news
|
||||||
|
(version 0)
|
||||||
|
(entry (commit ,(oid->string previous))
|
||||||
|
(title (en "Another file!"))
|
||||||
|
(body (en "Yeah, b.txt.")))
|
||||||
|
(entry (commit ,(oid->string second))
|
||||||
|
(title (en "Old news.")
|
||||||
|
(eo "Malnovaĵoj."))
|
||||||
|
(body (en "For a.txt"))))))))
|
||||||
|
(commit "fifth commit"))
|
||||||
|
(with-repository directory repository
|
||||||
|
(define (find-commit* message)
|
||||||
|
(oid->string (commit-id (find-commit repository message))))
|
||||||
|
|
||||||
|
(let ((channel (channel (url (string-append "file://" directory))
|
||||||
|
(name 'foo)))
|
||||||
|
(commit1 (find-commit* "first commit"))
|
||||||
|
(commit2 (find-commit* "second commit"))
|
||||||
|
(commit3 (find-commit* "third commit"))
|
||||||
|
(commit4 (find-commit* "fourth commit"))
|
||||||
|
(commit5 (find-commit* "fifth commit")))
|
||||||
|
;; First try fetching all the news up to a given commit.
|
||||||
|
(and (null? (channel-news-for-commit channel commit2))
|
||||||
|
(lset= string=?
|
||||||
|
(map channel-news-entry-commit
|
||||||
|
(channel-news-for-commit channel commit5))
|
||||||
|
(list commit2 commit4))
|
||||||
|
(lset= equal?
|
||||||
|
(map channel-news-entry-title
|
||||||
|
(channel-news-for-commit channel commit5))
|
||||||
|
'((("en" . "Another file!"))
|
||||||
|
(("en" . "Old news.") ("eo" . "Malnovaĵoj."))))
|
||||||
|
(lset= string=?
|
||||||
|
(map channel-news-entry-commit
|
||||||
|
(channel-news-for-commit channel commit3))
|
||||||
|
(list commit2))
|
||||||
|
|
||||||
|
;; Now fetch news entries that apply to a commit range.
|
||||||
|
(lset= string=?
|
||||||
|
(map channel-news-entry-commit
|
||||||
|
(channel-news-for-commit channel commit3 commit1))
|
||||||
|
(list commit2))
|
||||||
|
(lset= string=?
|
||||||
|
(map channel-news-entry-commit
|
||||||
|
(channel-news-for-commit channel commit5 commit3))
|
||||||
|
(list commit4))
|
||||||
|
(lset= string=?
|
||||||
|
(map channel-news-entry-commit
|
||||||
|
(channel-news-for-commit channel commit5 commit1))
|
||||||
|
(list commit4 commit2)))))))
|
||||||
|
|
||||||
(test-end "channels")
|
(test-end "channels")
|
||||||
|
|
Loading…
Reference in New Issue