lint: Check for trailing whitespace in synopsis.
[jackhill/guix/guix.git] / etc / committer.scm.in
index ebe6b96..96cd1fb 100755 (executable)
@@ -3,7 +3,7 @@
 !#
 
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2020 Ricardo Wurmus <rekado@elephly.net>
+;;; Copyright © 2020, 2021 Ricardo Wurmus <rekado@elephly.net>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
 
 (import (sxml xpath)
         (srfi srfi-1)
+        (srfi srfi-2)
         (srfi srfi-9)
+        (srfi srfi-11)
+        (srfi srfi-26)
         (ice-9 format)
         (ice-9 popen)
         (ice-9 match)
         (ice-9 rdelim)
         (ice-9 textual-ports))
 
+(define* (break-string str #:optional (max-line-length 70))
+  "Break the string STR into lines that are no longer than MAX-LINE-LENGTH.
+Return a single string."
+  (define (restore-line words)
+    (string-join (reverse words) " "))
+  (if (<= (string-length str) max-line-length)
+      str
+      (let ((words+lengths (map (lambda (word)
+                                  (cons word (string-length word)))
+                                (string-tokenize str))))
+        (match (fold (match-lambda*
+                       (((word . length)
+                         (count current lines))
+                        (let ((new-count (+ count length 1)))
+                          (if (< new-count max-line-length)
+                              (list new-count
+                                    (cons word current)
+                                    lines)
+                              (list length
+                                    (list word)
+                                    (cons (restore-line current) lines))))))
+                     '(0 () ())
+                     words+lengths)
+          ((_ last-words lines)
+           (string-join (reverse (cons (restore-line last-words) lines))
+                        "\n"))))))
+
 (define (read-excursion port)
   "Read an expression from PORT and reset the port position before returning
 the expression."
@@ -63,7 +93,8 @@ LINE-NO in PORT."
   (make-hunk file-name
              old-line-number
              new-line-number
-             diff)
+             diff-lines
+             definition?)
   hunk?
   (file-name       hunk-file-name)
   ;; Line number before the change
@@ -71,38 +102,45 @@ LINE-NO in PORT."
   ;; 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))
+  (diff-lines hunk-diff-lines)
+  ;; Does this hunk add a definition?
+  (definition? hunk-definition?))
 
 (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))))
+            (string-join (hunk-diff-lines hunk) ""))))
 
 (define (diff-info)
   "Read the diff and return a list of <hunk> values."
   (let ((port (open-pipe* OPEN_READ
-                          "git" "diff"
+                          "git" "diff-files"
                           "--no-prefix"
-                          ;; Do not include any context lines.  This makes it
-                          ;; easier to find the S-expression surrounding the
-                          ;; change.
-                          "--unified=0")))
+                          ;; Only include one context line to avoid lumping in
+                          ;; new definitions with changes to existing
+                          ;; definitions.
+                          "--unified=1"
+                          "gnu")))
     (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))))))))
+      (let loop ((lines '())
+                 (definition? #false))
+        (let ((line (read-line port 'concat)))
+          (cond
+           ((eof-object? line)
+            (values (reverse lines) definition?))
+           ((or (string-prefix? "@@ " line)
+                (string-prefix? "diff --git" line))
+            (unget-string port line)
+            (values (reverse lines) definition?))
+           (else
+            (loop (cons line lines)
+                  (or definition?
+                      (string-prefix? "+(define" line))))))))
     (define info
       (let loop ((acc '())
                  (file-name #f))
@@ -116,29 +154,42 @@ LINE-NO in PORT."
            ((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))))
+               (let-values
+                   (((diff-lines definition?) (read-hunk)))
+                 (loop (cons (make-hunk file-name
+                                        (extract-line-number old-start)
+                                        (extract-line-number new-start)
+                                        (cons (string-append line "\n")
+                                              diff-lines)
+                                        definition?) acc)
+                       file-name)))))
            (else (loop acc file-name))))))
     (close-pipe port)
     info))
 
+(define (lines-to-first-change hunk)
+  "Return the number of diff lines until the first change."
+  (1- (count (lambda (line)
+               ((negate char-set-contains?)
+                (char-set #\+ #\-)
+                (string-ref line 0)))
+             (hunk-diff-lines hunk))))
+
 (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))))
+                           "git" "cat-file" "-p" (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))))))
+        (surrounding-sexp port
+                          (+ (lines-to-first-change hunk)
+                             (hunk-old-line-number hunk)))))))
 
 (define (new-sexp hunk)
   "Using the diff information in HUNK return the modified S-expression
@@ -146,9 +197,10 @@ 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)))))
+                        (+ (lines-to-first-change hunk)
+                           (hunk-new-line-number hunk))))))
 
-(define* (commit-message file-name old new #:optional (port (current-output-port)))
+(define* (change-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)
@@ -179,20 +231,27 @@ corresponding to the top-level definition containing the staged changes."
                           (added (lset-difference equal? 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)))))))))
+                              (break-string
+                               (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* (add-commit-message file-name variable-name #:optional (port (current-output-port)))
+  "Print ChangeLog commit message for a change to FILE-NAME adding a definition."
+  (format port
+          "gnu: Add ~a.~%~%* ~a (~a): New variable.~%"
+          variable-name file-name variable-name))
+
 (define (group-hunks-by-sexp hunks)
   "Return a list of pairs associating all hunks with the S-expression they are
 modifying."
@@ -218,14 +277,45 @@ modifying."
           (cons* new (old-sexp (first hunks)) hunks)))
        (group-hunks-by-sexp hunks)))
 
+(define %delay 1000)
+
 (define (main . args)
   (match (diff-info)
     (()
-     (display "Nothing to be done." (current-error-port)))
+     (display "Nothing to be done.\n" (current-error-port)))
     (hunks
-     (for-each (match-lambda
-                 ((new old . hunks)
-                  (for-each (lambda (hunk)
+     (let-values
+         (((definitions changes)
+           (partition hunk-definition? hunks)))
+
+       ;; Additions.
+       (for-each (lambda (hunk)
+                   (and-let*
+                       ((define-line (find (cut string-prefix? "+(define" <>)
+                                           (hunk-diff-lines hunk)))
+                        (variable-name (and=> (string-tokenize define-line) second)))
+                     (add-commit-message (hunk-file-name hunk) variable-name)
+                     (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")))
+
+                     (let ((port (open-pipe* OPEN_WRITE "git" "commit" "-F" "-")))
+                       (add-commit-message (hunk-file-name hunk)
+                                           variable-name port)
+                       (usleep %delay)
+                       (unless (eqv? 0 (status:exit-val (close-pipe port)))
+                         (error "Cannot commit"))))
+                   (usleep %delay))
+                 definitions)
+
+       ;; Changes.
+       (for-each (match-lambda
+                   ((new old . hunks)
+                    (for-each (lambda (hunk)
                                 (let ((port (open-pipe* OPEN_WRITE
                                                         "git" "apply"
                                                         "--cached"
@@ -233,18 +323,20 @@ modifying."
                                   (hunk->patch hunk port)
                                   (unless (eqv? 0 (status:exit-val (close-pipe port)))
                                     (error "Cannot apply")))
-                                (sleep 1))
+                                (usleep %delay))
                               hunks)
-                    (commit-message (hunk-file-name (first hunks))
-                                    old new
-                                    (current-output-port))
+                    (change-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)
+                      (change-commit-message (hunk-file-name (first hunks))
+                                             old new
+                                             port)
+                      (usleep %delay)
                       (unless (eqv? 0 (status:exit-val (close-pipe port)))
                         (error "Cannot commit")))))
-               (new+old+hunks hunks)))))
+                 ;; XXX: we recompute the hunks here because previous
+                 ;; insertions lead to offsets.
+                 (new+old+hunks (diff-info)))))))
 
 (main)