2021-07-31 23:50:00 -04:00
|
|
|
;;; core/cli/ci.el -*- lexical-binding: t; -*-
|
|
|
|
|
|
|
|
(defcli! ci (&optional target &rest args)
|
|
|
|
"TODO"
|
|
|
|
(unless target
|
|
|
|
(user-error "No CI target given"))
|
|
|
|
(if-let (fn (intern-soft (format "doom-cli--ci-%s" target)))
|
|
|
|
(apply fn args)
|
|
|
|
(user-error "No known CI target: %S" target)))
|
|
|
|
|
|
|
|
|
|
|
|
;;
|
|
|
|
;;;
|
|
|
|
|
|
|
|
|
|
|
|
(defun doom-cli--ci-deploy-hooks ()
|
|
|
|
(let ((dir (doom-path doom-emacs-dir ".git/hooks"))
|
|
|
|
(default-directory doom-emacs-dir))
|
|
|
|
(make-directory dir 'parents)
|
|
|
|
(dolist (hook '("commit-msg"))
|
|
|
|
(let ((file (doom-path dir hook)))
|
|
|
|
(with-temp-file file
|
|
|
|
(insert "#!/usr/bin/env sh\n"
|
|
|
|
(doom-path doom-emacs-dir "bin/doom")
|
|
|
|
" --nocolor ci hook-" hook
|
|
|
|
" \"$@\""))
|
|
|
|
(set-file-modes file #o700)
|
|
|
|
(print! (success "Created %s") (relpath file))))))
|
|
|
|
|
|
|
|
|
|
|
|
;;
|
|
|
|
;;; Git hooks
|
|
|
|
|
|
|
|
(defvar doom-cli-commit-rules
|
2021-09-16 02:43:13 +02:00
|
|
|
(list (fn! (&key subject)
|
|
|
|
(when (<= (length subject) 10)
|
|
|
|
(cons 'error "Subject is too short (<10) and should be more descriptive")))
|
|
|
|
|
|
|
|
(fn! (&key subject type)
|
|
|
|
(unless (memq type '(bump revert))
|
|
|
|
(let ((len (length subject)))
|
|
|
|
(cond ((> len 50)
|
|
|
|
(cons 'warning
|
|
|
|
(format "Subject is %d characters; <=50 is ideal, 72 is max"
|
|
|
|
len)))
|
|
|
|
((> len 72)
|
|
|
|
(cons 'error
|
|
|
|
(format "Subject is %d characters; <=50 is ideal, 72 is max"
|
|
|
|
len)))))))
|
|
|
|
|
|
|
|
(fn! (&key type)
|
|
|
|
(unless (memq type '(bump dev docs feat fix merge module nit perf
|
|
|
|
refactor release revert test tweak))
|
|
|
|
(cons 'error (format "Commit has an invalid type (%s)" type))))
|
|
|
|
|
|
|
|
(fn! (&key summary)
|
|
|
|
(when (or (not (stringp summary))
|
|
|
|
(string-blank-p summary))
|
|
|
|
(cons 'error "Commit has no summary")))
|
|
|
|
|
|
|
|
(fn! (&key summary subject)
|
|
|
|
(and (stringp summary)
|
|
|
|
(string-match-p "^[A-Z][^-]" summary)
|
|
|
|
(not (string-match-p "\\(SPC\\|TAB\\|ESC\\|LFD\\|DEL\\|RET\\)" summary))
|
|
|
|
(cons 'error (format "%S in summary is capitalized; do not capitalize the summary"
|
|
|
|
(car (split-string summary " "))))))
|
|
|
|
|
|
|
|
(fn! (&key type scopes summary)
|
|
|
|
(and (memq type '(bump revert release merge module))
|
|
|
|
scopes
|
|
|
|
(cons 'error
|
|
|
|
(format "Scopes for %s commits should go after the colon, not before"
|
|
|
|
type))))
|
|
|
|
|
|
|
|
(fn! (&key type scopes)
|
|
|
|
(unless (memq type '(bump revert merge module release))
|
|
|
|
(cl-loop with scopes =
|
|
|
|
(cl-loop for path
|
|
|
|
in (cdr (doom-module-load-path (list doom-modules-dir)))
|
|
|
|
for (_category . module)
|
|
|
|
= (doom-module-from-path path)
|
|
|
|
collect (symbol-name module))
|
|
|
|
with extra-scopes = '("cli")
|
|
|
|
with regexp-scopes = '("^&")
|
|
|
|
with type-scopes =
|
|
|
|
(pcase type
|
|
|
|
(`docs
|
|
|
|
(cons "install"
|
|
|
|
(mapcar #'file-name-base
|
|
|
|
(doom-glob doom-docs-dir "[a-z]*.org")))))
|
|
|
|
with scopes-re =
|
|
|
|
(concat (string-join regexp-scopes "\\|")
|
|
|
|
"\\|"
|
|
|
|
(regexp-opt (append type-scopes extra-scopes scopes)))
|
|
|
|
for scope in scopes
|
|
|
|
if (not (string-match scopes-re scope))
|
|
|
|
collect scope into error-scopes
|
|
|
|
finally return
|
|
|
|
(when error-scopes
|
|
|
|
(cons 'error (format "Commit has invalid scope(s): %s"
|
|
|
|
error-scopes))))))
|
|
|
|
|
|
|
|
(fn! (&key scopes)
|
|
|
|
(unless (equal scopes (sort scopes #'string-lessp))
|
|
|
|
(cons 'error "Scopes are not in lexicographical order")))
|
|
|
|
|
|
|
|
(fn! (&key type body)
|
|
|
|
(unless (memq type '(bump revert merge))
|
|
|
|
(catch 'result
|
|
|
|
(with-temp-buffer
|
|
|
|
(save-excursion (insert body))
|
|
|
|
(while (re-search-forward "^[^\n]\\{73,\\}" nil t)
|
|
|
|
;; Exclude ref lines, bump lines, comments, lines with URLs,
|
|
|
|
;; or indented lines
|
|
|
|
(save-excursion
|
|
|
|
(or (let ((bump-re "\\(https?://.+\\|[^/]+\\)/[^/]+@[a-z0-9]\\{12\\}"))
|
|
|
|
(re-search-backward (format "^%s -> %s$" bump-re bump-re) nil t))
|
|
|
|
(re-search-backward "https?://[^ ]+\\{73,\\}" nil t)
|
|
|
|
(re-search-backward "^\\(?:#\\| +\\)" nil t)
|
|
|
|
(throw 'result (cons 'error "Line(s) in commit body exceed 72 characters")))))))))
|
|
|
|
|
|
|
|
(fn! (&key bang body type)
|
|
|
|
(if bang
|
|
|
|
(cond ((not (string-match-p "^BREAKING CHANGE:" body))
|
|
|
|
(cons 'error "'!' present in commit type, but missing 'BREAKING CHANGE:' in body"))
|
|
|
|
((not (string-match-p "^BREAKING CHANGE: .+" body))
|
|
|
|
(cons 'error "'BREAKING CHANGE:' present in commit body, but missing explanation")))
|
|
|
|
(when (string-match-p "^BREAKING CHANGE:" body)
|
|
|
|
(cons 'error (format "'BREAKING CHANGE:' present in body, but missing '!' after %S"
|
|
|
|
type)))))
|
|
|
|
|
|
|
|
(fn! (&key type body)
|
|
|
|
(and (eq type 'bump)
|
|
|
|
(let ((bump-re "\\(?:https?://.+\\|[^/]+\\)/[^/]+@\\([a-z0-9]+\\)"))
|
|
|
|
(not (string-match-p (concat "^" bump-re " -> " bump-re "$")
|
|
|
|
body)))
|
|
|
|
(cons 'error "Bump commit is missing commit hash diffs")))
|
|
|
|
|
|
|
|
(fn! (&key body)
|
|
|
|
(with-temp-buffer
|
|
|
|
(insert body)
|
|
|
|
(catch 'result
|
|
|
|
(let ((bump-re "^\\(?:https?://.+\\|[^/]+\\)/[^/]+@\\([a-z0-9]+\\)"))
|
|
|
|
(while (re-search-backward bump-re nil t)
|
|
|
|
(when (/= (length (match-string 1)) 12)
|
|
|
|
(throw 'result (cons 'error (format "Commit hash in %S must be 12 characters long"
|
|
|
|
(match-string 0))))))))))
|
2021-07-31 23:50:00 -04:00
|
|
|
|
2021-08-01 11:38:09 -04:00
|
|
|
;; TODO Add bump validations for revert: type.
|
|
|
|
|
2021-09-16 02:43:13 +02:00
|
|
|
(fn! (&key body)
|
|
|
|
(when (string-match-p "^\\(\\(Fix\\|Clos\\|Revert\\)ed\\|Reference[sd]\\|Refs\\):? " body)
|
|
|
|
(cons 'error "No present tense or imperative mood for a reference line")))
|
|
|
|
|
|
|
|
(fn! (&key refs)
|
|
|
|
(and (seq-filter (lambda (ref)
|
|
|
|
(string-match-p "^\\(\\(Fix\\|Close\\|Revert\\)\\|Ref\\): " ref))
|
|
|
|
refs)
|
|
|
|
(cons 'error "Colon after reference line keyword; omit the colon on Fix, Close, Revert, and Ref lines")))
|
|
|
|
|
|
|
|
(fn! (&key refs)
|
|
|
|
(catch 'found
|
|
|
|
(dolist (line refs)
|
|
|
|
(cl-destructuring-bind (type . ref) (split-string line " +")
|
|
|
|
(setq ref (string-join ref " "))
|
|
|
|
(or (string-match "^\\(https?://.+\\|[^/]+/[^/]+\\)?\\(#[0-9]+\\|@[a-z0-9]+\\)" ref)
|
|
|
|
(string-match "^https?://" ref)
|
|
|
|
(and (string-match "^[a-z0-9]\\{12\\}$" ref)
|
|
|
|
(= (car (doom-call-process "git" "show" ref))
|
|
|
|
0))
|
|
|
|
(throw 'found
|
|
|
|
(cons 'error
|
|
|
|
(format "%S is not a valid issue/PR, URL, or 12-char commit hash"
|
|
|
|
line))))))))
|
2021-07-31 23:50:00 -04:00
|
|
|
|
2021-08-01 15:40:45 -04:00
|
|
|
;; TODO Check that bump/revert SUBJECT list: 1) valid modules and 2)
|
2021-07-31 23:50:00 -04:00
|
|
|
;; modules whose files are actually being touched.
|
|
|
|
|
|
|
|
;; TODO Ensure your diff corraborates your SCOPE
|
2021-09-16 02:43:13 +02:00
|
|
|
|
2021-07-31 23:50:00 -04:00
|
|
|
))
|
|
|
|
|
|
|
|
(defun doom-cli--ci-hook-commit-msg (file)
|
|
|
|
(with-temp-buffer
|
|
|
|
(insert-file-contents file)
|
2021-09-16 02:43:13 +02:00
|
|
|
(doom-cli--ci--lint
|
|
|
|
(list (cons
|
|
|
|
"CURRENT"
|
|
|
|
(buffer-substring (point-min)
|
2021-09-17 09:05:53 +02:00
|
|
|
(if (re-search-forward "^# Please enter the commit message" nil t)
|
|
|
|
(match-beginning 0)
|
|
|
|
(point-max))))))))
|
2021-07-31 23:50:00 -04:00
|
|
|
|
|
|
|
|
|
|
|
;;
|
|
|
|
;;;
|
|
|
|
|
2021-09-16 02:43:13 +02:00
|
|
|
(defun doom-cli--ci--lint (commits)
|
2021-08-01 15:40:45 -04:00
|
|
|
(let ((errors? 0)
|
2021-09-16 02:43:13 +02:00
|
|
|
(warnings? 0))
|
|
|
|
(print! (start "Linting %d commits" (length commits)))
|
|
|
|
(print-group!
|
|
|
|
(dolist (commit commits)
|
|
|
|
(let (subject body refs summary type scopes bang refs errors warnings)
|
|
|
|
(with-temp-buffer
|
|
|
|
(save-excursion (insert (cdr commit)))
|
|
|
|
(setq subject (buffer-substring (point-min) (line-end-position))
|
|
|
|
body (buffer-substring
|
|
|
|
(line-beginning-position 3)
|
|
|
|
(save-excursion
|
|
|
|
(or (and (re-search-forward (format "\n\n%s "
|
|
|
|
(regexp-opt '("Co-authored-by:" "Signed-off-by:" "Fix" "Ref" "Close" "Revert")
|
|
|
|
t))
|
|
|
|
nil t)
|
|
|
|
(match-beginning 1))
|
|
|
|
(point-max))))
|
|
|
|
refs (split-string
|
|
|
|
(save-excursion
|
|
|
|
(buffer-substring
|
|
|
|
(or (and (re-search-forward (format "\n\n%s "
|
|
|
|
(regexp-opt '("Co-authored-by:" "Signed-off-by:" "Fix" "Ref" "Close" "Revert")
|
|
|
|
t))
|
|
|
|
nil t)
|
|
|
|
(match-beginning 1))
|
|
|
|
(point-max))
|
|
|
|
(point-max)))
|
|
|
|
"\n" t))
|
|
|
|
(save-match-data
|
|
|
|
(when (looking-at "^\\([a-zA-Z0-9_-]+\\)\\(!?\\)\\(?:(\\([^)]+\\))\\)?: \\([^\n]+\\)")
|
|
|
|
(setq type (intern (match-string 1))
|
|
|
|
bang (equal (match-string 2) "!")
|
|
|
|
scopes (ignore-errors (split-string (match-string 3) ","))
|
|
|
|
summary (match-string 4)))))
|
|
|
|
(dolist (fn doom-cli-commit-rules)
|
|
|
|
(pcase (funcall fn
|
|
|
|
:bang bang
|
|
|
|
:body body
|
|
|
|
:refs refs
|
|
|
|
:scopes scopes
|
|
|
|
:subject subject
|
|
|
|
:summary summary
|
|
|
|
:type type)
|
|
|
|
(`(,type . ,msg)
|
|
|
|
(push msg (if (eq type 'error) errors warnings)))))
|
|
|
|
(if (and (null errors) (null warnings))
|
|
|
|
(print! (success "%s %s") (substring (car commit) 0 7) subject)
|
|
|
|
(print! (start "%s %s") (substring (car commit) 0 7) subject))
|
|
|
|
(print-group!
|
|
|
|
(when errors
|
2021-08-01 15:40:45 -04:00
|
|
|
(cl-incf errors?)
|
2021-09-16 02:43:13 +02:00
|
|
|
(dolist (e (reverse errors))
|
|
|
|
(print! (error "%s" e))))
|
|
|
|
(when warnings
|
|
|
|
(cl-incf warnings?)
|
|
|
|
(dolist (e (reverse warnings))
|
|
|
|
(print! (warn "%s" e))))))))
|
|
|
|
(when (> warnings? 0)
|
2021-09-17 09:05:42 +02:00
|
|
|
(print! (warn "Warnings: %d") warnings?))
|
2021-08-01 15:40:45 -04:00
|
|
|
(when (> errors? 0)
|
2021-09-16 02:43:13 +02:00
|
|
|
(print! (error "Failures: %d") errors?))
|
|
|
|
(if (not (or (> errors? 0) (> warnings? 0)))
|
|
|
|
(print! (success "There were no issues!"))
|
2021-08-01 15:40:45 -04:00
|
|
|
(terpri)
|
2021-09-16 02:43:13 +02:00
|
|
|
(print! "See https://docs.doomemacs.org/latest/#/developers/conventions/git-commits for details")
|
|
|
|
(when (> errors? 0)
|
|
|
|
(throw 'exit 1)))))
|
|
|
|
|
|
|
|
(defun doom-cli--ci--read-commits ()
|
|
|
|
(let (commits)
|
|
|
|
(while (re-search-backward "^commit \\([a-z0-9]\\{40\\}\\)" nil t)
|
|
|
|
(push (cons (match-string 1)
|
|
|
|
(replace-regexp-in-string
|
|
|
|
"^ " ""
|
|
|
|
(save-excursion
|
|
|
|
(buffer-substring-no-properties
|
|
|
|
(search-forward "\n\n")
|
|
|
|
(if (re-search-forward "\ncommit \\([a-z0-9]\\{40\\}\\)" nil t)
|
|
|
|
(match-beginning 0)
|
|
|
|
(point-max))))))
|
|
|
|
commits))
|
|
|
|
commits))
|
|
|
|
|
|
|
|
(defun doom-cli--ci-lint-commits (from &optional to)
|
|
|
|
(with-temp-buffer
|
|
|
|
(insert
|
|
|
|
(cdr (doom-call-process
|
|
|
|
"git" "log"
|
|
|
|
(format "%s..%s" from (or to "HEAD")))))
|
|
|
|
(doom-cli--ci--lint (doom-cli--ci--read-commits))))
|