doomemacs/core/cli/ci.el

188 lines
7 KiB
EmacsLisp
Raw Normal View History

;;; 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
(list (cons "^[^\n]\\{10,\\}$"
"Subject is too short (<10) and should be more descriptive")
(cons "^\\(\\(revert\\|bump\\)!?: \\|[^\n]\\{,80\\}\\)$"
"Subject too long; <=72 is ideal, 80 is max")
(cons (concat
"^\\("
(regexp-opt
'("bump" "dev" "docs" "feat" "fix" "merge" "module" "nit" "perf"
"refactor" "release" "revert" "test" "tweak"))
"\\)!?[^ :]*: ")
"Invalid type")
(cons (lambda ()
(when (re-search-forward "^[^ :]+: " nil t)
(and (looking-at "[A-Z][^-]")
(not (looking-at "\\(SPC\\|TAB\\|ESC\\|LFD\\|DEL\\|RET\\)")))))
"Do not capitalize the first word of the subject")
(cons (lambda ()
(looking-at "^\\(bump\\|revert\\|release\\|merge\\|module\\)!?([^)]+):"))
"This type's scope goes after the colon, not before")
(cons (lambda ()
(when (looking-at "[^ :!(]+!?(\\([^)]+\\)): ")
(not
(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 scopes-re =
(string-join (cons (concat "^" (regexp-opt scopes) "$")
'("^&" "^cli$"))
"\\|")
for scope in (split-string (match-string 1) ",")
if (string-match scopes-re scope)
return t))))
"Invalid scope")
(cons (lambda ()
(when (looking-at "[^ :!(]+![(:]")
(not (re-search-forward "^BREAKING CHANGE: .+" nil t))))
"'!' present in type, but missing 'BREAKING CHANGE:' in body")
(cons (lambda ()
(when (looking-at "[^ :!]+\\(([^)])\\)?: ")
(re-search-forward "^BREAKING CHANGE: .+" nil t)))
"'BREAKING CHANGE:' present in body, but missing '!' in type")
(cons (lambda ()
(when (re-search-forward "^BREAKING CHANGE:" nil t)
(not (looking-at " [^\n]+"))))
"'BREAKING CHANGE:' present in body, but empty")
(cons (lambda ()
(when (looking-at "bump: ")
(let ((bump-re "^\\(https?://.+\\|[^/]+\\)/[^/]+@[a-z0-9]\\{7\\}"))
(re-search-forward (concat "^" bump-re " -> " bump-re "$")
nil t))))
"Bump commit doesn't contain commit diff")
;; TODO Add bump validations for revert: type.
(cons (lambda ()
(re-search-forward "^\\(\\(Fix\\|Clos\\|Revert\\)ed\\|Reference[sd]\\|Refs\\): "
nil t))
"Use present tense/imperative voice for footer references, without a colon")
;; TODO Check that bump/revert SUBJECT list: 1) valid modules and 2)
;; modules whose files are actually being touched.
;; TODO Ensure your diff corraborates your SCOPE
))
(defun doom-cli--ci-hook-commit-msg (file)
(with-temp-buffer
(insert-file-contents file)
(let (errors)
(dolist (rule doom-cli-commit-rules)
(cl-destructuring-bind (pred . msg) rule
(goto-char (point-min))
(save-match-data
(when (if (functionp pred)
(funcall pred)
(if (stringp pred)
(not (re-search-forward pred nil t))
(error "Invalid predicate: %S" pred)))
(push msg errors)))))
(when errors
(print! (error "Your commit message failed to pass Doom's conventions, here's why:"))
(dolist (error (reverse errors))
(print-group! (print! (info error))))
(terpri)
(print! "See https://gist.github.com/hlissner/4d78e396acb897d9b2d8be07a103a854 for details")
(throw 'exit 0))
t)))
;;
;;;
(defun doom-cli--ci-lint-commits (from &optional to)
(let ((errors? 0)
commits
case-fold-search)
(with-temp-buffer
(insert
(cdr (doom-call-process
"git" "log"
(format "%s...%s" from (or to "HEAD")))))
(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)))
(dolist (commit commits)
(let (errors)
(with-temp-buffer
(save-excursion (insert (cdr commit)))
(dolist (rule doom-cli-commit-rules)
(save-excursion
(save-match-data
(cl-destructuring-bind (pred . msg) rule
(and (cond ((functionp pred)
(funcall pred))
((stringp pred)
(not (re-search-forward pred nil t)))
((error "Invalid predicate: %S" pred)))
(push msg errors))))))
(if (not errors)
(print! (success "Commit %s") (car commit))
(cl-incf errors?)
(print! (error "Commit %s") (car commit))
(print-group!
(print! "%S" (cdr commit))
(dolist (e (reverse errors))
(print! (error "%s" e))))))))
(when (> errors? 0)
(terpri)
(print! "%d commit(s) failed the linter" errors?)
(terpri)
(print! "See https://doomemacs.org/project.org#commit-message-formatting for details")
(throw 'exit 1)))
t)