guix/etc/committer.scm.in
Ricardo Wurmus 76a841cc8b
etc: Add committer script.
* etc/committer.scm.in: New file.
* configure.ac: Configure it.
2020-06-16 11:32:53 +02:00

251 lines
9.3 KiB
Scheme
Executable File

#!@GUILE@ \
--no-auto-compile -s
!#
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2020 Ricardo Wurmus <rekado@elephly.net>
;;;
;;; 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/>.
;;; Commentary:
;; This script stages and commits changes to package definitions.
;;; Code:
(import (sxml xpath)
(srfi srfi-1)
(srfi srfi-9)
(ice-9 format)
(ice-9 popen)
(ice-9 match)
(ice-9 rdelim)
(ice-9 textual-ports))
(define (read-excursion port)
"Read an expression from PORT and reset the port position before returning
the expression."
(let ((start (ftell port))
(result (read port)))
(seek port start SEEK_SET)
result))
(define (surrounding-sexp port line-no)
"Return the top-level S-expression surrounding the change at line number
LINE-NO in PORT."
(let loop ((i (1- line-no))
(last-top-level-sexp #f))
(if (zero? i)
last-top-level-sexp
(match (peek-char port)
(#\(
(let ((sexp (read-excursion port)))
(read-line port)
(loop (1- i) sexp)))
(_
(read-line port)
(loop (1- i) last-top-level-sexp))))))
(define-record-type <hunk>
(make-hunk file-name
old-line-number
new-line-number
diff)
hunk?
(file-name hunk-file-name)
;; Line number before the change
(old-line-number hunk-old-line-number)
;; Line number after the change
(new-line-number hunk-new-line-number)
;; The full diff to be used with "git apply --cached"
(diff hunk-diff))
(define* (hunk->patch hunk #:optional (port (current-output-port)))
(let ((file-name (hunk-file-name hunk)))
(format port
"diff --git a/~a b/~a~%--- a/~a~%+++ b/~a~%~a"
file-name file-name file-name file-name
(hunk-diff hunk))))
(define (diff-info)
"Read the diff and return a list of <hunk> values."
(let ((port (open-pipe* OPEN_READ
"git" "diff"
"--no-prefix"
;; Do not include any context lines. This makes it
;; easier to find the S-expression surrounding the
;; change.
"--unified=0")))
(define (extract-line-number line-tag)
(abs (string->number
(car (string-split line-tag #\,)))))
(define (read-hunk)
(reverse
(let loop ((lines '()))
(let ((line (read-line port 'concat)))
(cond
((eof-object? line) lines)
((or (string-prefix? "@@ " line)
(string-prefix? "diff --git" line))
(unget-string port line)
lines)
(else (loop (cons line lines))))))))
(define info
(let loop ((acc '())
(file-name #f))
(let ((line (read-line port)))
(cond
((eof-object? line) acc)
((string-prefix? "--- " line)
(match (string-split line #\space)
((_ file-name)
(loop acc file-name))))
((string-prefix? "@@ " line)
(match (string-split line #\space)
((_ old-start new-start . _)
(loop (cons (make-hunk file-name
(extract-line-number old-start)
(extract-line-number new-start)
(string-join (cons* line "\n"
(read-hunk)) ""))
acc)
file-name))))
(else (loop acc file-name))))))
(close-pipe port)
info))
(define (old-sexp hunk)
"Using the diff information in HUNK return the unmodified S-expression
corresponding to the top-level definition containing the staged changes."
;; TODO: We can't seek with a pipe port...
(let* ((port (open-pipe* OPEN_READ
"git" "show" (string-append "HEAD:"
(hunk-file-name hunk))))
(contents (get-string-all port)))
(close-pipe port)
(call-with-input-string contents
(lambda (port)
(surrounding-sexp port (hunk-old-line-number hunk))))))
(define (new-sexp hunk)
"Using the diff information in HUNK return the modified S-expression
corresponding to the top-level definition containing the staged changes."
(call-with-input-file (hunk-file-name hunk)
(lambda (port)
(surrounding-sexp port
(hunk-new-line-number hunk)))))
(define* (commit-message file-name old new #:optional (port (current-output-port)))
"Print ChangeLog commit message for changes between OLD and NEW."
(define (get-values expr field)
(match ((sxpath `(// ,field quasiquote *)) expr)
(() '())
((first . rest)
(map cadadr first))))
(define (listify items)
(match items
((one) one)
((one two)
(string-append one " and " two))
((one two . more)
(string-append (string-join (drop-right items 1) ", ")
", and " (first (take-right items 1))))))
(define variable-name
(second old))
(define version
(and=> ((sxpath '(// version *any*)) new)
first))
(format port
"gnu: ~a: Update to ~a.~%~%* ~a (~a): Update to ~a.~%"
variable-name version file-name variable-name version)
(for-each (lambda (field)
(let ((old-values (get-values old field))
(new-values (get-values new field)))
(or (equal? old-values new-values)
(let ((removed (lset-difference eq? old-values new-values))
(added (lset-difference eq? new-values old-values)))
(format port
"[~a]: ~a~%" field
(match (list (map symbol->string removed)
(map symbol->string added))
((() added)
(format #f "Add ~a."
(listify added)))
((removed ())
(format #f "Remove ~a."
(listify removed)))
((removed added)
(format #f "Remove ~a; add ~a."
(listify removed)
(listify added)))))))))
'(inputs propagated-inputs native-inputs)))
(define (group-hunks-by-sexp hunks)
"Return a list of pairs associating all hunks with the S-expression they are
modifying."
(fold (lambda (sexp hunk acc)
(match acc
(((previous-sexp . hunks) . rest)
(if (equal? sexp previous-sexp)
(cons (cons previous-sexp
(cons hunk hunks))
rest)
(cons (cons sexp (list hunk))
acc)))
(_
(cons (cons sexp (list hunk))
acc))))
'()
(map new-sexp hunks)
hunks))
(define (new+old+hunks hunks)
(map (match-lambda
((new . hunks)
(cons* new (old-sexp (first hunks)) hunks)))
(group-hunks-by-sexp hunks)))
(define (main . args)
(match (diff-info)
(()
(display "Nothing to be done." (current-error-port)))
(hunks
(for-each (match-lambda
((new old . hunks)
(for-each (lambda (hunk)
(let ((port (open-pipe* OPEN_WRITE
"git" "apply"
"--cached"
"--unidiff-zero")))
(hunk->patch hunk port)
(unless (eqv? 0 (status:exit-val (close-pipe port)))
(error "Cannot apply")))
(sleep 1))
hunks)
(commit-message (hunk-file-name (first hunks))
old new
(current-output-port))
(let ((port (open-pipe* OPEN_WRITE "git" "commit" "-F" "-")))
(commit-message (hunk-file-name (first hunks))
old new
port)
(sleep 1)
(unless (eqv? 0 (status:exit-val (close-pipe port)))
(error "Cannot commit")))))
(new+old+hunks hunks)))))
(main)