* guix/channels.scm (latest-channel-instance): Add #:starting-commit and
pass it to 'update-cached-checkout'. Return the commit relation as a
(ensure-forward-channel-update): New procedure.
(latest-channel-instances): Add #:current-channels and #:validate-pull.
[current-commit]: New procedure.
Pass #:starting-commit to 'latest-channel-instance'. When the returned
relation is true, call VALIDATE-PULL.
(latest-channel-derivation): Add #:current-channels and #:validate-pull.
Pass them to 'latest-channel-instances*'.
* tests/channels.scm ("latest-channel-instances #:validate-pull"): New
guix/channels.scm | 89 ++++++++++++++++++++++++++++++++++++++++------
tests/channels.scm | 35 ++++++++++++++++++
2 files changed, 114 insertions(+), 10 deletions(-)
Toggle diff (199 lines)
diff --git a/guix/channels.scm b/guix/channels.scm
index 75b767a94c..70e2d7f07c 100644
@@ -73,6 +73,7 @@
@@ -212,15 +213,18 @@ result is unspecified."
(define* (latest-channel-instance store channel
- #:key (patches %patches))
- "Return the latest channel instance for CHANNEL."
+ #:key (patches %patches)
+ "Return two values: the latest channel instance for CHANNEL, and its
+relation to STARTING-COMMIT when provided."
(define (dot-git? file stat)
(and (string=? (basename file) ".git")
(eq? 'directory (stat:type stat))))
(let-values (((checkout commit relation)
(update-cached-checkout (channel-url channel)
- #:ref (channel-reference channel))))
+ #:ref (channel-reference channel)
+ #:starting-commit starting-commit)))
(when (guix-channel? channel)
;; Apply the relevant subset of PATCHES directly in CHECKOUT. This is
;; safe to do because 'switch-to-ref' eventually does a hard reset.
@@ -229,11 +233,51 @@ result is unspecified."
(let* ((name (url+commit->name (channel-url channel) commit))
(checkout (add-to-store store name #t "sha256" checkout
#:select? (negate dot-git?))))
- (channel-instance channel commit checkout))))
+ (values (channel-instance channel commit checkout)
-(define* (latest-channel-instances store channels)
+(define (ensure-forward-channel-update channel start instance relation)
+ "Raise an error if RELATION is not 'ancestor, meaning that START is not an
+ancestor of the commit in INSTANCE, unless CHANNEL specifies a commit.
+This procedure implements a channel update policy meant to be used as a
+ (match relation
+ ('ancestor #t)
+ ('self #t)
+ (raise (apply make-compound-condition
+ (&message (message
+ (format #f (G_ "\
+aborting update of channel '~a' to commit ~a, which is not a descendant of ~a")
+ (channel-name channel)
+ (channel-instance-commit instance)
+ ;; Don't show the hint when the user explicitly specified a
+ ;; commit in CHANNEL.
+ (if (channel-commit channel)
+ (list (condition
+ (hint (G_ "This could indicate that the channel has
+been tampered with and is trying to force a roll-back, preventing you from
+getting the latest updates. If you think this is not the case, explicitly
+allow non-forward updates.")))))))))))
+(define* (latest-channel-instances store channels
+ (current-channels '())
"Return a list of channel instances corresponding to the latest checkouts of
-CHANNELS and the channels on which they depend."
+CHANNELS and the channels on which they depend.
+CURRENT-CHANNELS is the list of currently used channels. It is compared
+against the newly-fetched instances of CHANNELS, and VALIDATE-PULL is called
+for each channel update and can choose to emit warnings or raise an error,
+depending on the policy it implements."
;; Only process channels that are unique, or that are more specific than a
;; previous channel specification.
(define (ignore? channel others)
@@ -244,6 +288,13 @@ CHANNELS and the channels on which they depend."
(not (or (channel-commit a)
+ (define (current-commit name)
+ ;; Return the current commit for channel NAME.
+ (any (lambda (channel)
+ (and (eq? (channel-name channel) name)
+ (channel-commit channel)))
(let loop ((channels channels)
;; Accumulate a list of instances. A list of processed channels is also
@@ -257,7 +308,15 @@ CHANNELS and the channels on which they depend."
(G_ "Updating channel '~a' from Git repository at '~a'...~%")
- (let ((instance (latest-channel-instance store channel)))
+ (let*-values (((current)
+ (current-commit (channel-name channel)))
+ ((instance relation)
+ (latest-channel-instance store channel
+ (when relation
+ (validate-pull channel current instance relation))
(let-values (((new-instances new-channels)
(loop (channel-instance-dependencies instance)
@@ -617,10 +676,20 @@ channel instances."
-(define* (latest-channel-derivation #:optional (channels %default-channels))
+(define* (latest-channel-derivation #:optional (channels %default-channels)
+ (current-channels '())
"Return as a monadic value the derivation that builds the profile for the
-latest instances of CHANNELS."
- (mlet %store-monad ((instances (latest-channel-instances* channels)))
+latest instances of CHANNELS. CURRENT-CHANNELS and VALIDATE-PULL are passed
+ (mlet %store-monad ((instances
+ (latest-channel-instances* channels
(define (profile-channels profile)
diff --git a/tests/channels.scm b/tests/channels.scm
index 3578b57204..3b141428c8 100644
@@ -37,6 +37,7 @@
#:use-module (srfi srfi-34)
#:use-module (srfi srfi-35)
#:use-module (srfi srfi-64)
+ #:use-module (ice-9 control)
#:use-module (ice-9 match))
@@ -178,6 +179,40 @@
+(unless (which (git-command)) (test-skip 1))
+(test-equal "latest-channel-instances #:validate-pull"
+ ;; Make sure the #:validate-pull procedure receives the right values.
+ (let/ec return
+ (with-temporary-git-repository directory
+ '((add "a.txt" "A")
+ (commit "first commit")
+ (add "b.scm" "#t")
+ (commit "second commit"))
+ (with-repository directory repository
+ (let* ((commit1 (find-commit repository "first"))
+ (commit2 (find-commit repository "second"))
+ (spec (channel (url (string-append "file://" directory))
+ (name 'foo)))
+ (new (channel (inherit spec)
+ (commit (oid->string (commit-id commit2)))))
+ (old (channel (inherit spec)
+ (commit (oid->string (commit-id commit1))))))
+ (define (validate-pull channel current instance relation)
+ (return (and (eq? channel old)
+ (string=? (oid->string (commit-id commit2))
+ (string=? (oid->string (commit-id commit1))
+ (channel-instance-commit instance))
+ (with-store store
+ ;; Attempt a downgrade from NEW to OLD.
+ (latest-channel-instances store (list old)
+ #:current-channels (list new)
+ #:validate-pull validate-pull)))))))
;; Compute the manifest for a graph of instances and make sure we get a
;; derivation graph that mirrors the instance graph. This test also ensures