5 ;;; GNU Guix --- Functional package management for GNU
6 ;;; Copyright © 2020, 2021 Ricardo Wurmus <rekado@elephly.net>
8 ;;; This file is part of GNU Guix.
10 ;;; GNU Guix is free software; you can redistribute it and/or modify it
11 ;;; under the terms of the GNU General Public License as published by
12 ;;; the Free Software Foundation; either version 3 of the License, or (at
13 ;;; your option) any later version.
15 ;;; GNU Guix is distributed in the hope that it will be useful, but
16 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;;; GNU General Public License for more details.
20 ;;; You should have received a copy of the GNU General Public License
21 ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
25 ;; This script stages and commits changes to package definitions.
39 (ice-9 textual-ports))
41 (define (read-excursion port)
42 "Read an expression from PORT and reset the port position before returning
44 (let ((start (ftell port))
46 (seek port start SEEK_SET)
49 (define (surrounding-sexp port line-no)
50 "Return the top-level S-expression surrounding the change at line number
52 (let loop ((i (1- line-no))
53 (last-top-level-sexp #f))
56 (match (peek-char port)
58 (let ((sexp (read-excursion port)))
63 (loop (1- i) last-top-level-sexp))))))
65 (define-record-type <hunk>
72 (file-name hunk-file-name)
73 ;; Line number before the change
74 (old-line-number hunk-old-line-number)
75 ;; Line number after the change
76 (new-line-number hunk-new-line-number)
77 ;; The full diff to be used with "git apply --cached"
78 (diff-lines hunk-diff-lines)
79 ;; Does this hunk add a definition?
80 (definition? hunk-definition?))
82 (define* (hunk->patch hunk #:optional (port (current-output-port)))
83 (let ((file-name (hunk-file-name hunk)))
85 "diff --git a/~a b/~a~%--- a/~a~%+++ b/~a~%~a"
86 file-name file-name file-name file-name
87 (string-join (hunk-diff-lines hunk) ""))))
90 "Read the diff and return a list of <hunk> values."
91 (let ((port (open-pipe* OPEN_READ
94 ;; Only include one context line to avoid lumping in
95 ;; new definitions with changes to existing
99 (define (extract-line-number line-tag)
101 (car (string-split line-tag #\,)))))
103 (let loop ((lines '())
104 (definition? #false))
105 (let ((line (read-line port 'concat)))
108 (values (reverse lines) definition?))
109 ((or (string-prefix? "@@ " line)
110 (string-prefix? "diff --git" line))
111 (unget-string port line)
112 (values (reverse lines) definition?))
114 (loop (cons line lines)
116 (string-prefix? "+(define" line))))))))
120 (let ((line (read-line port)))
122 ((eof-object? line) acc)
123 ((string-prefix? "--- " line)
124 (match (string-split line #\space)
126 (loop acc file-name))))
127 ((string-prefix? "@@ " line)
128 (match (string-split line #\space)
129 ((_ old-start new-start . _)
131 (((diff-lines definition?) (read-hunk)))
132 (loop (cons (make-hunk file-name
133 (extract-line-number old-start)
134 (extract-line-number new-start)
135 (cons (string-append line "\n")
139 (else (loop acc file-name))))))
143 (define (lines-to-first-change hunk)
144 "Return the number of diff lines until the first change."
145 (1- (count (lambda (line)
146 ((negate char-set-contains?)
148 (string-ref line 0)))
149 (hunk-diff-lines hunk))))
151 (define (old-sexp hunk)
152 "Using the diff information in HUNK return the unmodified S-expression
153 corresponding to the top-level definition containing the staged changes."
154 ;; TODO: We can't seek with a pipe port...
155 (let* ((port (open-pipe* OPEN_READ
156 "git" "show" (string-append "HEAD:"
157 (hunk-file-name hunk))))
158 (contents (get-string-all port)))
160 (call-with-input-string contents
162 (surrounding-sexp port
163 (+ (lines-to-first-change hunk)
164 (hunk-old-line-number hunk)))))))
166 (define (new-sexp hunk)
167 "Using the diff information in HUNK return the modified S-expression
168 corresponding to the top-level definition containing the staged changes."
169 (call-with-input-file (hunk-file-name hunk)
171 (surrounding-sexp port
172 (+ (lines-to-first-change hunk)
173 (hunk-new-line-number hunk))))))
175 (define* (change-commit-message file-name old new #:optional (port (current-output-port)))
176 "Print ChangeLog commit message for changes between OLD and NEW."
177 (define (get-values expr field)
178 (match ((sxpath `(// ,field quasiquote *)) expr)
181 (map cadadr first))))
182 (define (listify items)
186 (string-append one " and " two))
188 (string-append (string-join (drop-right items 1) ", ")
189 ", and " (first (take-right items 1))))))
190 (define variable-name
193 (and=> ((sxpath '(// version *any*)) new)
196 "gnu: ~a: Update to ~a.~%~%* ~a (~a): Update to ~a.~%"
197 variable-name version file-name variable-name version)
198 (for-each (lambda (field)
199 (let ((old-values (get-values old field))
200 (new-values (get-values new field)))
201 (or (equal? old-values new-values)
202 (let ((removed (lset-difference equal? old-values new-values))
203 (added (lset-difference equal? new-values old-values)))
206 (match (list (map symbol->string removed)
207 (map symbol->string added))
212 (format #f "Remove ~a."
215 (format #f "Remove ~a; add ~a."
217 (listify added)))))))))
218 '(inputs propagated-inputs native-inputs)))
220 (define* (add-commit-message file-name variable-name #:optional (port (current-output-port)))
221 "Print ChangeLog commit message for a change to FILE-NAME adding a definition."
223 "gnu: Add ~a.~%~%* ~a (~a): New variable.~%"
224 variable-name file-name variable-name))
226 (define (group-hunks-by-sexp hunks)
227 "Return a list of pairs associating all hunks with the S-expression they are
229 (fold (lambda (sexp hunk acc)
231 (((previous-sexp . hunks) . rest)
232 (if (equal? sexp previous-sexp)
233 (cons (cons previous-sexp
236 (cons (cons sexp (list hunk))
239 (cons (cons sexp (list hunk))
245 (define (new+old+hunks hunks)
248 (cons* new (old-sexp (first hunks)) hunks)))
249 (group-hunks-by-sexp hunks)))
253 (define (main . args)
256 (display "Nothing to be done." (current-error-port)))
259 (((definitions changes)
260 (partition hunk-definition? hunks)))
263 (for-each (lambda (hunk)
265 ((define-line (find (cut string-prefix? "+(define" <>)
266 (hunk-diff-lines hunk)))
267 (variable-name (and=> (string-tokenize define-line) second)))
268 (add-commit-message (hunk-file-name hunk) variable-name)
269 (let ((port (open-pipe* OPEN_WRITE
273 (hunk->patch hunk port)
274 (unless (eqv? 0 (status:exit-val (close-pipe port)))
275 (error "Cannot apply")))
277 (let ((port (open-pipe* OPEN_WRITE "git" "commit" "-F" "-")))
278 (add-commit-message (hunk-file-name hunk)
281 (unless (eqv? 0 (status:exit-val (close-pipe port)))
282 (error "Cannot commit"))))
287 (for-each (match-lambda
289 (for-each (lambda (hunk)
290 (let ((port (open-pipe* OPEN_WRITE
294 (hunk->patch hunk port)
295 (unless (eqv? 0 (status:exit-val (close-pipe port)))
296 (error "Cannot apply")))
299 (change-commit-message (hunk-file-name (first hunks))
301 (current-output-port))
302 (let ((port (open-pipe* OPEN_WRITE "git" "commit" "-F" "-")))
303 (change-commit-message (hunk-file-name (first hunks))
307 (unless (eqv? 0 (status:exit-val (close-pipe port)))
308 (error "Cannot commit")))))
309 ;; XXX: we recompute the hunks here because previous
310 ;; insertions lead to offsets.
311 (new+old+hunks (diff-info)))))))