#!/usr/bin/env -S guix repl -- !# ;; Copyright (C) 2024 Denis 'GNUtoo' Carikli ;; ;; This program 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. ;; ;; This program 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 this program. If not, see . (use-modules (ice-9 rdelim)) (use-modules (ice-9 regex)) (use-modules (srfi srfi-1)) (use-modules (srfi srfi-9)) (use-modules (srfi srfi-19)) (define (startswith str value) (if (> (string-length str) (string-length value)) (string=? (substring str 0 (string-length value)) value) #f)) (define (read-file path func) (define results #f) (let ((port (open-input-file path))) (set! results (func path port)) (close-port port) results)) (define (print-patch-name path) (define dashes (string-append (string-join (make-list (string-length path) "-") "") "\n")) (display dashes) (display (string-append path "\n")) (display dashes)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; ;; Patch parsing logic ;; ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-record-type (make-rule name default line-match line end) rule? (name rule-name) ;; Name of the rule (default rule-default) ;; Runs once at the beginning, inconditionally (line-match rule-line-match) ;; Runs each line, returns true/false (line rule-line) ;; Runs if rule-line-match is true (end rule-end)) ;; Runs once at the end, inconditionally (define parse-rules (list ;; Here's an example of a parse rule below. Since it runs each time it is ;; also tested. TODO: A a proper unit testing environment needs to ;; be added and then this could be moved to only run inside that ;; separate testing environment. (make-rule "Example empty rule" (lambda (results) results) (lambda (line _ results) #t) (lambda (line _ results) results) (lambda (path _ results) results)) (make-rule "Count lines" (lambda (results) (acons 'line 0 results)) (lambda (line _ results) #t) (lambda (line _ results) (acons 'line (+ 1 (assq-ref results 'line)) results)) (lambda (path _ results) results)) (make-rule "Find diff start" (lambda (results) results) (lambda (line _ results) (startswith line "diff --git ")) (lambda (line _ results) (acons 'diff-start (assq-ref results 'line) results)) (lambda (path _ results) results)) (make-rule "Retrieve Signed-off-by" (lambda (results) (acons 'signed-off-by '() results)) (lambda (line _ results) (startswith line "Signed-off-by: ")) (lambda (line _ results) (let ((signed-off-by (string-join (cdr (string-split line #\ )) " "))) (acons 'signed-off-by (append (assq-ref results 'signed-off-by) (list signed-off-by)) results))) (lambda (path _ results) results)) ;; TODO: Raise an exception if there is no lines with From:, and ;; when handling it, complain that the file is not a valid git ;; patch. (make-rule "Find commit author" (lambda (results) results) (lambda (line _ results) (startswith line "From: ")) (lambda (line _ results) (let ((commit-author (string-join (cdr (string-split line #\ )) " "))) (acons 'commit-author commit-author results))) (lambda (path _ results) results)) (make-rule "Find commit hash" (lambda (results) results) (lambda (line _ results) ;; Example: ;; From 0df4fe5fadfb7f51c1c34dad10ca9e6e04c3fa18 Mon Sep 17 00:00:00 2001 (and (not (startswith line "From: ")) (startswith line "From "))) (lambda (line _ results) (let ((commit-hash (list-ref (string-split line #\ ) 1))) (acons 'commit-hash commit-hash results))) (lambda (path _ results) results)) ;; TODO: Raise an exception if there is no lines with Date:, and ;; when handling it, complain that the file is not a valid git ;; patch. (make-rule "Find commit date" (lambda (results) results) (lambda (line _ results) (startswith line "Date: ")) (lambda (line _ results) (acons 'commit-date (string->date (string-join (cdr (string-split line #\ )) " ") "~a, ~d ~b ~Y ~H:~M:~S ~z") results)) (lambda (path _ results) results)) ;; TODO: ;; - In general we might want to have the commit summary instead of ;; the subject, but for now we will use the mail subject instead ;; as we don't use the summary yet and properly parsing the ;; subject would require to reimplement the cleanup_subject ;; function from mailinfo.c in git source code. ;; - Raise an exception if there is no lines with From:, and when ;; handling it, complain that the file is not a valid git patch. (make-rule "Find patch subject" (lambda (results) results) (lambda (line _ results) (startswith line "Subject: ")) (lambda (line _ results) (let ((commit-subject (string-join (cdr (string-split line #\ )) " "))) (acons 'commit-subject-line (assq-ref results 'line) (acons 'commit-subject commit-subject results)))) (lambda (path _ results) results)) (make-rule "Find commit subject and message separator" (lambda (results) results) (lambda (line _ results) ;; TODO: Raise an exception if the line after the commit subject ;; line is not empty, and when handling it, complain that the ;; file is not a valid git patch. (and (not (assq-ref results 'commit-message-end-line)) (assq-ref results 'commit-subject) (string=? line "") (eq? (+ 1 (assq-ref results 'commit-subject-line)) (assq-ref results 'line)))) (lambda (line _ results) (acons 'commit-subject-message-separator-line (assq-ref results 'line) results)) (lambda (path _ results) results)) ;; TODO: Raise an exception if there is more than two lines with ;; ---, and when handling it, complain that the file is not a valid ;; git patch. (make-rule "Find changelog end" (lambda (results) results) (lambda (line _ results) (and (assq-ref results 'commit-message-end-line) (string=? line "---"))) (lambda (line _ results) (acons 'changelog-end-line (assq-ref results 'line) results)) (lambda (path _ results) results)) ;; TODO: Raise an exception if there is no line with ---, and when ;; handling it, complain that the file is not a valid git patch. (make-rule "Find commit message end" (lambda (results) results) (lambda (line _ results) ;; This matches the first "---" but there could be more as shown ;; in the example below: ;; --- ;; ChangeLog: [...] ;; --- ;; So until found we are in the commit message, but after it is found ;; we could also be in the ChangeLog. (and (string=? line "---") (not (assq-ref results 'commit-message-end-line)))) (lambda (line _ results) (acons 'commit-message-end-line (assq-ref results 'line) results)) (lambda (path _ results) results)) (make-rule "Find the end of the commit" (lambda (results) results) (lambda (line _ results) #f) (lambda (line _ results) results) (lambda (path _ results) (acons 'commit-end-line (if (assq-ref results 'changelog-end-line) (assq-ref results 'changelog-end-line) (assq-ref results 'commit-message-end-line)) results))) (make-rule "Find commit message" (lambda (results) results) (lambda (line _ results) (and (not (assq-ref results 'commit-message-end-line)) (assq-ref results 'commit-subject-message-separator-line) (> (assq-ref results 'line) (assq-ref results 'commit-subject-message-separator-line)))) (lambda (line _ results) (let ((commit-message (if (not (assq-ref results 'commit-message)) (list) (append (assq-ref results 'commit-message) (list line))))) (acons 'commit-message commit-message results))) (lambda (path _ results) results)) (make-rule "Find added files" (lambda (results) (acons 'added-files '() results)) (lambda (line _ results) (and (startswith line " create mode "))) (lambda (line _ results) (define line-parts (string-split line #\space)) (define added-file '()) (if (> (length line-parts) 3) (set! added-file (list (list-ref line-parts 4)))) (acons 'added-files (append (assq-ref results 'added-files) added-file) results)) (lambda (path _ results) results)) (make-rule "Find deleted files" (lambda (results) (acons 'deleted-files '() results)) (lambda (line _ results) (and (startswith line " delete mode "))) (lambda (line _ results) (define line-parts (string-split line #\space)) (define deleted-file '()) (if (> (length line-parts) 3) (set! deleted-file (list (list-ref line-parts 4)))) (acons 'deleted-files (append (assq-ref results 'deleted-files) deleted-file) results)) (lambda (path _ results) results)) (make-rule "Find modified files and track current file diff" (lambda (results) (acons 'current-diff-file #f (acons 'modified-files '() results))) (lambda (line _ results) (startswith line "diff --git a/")) (lambda (line _ results) (define line-parts (string-split line #\space)) (define current-diff-file #f) (define modified-file '()) (if (> (length line-parts) 3) ;; Example: b/www/x60t_unbrick/0009.JPG (let* ((part3 (list-ref line-parts 3)) ;; remove the b/ (path (substring part3 2 (string-length part3)))) (set! current-diff-file path) (if (not (or (any (lambda (added-file-path) (string=? added-file-path path)) (assq-ref results 'added-files)) (any (lambda (deleted-file-path) (string=? deleted-file-path path)) (assq-ref results 'deleted-files)))) (set! modified-file (list path))))) (acons 'modified-files (append (assq-ref results 'modified-files) modified-file) (acons 'current-diff-file current-diff-file results))) (lambda (path _ results) results)) (make-rule "Track diff" (lambda (results) results) (lambda (line _ results) #t) (lambda (line _ results) (define diff-start 0) (define diff-end 0) (if (and (assq-ref results 'current-diff-file) (startswith line "@@")) (set! diff-start (assq-ref results 'line))) (if (startswith line "diff --git a/") (set! diff-end (assq-ref results 'line))) (if (and (not (eq? diff-start 0)) (not (eq? diff-end 0))) (acons 'diff-end diff-end (acons 'diff-start diff-start results)) (if (not (eq? diff-start 0)) (acons 'diff-start diff-start results) (acons 'diff-end diff-end results)))) (lambda (path _ results) results)) (make-rule "Check for copyrights inside the patch" (lambda (results) (acons 'diff-path-added-proper-copyright '() results)) (lambda (line _ results) (and (startswith line "+") (assq-ref results 'current-diff-file) (> (assq-ref results 'diff-start) 0))) (lambda (line _ results) (let ((diff-start (assq-ref results 'diff-start)) (diff-end (assq-ref results 'diff-end)) (current-diff-file (assq-ref results 'current-diff-file)) (commit-author (assq-ref results 'commit-author)) (commit-year (date-year (assq-ref results 'commit-date)))) ;; Example: Copyright (C) 2024 Some Name (if (string-match (string-append "Copyright[ ]\\(C\\)[ ]" ;"Copyright (C) " ".*" ;We can have multiple years (number->string commit-year 10) ;Year ".*" ;We can have multiple years " " ;We have at least 1 space before the author line commit-author) line) (acons 'diff-path-added-proper-copyright (append (assq-ref results 'diff-path-added-proper-copyright) (list current-diff-file)) results) results))) (lambda (path _ results) results)) ;; We can also use rules for debugging the code, here are two ;; examples below. ;; (make-rule ;; "Debug: print lines." ;; (lambda (results) results) ;; (lambda (line _ results) #t) ;; (lambda (line _ results) ;; (display "Count lines: line #") ;; (display (+ 1 (assq-ref results 'line))) ;; (display (string-append ": " line "\n")) ;; results) ;; (lambda (path _ results) results)) ;; (make-rule ;; "Debug: print results." ;; (lambda (results) results) ;; (lambda (line _ results) #f) ;; (lambda (line _ results) results) ;; (lambda (path _ results) ;; (pk results) ;; results)) )) (define (set-defaults rules results) (for-each (lambda (rule) (set! results ((rule-default rule) results))) rules) results) (define (run-line-match-rules port rules parse-results results) (define line (read-line port)) (if (eof-object? line) results ((lambda _ (for-each (lambda (rule) (if ((rule-line-match rule) line parse-results results) (set! results ((rule-line rule) line parse-results results)))) rules) (run-line-match-rules port rules parse-results results))))) (define (run-end-rules path rules other-results results) (for-each (lambda (rule) (set! results ((rule-end rule) path other-results results))) rules) results) (define (run-parse-rules rules path) (read-file path (lambda (path port) (let* ((defaults (set-defaults rules '())) (results (run-line-match-rules port rules #f defaults))) (run-end-rules path rules #f results))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; ;; ;;;;;;;;;;;; ;; ;; ;; Checks ;; ;; ;; ;;;;;;;;;;;; ;; ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (run-check-rules parse-results rules path) (read-file path (lambda (path port) (let* ((defaults (set-defaults rules '())) (check-results (run-line-match-rules port rules parse-results defaults))) (run-end-rules path rules parse-results check-results))))) (define check-rules (list ;; Here's an example of a check rule below. Since it runs each time it is ;; also tested. TODO: A a proper unit testing environment needs to ;; be added and then this could be moved to only run inside that ;; separate testing environment. (make-rule "Example empty rule" (lambda (check-results) check-results) (lambda (line parse-results check-results) #t) (lambda (line parse-results check-results) check-results) (lambda (path parse-results check-results) check-results)) (make-rule "Count lines" (lambda (check-results) (acons 'line 0 check-results)) (lambda (line parse-results check-results) #t) (lambda (line parse-results check-results) (acons 'line (+ 1 (assq-ref check-results 'line)) check-results)) (lambda (path parse-results check-results) check-results)) ;; Workarround for the bug #66268 ;; [1]https://debbugs.gnu.org/cgi/bugreport.cgi?bug=66268 (make-rule "Enforce commit size < 4KB" (lambda (check-results) (acons 'commit-size 0 check-results)) (lambda (line parse-results check-results) (< (assq-ref check-results 'line) (assq-ref parse-results 'commit-end-line))) (lambda (line parse-results check-results) (acons 'commit-size (+ 1 ;; for the \n (string-length line) (assq-ref check-results 'commit-size)) check-results)) (lambda (path parse-results check-results) ;; We're not sure of the exact size limit so let's use 2500 ;; instead of 4096, since we're not counting signatures etc (let ((limit 2500) (commit-size (assq-ref check-results 'commit-size))) (if (>= commit-size limit) ((lambda _ (display (string-append "ERROR: Commit size is " (number->string commit-size) " B" " which is over the " (number->string limit) " B limit\n\n")) (acons 'errors (+ 1 (assq-ref check-results 'errors)) check-results))) check-results)))) (make-rule "Check for Signed-off-by" (lambda (check-results) check-results) (lambda (line parse-results check-results) #t) (lambda (line parse-results check-results) check-results) (lambda (path parse-results check-results) (let ((author (assq-ref parse-results 'commit-author))) (if (not (any (lambda (elm) (string=? author elm)) (assq-ref parse-results 'signed-off-by))) ((lambda _ (display (string-append "ERROR: Missing Signed-off-by: " author "\n\n")) (acons 'errors (+ 1 (assq-ref check-results 'errors)) check-results))) check-results)))) (make-rule "Track total errors and warnings" (lambda (check-results) (acons 'warnings 0 (acons 'errors 0 check-results))) (lambda (line parse-results check-results) #t) (lambda (line parse-results check-results) check-results) (lambda (path parse-results check-results) (let* ((nr-lines (number->string (assq-ref parse-results 'line) 10)) (errors (assq-ref check-results 'errors)) (warnings (assq-ref check-results 'warnings)) (error-text (string-append (number->string errors 10) (if (> errors 1) " errors, " " error, "))) (warning-text (string-append (number->string warnings 10) (if (> warnings 1) " warnings, " " warning, ")))) (display (string-append "total: " error-text warning-text nr-lines " lines checked\n\n")) (if (or (> errors 0) (> warnings 0)) ((lambda _ (display (string-append path " has style problems, please review.\n")) (display (string-append "NOTE: If any of the errors are false positives, " "please report them to the GNU Boot maintainers.\n")))) (display (string-append path " has no obvious style problems " "and is ready for submission.\n")))) check-results)))) (define (test-patch path) (let* ((parse-results (run-parse-rules parse-rules path)) (check-results (run-check-rules parse-results check-rules path))) parse-results)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; ;; Command line parsing handlig ;; ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; error if not in git tree and in topdir. (define (in-tree-topdir?) (string=? (canonicalize-path (getcwd)) (dirname (dirname (current-filename))))) (define (usage progname exit-code) (display (string-append "Usage: " progname " [path/to/file.patch [path/to/file.patch ...]]\n")) (exit exit-code)) (if (eq? (length (program-arguments)) 1) (usage "checkpatch.pl" 64) ;; 64 is EX_USAGE in sysexits.h (if (not (in-tree-topdir?)) ((lambda _ (display (string-append "Error: please run checkpatch.scm in the git top directory.\n")) (exit 69))) ;; 69 is EX_UNAVAILABLE in sysexits.h (map (lambda (path) (if (> (length (program-arguments)) 2) (print-patch-name path)) (test-patch path)) (cdr (program-arguments)))))