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)
|
2021-09-26 13:42:07 +02:00
|
|
|
(dolist (hook '("commit-msg" "pre-push"))
|
2021-07-31 23:50:00 -04:00
|
|
|
(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
|
|
|
|
|
2021-09-17 09:10:37 +02:00
|
|
|
(defvar doom-cli-commit-ref-types '("Fix" "Ref" "Close" "Revert"))
|
|
|
|
|
|
|
|
(defvar doom-cli-commit-ref-git-types '("Co-authored-by:" "Signed-off-by:"))
|
|
|
|
|
2021-07-31 23:50:00 -04:00
|
|
|
(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))
|
2021-09-24 12:31:46 +02:00
|
|
|
(cl-loop with valid-scopes =
|
|
|
|
(let ((modules (mapcar #'doom-module-from-path (cdr (doom-module-load-path (list doom-modules-dir))))))
|
|
|
|
(append (seq-uniq (mapcar #'car modules))
|
|
|
|
(mapcar #'cdr modules)))
|
|
|
|
with extra-scopes = '("cli" "ci" "lib")
|
2021-09-16 02:43:13 +02:00
|
|
|
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 "\\|")
|
2021-09-24 12:31:46 +02:00
|
|
|
"\\|^\\("
|
|
|
|
(regexp-opt (append type-scopes
|
|
|
|
extra-scopes
|
|
|
|
(mapcar #'symbol-name valid-scopes)))
|
|
|
|
"\\)$")
|
2021-09-16 02:43:13 +02:00
|
|
|
for scope in scopes
|
|
|
|
if (not (string-match scopes-re scope))
|
|
|
|
collect scope into error-scopes
|
|
|
|
finally return
|
|
|
|
(when error-scopes
|
2021-09-24 12:31:46 +02:00
|
|
|
(cons 'error
|
|
|
|
(format "Commit has invalid scope%s: %s"
|
|
|
|
(if (cdr error-scopes) "s" "")
|
|
|
|
(string-join (nreverse error-scopes) ", ")))))))
|
2021-09-16 02:43:13 +02:00
|
|
|
|
|
|
|
(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)
|
2021-09-29 18:26:10 +02:00
|
|
|
(let ((bump-re "\\<\\(?:https?://[^@]+\\|[^/]+\\)/[^/]+@\\([a-z0-9]+\\)")
|
|
|
|
refs)
|
|
|
|
(while (re-search-backward bump-re nil t)
|
|
|
|
(when (/= (length (match-string 1)) 12)
|
|
|
|
(push (match-string 0) refs)))
|
|
|
|
(when refs
|
|
|
|
(cons 'error (format "%d commit hash(s) not 12 characters long: %s"
|
|
|
|
(length refs) (string-join (nreverse refs) ", ")))))))
|
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 " +")
|
2021-09-17 09:10:37 +02:00
|
|
|
(unless (member type doom-cli-commit-ref-git-types)
|
2021-09-17 09:10:14 +02:00
|
|
|
(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-09-30 13:24:06 +02:00
|
|
|
)
|
|
|
|
"A list of validator functions to run against a commit.
|
|
|
|
|
|
|
|
Each function is N-arity and is passed a plist with the following keys:
|
|
|
|
|
|
|
|
:bang
|
|
|
|
(Boolean) If `t', the commit is declared to contain a breaking change.
|
|
|
|
e.g. 'refactor!: this commit breaks everything'
|
|
|
|
:body
|
|
|
|
(String) Contains the whole BODY of a commit message. This includes the
|
|
|
|
subject line (the first line) and the footer.
|
|
|
|
:refs
|
|
|
|
(List<String>) Contains a list of reference lines, i.e. All Fix, Ref, Close,
|
|
|
|
or Revert lines with a valid reference (an URL, commit hash, or valid Github
|
|
|
|
issue/PR reference).
|
|
|
|
:scopes
|
|
|
|
(List<Symbol>) Contains a list of scopes, as symbols. e.g. with
|
|
|
|
'feat(org,lsp): so on and so forth', this contains '(org lsp).
|
|
|
|
:subject
|
|
|
|
(String) Contains the whole first line of a commit message.
|
|
|
|
:summary
|
|
|
|
(String) Contains the summary following the type and scopes. e.g. In
|
|
|
|
'feat(org): fix X, Y, and Z' the summary is 'fix X, Y, and Z.
|
|
|
|
:type
|
|
|
|
(Symbol) The type of commit this is. E.g. `feat', `fix', `bump', etc.
|
|
|
|
|
|
|
|
Each function should return nothing if there was no error, otherwise return a
|
|
|
|
cons cell whose CAR is the type of incident as a symbol (one of `error' or
|
|
|
|
`warn') and whose CDR is an explanation (string) for the result.
|
|
|
|
|
|
|
|
Note: warnings are not considered failures.")
|
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-26 13:42:07 +02:00
|
|
|
(defun doom-cli--ci-hook-pre-push (_remote _url)
|
|
|
|
(with-temp-buffer
|
|
|
|
(let ((z40 "0000000000000000000000000000000000000000")
|
|
|
|
line range errors)
|
|
|
|
(while (setq line (ignore-errors (read-from-minibuffer "")))
|
|
|
|
(catch 'continue
|
|
|
|
(cl-destructuring-bind (local-ref local-sha remote-ref remote-sha)
|
|
|
|
(split-string line " ")
|
|
|
|
(unless (or (string-match-p "^refs/heads/\\(master\\|main\\)$" remote-ref)
|
|
|
|
(equal local-sha z40))
|
|
|
|
(throw 'continue t))
|
|
|
|
(setq
|
|
|
|
range (if (equal remote-sha z40)
|
|
|
|
local-sha
|
|
|
|
(format "%s..%s" remote-sha local-sha)))
|
|
|
|
|
|
|
|
(dolist (type '("WIP" "squash!" "fixup!" "FIXUP"))
|
|
|
|
(let ((commits
|
|
|
|
(split-string
|
|
|
|
(cdr (doom-call-process
|
|
|
|
"git" "rev-list" "--grep" (concat "^" type) range))
|
|
|
|
"\n" t)))
|
|
|
|
(dolist (commit commits)
|
|
|
|
(push (cons type commit) errors))))
|
|
|
|
|
|
|
|
(if (null errors)
|
|
|
|
(print! (success "No errors during push"))
|
|
|
|
(print! (error "Aborting push due to lingering WIP, squash!, or fixup! commits"))
|
|
|
|
(print-group!
|
|
|
|
(dolist (error errors)
|
|
|
|
(print! (info "%s commit in %s" (car error) (cdr error)))))
|
|
|
|
(throw 'exit 1))))))))
|
|
|
|
|
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)))
|
2021-09-17 09:10:37 +02:00
|
|
|
(let ((end (save-excursion
|
|
|
|
(or (and (re-search-forward
|
|
|
|
(format "\n\n%s "
|
|
|
|
(regexp-opt (append doom-cli-commit-ref-types
|
|
|
|
doom-cli-commit-ref-git-types)
|
|
|
|
t))
|
|
|
|
nil t)
|
|
|
|
(match-beginning 1))
|
|
|
|
(point-max)))))
|
|
|
|
(setq subject (buffer-substring (point-min) (line-end-position))
|
|
|
|
body (buffer-substring (line-beginning-position 3) end)
|
|
|
|
refs (split-string (buffer-substring end (point-max))
|
|
|
|
"\n" t)))
|
2021-09-16 02:43:13 +02:00
|
|
|
(save-match-data
|
|
|
|
(when (looking-at "^\\([a-zA-Z0-9_-]+\\)\\(!?\\)\\(?:(\\([^)]+\\))\\)?: \\([^\n]+\\)")
|
|
|
|
(setq type (intern (match-string 1))
|
|
|
|
bang (equal (match-string 2) "!")
|
2021-09-30 13:00:08 +02:00
|
|
|
summary (match-string 4)
|
|
|
|
scopes (ignore-errors (split-string (match-string 3) ","))))))
|
2021-09-26 13:34:12 +02:00
|
|
|
(unless (string-match-p "^\\(?:\\(?:fixup\\|squash\\)!\\|FIXUP\\|WIP\\) " subject)
|
|
|
|
(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
|
|
|
|
(cl-incf errors?)
|
|
|
|
(dolist (e (reverse errors))
|
|
|
|
(print! (error "%s" e))))
|
|
|
|
(when warnings
|
|
|
|
(cl-incf warnings?)
|
|
|
|
(dolist (e (reverse warnings))
|
|
|
|
(print! (warn "%s" e)))))))))
|
2021-09-16 02:43:13 +02:00
|
|
|
(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))))
|