dev(ci): refactor & update commit linter

This generalizes and cleans up the linter API so that it can be used in
other Doom projects (or CLI commands, like our WIP changelog generator).

Besides that, our git's commit conventions saw two changes:
- A new 'Amend' keyword, for indicating a commit corrects an earlier,
  recent one. This should be used to omit (or merge) commits in the eyes
  of the changelog generator.
- Trailers must now follow the 'KEY: VALUE' format, which is supported
  OOTB by 'git interpret-trailers' and makes scraping them much eacher.
  Before, omitting the colon was mandatory, this is no longer the case.

Other highlights:
- For linter rules: replaced :footer and :refs keys with :trailers (a
  string->string alist). Invalid trailers will be left in BODY's tail.
- Added a linter for colon delimiters in commit trailers (along with
  other formatting checks, like capitalization and one-per-line checks).
This commit is contained in:
Henrik Lissner 2021-10-20 20:14:14 +02:00
parent b35b32273a
commit e4aecd1a5a

View file

@ -32,120 +32,156 @@
;; ;;
;;; Git hooks ;;; Git hooks
(defvar doom-cli-commit-ref-types '("Fix" "Ref" "Close" "Revert")) (defvar doom-cli-commit-trailer-keys
'(("Fix" ref hash url)
("Ref" ref hash url)
("Close" ref)
("Revert" ref hash)
("Amend" ref hash)
("Co-authored-by" name)
("Signed-off-by" name))
"An alist of valid trailer keys and their accepted value types.
(defvar doom-cli-commit-ref-git-types '("Co-authored-by:" "Signed-off-by:")) Accapted value types can be one or more of ref, hash, url, username, or name.")
(defvar doom-cli-commit-trailer-types
'((ref . "^\\(https?://[^ ]+\\|[^/]+/[^/]+\\)?#[0-9]+$")
(hash . "^\\(https?://[^ ]+\\|[^/]+/[^/]+@\\)?[a-z0-9]\\{12\\}$")
(url . "^https?://")
(name . "^[a-zA-Z0-9-_ ]+<[^@]+@[^.]+\\.[^>]+>$")
(username . "^@[^a-zA-Z0-9_-]+$"))
"An alist of valid trailer keys and their accepted value types.
Accapted value types can be one or more of ref, hash, url, username, or name.")
(defvar doom-cli-commit-types
'(bump dev docs feat fix merge module nit perf refactor release revert test tweak)
"A list of valid commit types.")
(defvar doom-cli-commit-scopes
(list "cli"
"ci"
"lib"
(fn! (scope (&key type))
(when (and (memq type '(bump merge module release revert))
scope)
(user-error "%s commits should never have a scope" type)))
(fn! (scope _)
(doom-glob doom-modules-dir
(if (string-prefix-p ":" scope)
(format "%s" (substring scope 1))
(format "*/%s" scope)))))
"A list of valid commit scopes as strings or functions.
Functions should take two arguments: a single scope (symbol) and a commit plist
representing the current commit being checked against. See
`doom-cli-commit-core-rules' for possible values.")
(defvar doom-cli-commit-rules (defvar doom-cli-commit-rules
(list (fn! (&key subject) (list (fn! (&key subject)
(when (<= (length subject) 10) "If a fixup/squash commit, don't lint this commit"
(cons 'error "Subject is too short (<10) and should be more descriptive"))) (when (string-match "^\\(\\(?:fixup\\|squash\\)!\\|FIXUP\\|WIP\\) " subject)
(skip! (format "Found %S commit, skipping commit" (match-string 1 subject)))))
(fn! (&key subject type) (fn! (&key type subject)
(unless (memq type '(bump revert)) "Test SUBJECT length"
(let ((len (length subject))) (let ((len (length subject)))
(cond ((> len 50) (cond ((<= len 10)
(cons 'warning (fail! "Subject is too short (<10) and should be more descriptive"))
(format "Subject is %d characters; <=50 is ideal, 72 is max" ((<= len 20)
len))) (warn! "Subject is short (<20); are you sure it's descriptive enough?"))
((> len 72) ((memq type '(bump revert)))
(cons 'error ((> len 72)
(format "Subject is %d characters; <=50 is ideal, 72 is max" (fail! "Subject is %d characters, above the 72 maximum"
len))))))) len))
((> len 50)
(warn! "Subject is %d characters; <=50 is ideal"
len)))))
(fn! (&key type) (fn! (&key type)
(unless (memq type '(bump dev docs feat fix merge module nit perf "Ensure commit has valid type"
refactor release revert test tweak)) (or (memq type doom-cli-commit-types)
(cons 'error (format "Commit has an invalid type (%s)" type)))) (if type
(fail! "Invalid commit type: %s" type)
(fail! "Commit has no detectable type"))))
(fn! (&key summary) (fn! (&key summary)
"Ensure commit has a summary"
(when (or (not (stringp summary)) (when (or (not (stringp summary))
(string-blank-p summary)) (string-blank-p summary))
(cons 'error "Commit has no summary"))) (fail! "Commit has no summary")))
(fn! (&key type summary subject) (fn! (&key type summary subject)
(and (not (eq type 'revert)) "Ensure summary isn't needlessly capitalized"
(stringp summary) (and (stringp summary)
(string-match-p "^[A-Z][^-]" summary) (string-match-p "^[A-Z][^-A-Z.]" summary)
(not (string-match-p "\\(SPC\\|TAB\\|ESC\\|LFD\\|DEL\\|RET\\)" summary)) (fail! "%S in summary should not be capitalized"
(cons 'error (format "%S in summary is capitalized; do not capitalize the summary" (car (split-string summary " ")))))
(car (split-string summary " "))))))
(fn! (&key type scopes summary) (fn! (&key type scopes summary)
(and (memq type '(bump revert release merge module)) "Complain about scoped types that are incompatible with scopes"
(and (memq type '(bump revert merge module release))
scopes scopes
(cons 'error (fail! "Scopes for %s commits should go after the colon, not before"
(format "Scopes for %s commits should go after the colon, not before" type)))
type))))
(fn! (&key type scopes) (fn! (&rest plist &key type scopes)
(unless (memq type '(bump revert merge module release)) "Ensure scopes are valid"
(cl-loop with valid-scopes = (dolist (scope scopes)
(let ((modules (mapcar #'doom-module-from-path (cdr (doom-module-load-path (list doom-modules-dir)))))) (condition-case e
(append (seq-uniq (mapcar #'car modules)) (or (cl-loop for rule in doom-cli-commit-scopes
(mapcar #'cdr modules))) if (or (and (stringp rule)
with extra-scopes = '("cli" "ci" "lib") (string= rule scope))
with regexp-scopes = '("^&") (and (functionp rule)
with type-scopes = (funcall rule scope plist)))
(pcase type return t)
(`docs (fail! "Invalid scope: %s" scope))
(cons "install" (user-error (fail! "%s" (error-message-string))))))
(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
(mapcar #'symbol-name valid-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"
(if (cdr error-scopes) "s" "")
(string-join (nreverse error-scopes) ", ")))))))
(fn! (&key scopes) (fn! (&key scopes)
(unless (equal scopes (sort scopes #'string-lessp)) "Esnure scopes are sorted correctly"
(cons 'error "Scopes are not in lexicographical order"))) (unless (equal scopes (sort (copy-sequence scopes) #'string-lessp))
(fail! "Scopes are not in lexicographical order")))
(fn! (&key type body) (fn! (&key type body)
(unless (memq type '(bump revert merge)) "Enforce 72 character line width for BODY"
(catch 'result (catch 'result
(with-temp-buffer (with-temp-buffer
(save-excursion (insert body)) (save-excursion (insert body))
(while (re-search-forward "^[^\n]\\{73,\\}" nil t) (while (re-search-forward "^[^\n]\\{73,\\}" nil t)
;; Exclude ref lines, bump lines, comments, lines with URLs, (save-excursion
;; or indented lines (or
(save-excursion ;; Long bump lines are acceptable
(or (let ((bump-re "\\(https?://.+\\|[^/]+\\)/[^/]+@[a-z0-9]\\{12\\}")) (let ((bump-re "\\(https?://.+\\|[^/]+\\)/[^/]+@[a-z0-9]\\{12\\}"))
(re-search-backward (format "^%s -> %s$" bump-re bump-re) nil t)) (re-search-backward (format "^%s -> %s$" bump-re bump-re) nil t))
(re-search-backward "https?://[^ ]+\\{73,\\}" nil t) ;; Long URLs are acceptable
(re-search-backward "^\\(?:#\\| +\\)" nil t) (re-search-backward "https?://[^ ]+\\{73,\\}" nil t)
(throw 'result (cons 'error "Line(s) in commit body exceed 72 characters"))))))))) ;; Lines that start with # or whitespace are comment or
;; code blocks.
(re-search-backward "^\\(?:#\\| +\\)" nil t)
(throw 'result (fail! "Line(s) in commit body exceed 72 characters"))))))))
(fn! (&key bang body type) (fn! (&key bang body type)
"Ensure ! is accompanied by a 'BREAKING CHANGE:' in BODY"
(if bang (if bang
(cond ((not (string-match-p "^BREAKING CHANGE:" body)) (cond ((not (string-match-p "^BREAKING CHANGE:" body))
(cons 'error "'!' present in commit type, but missing 'BREAKING CHANGE:' in body")) (fail! "'!' present in commit type, but missing 'BREAKING CHANGE:' in body"))
((not (string-match-p "^BREAKING CHANGE: .+" body)) ((not (string-match-p "^BREAKING CHANGE: .+" body))
(cons 'error "'BREAKING CHANGE:' present in commit body, but missing explanation"))) (fail! "'BREAKING CHANGE:' present in commit body, but missing explanation")))
(when (string-match-p "^BREAKING CHANGE:" body) (when (string-match-p "^BREAKING CHANGE:" body)
(cons 'error (format "'BREAKING CHANGE:' present in body, but missing '!' after %S" (fail! "'BREAKING CHANGE:' present in body, but missing '!' after %S"
type))))) type))))
(fn! (&key type body) (fn! (&key type body)
"Ensure bump commits have package ref lines"
(and (eq type 'bump) (and (eq type 'bump)
(let ((bump-re "\\(?:https?://.+\\|[^/]+\\)/[^/]+@\\([a-z0-9]+\\)")) (let ((bump-re "\\(?:https?://.+\\|[^/]+\\)/[^/]+@\\([a-z0-9]+\\)"))
(not (string-match-p (concat "^" bump-re " -> " bump-re "$") (not (string-match-p (concat "^" bump-re " -> " bump-re "$")
body))) body)))
(cons 'error "Bump commit is missing commit hash diffs"))) (fail! "Bump commit is missing commit hash diffs")))
(fn! (&key body) (fn! (&key body)
"Ensure commit hashes in bump lines are 12 characters long"
(with-temp-buffer (with-temp-buffer
(insert body) (insert body)
(let ((bump-re "\\<\\(?:https?://[^@]+\\|[^/]+\\)/[^/]+@\\([a-z0-9]+\\)") (let ((bump-re "\\<\\(?:https?://[^@]+\\|[^/]+\\)/[^/]+@\\([a-z0-9]+\\)")
@ -154,36 +190,45 @@
(when (/= (length (match-string 1)) 12) (when (/= (length (match-string 1)) 12)
(push (match-string 0) refs))) (push (match-string 0) refs)))
(when refs (when refs
(cons 'error (format "%d commit hash(s) not 12 characters long: %s" (fail! "%d commit hash(s) not 12 characters long: %s"
(length refs) (string-join (nreverse refs) ", "))))))) (length refs) (string-join (nreverse refs) ", "))))))
;; TODO Add bump validations for revert: type. ;; TODO Add bump validations for revert: type.
(fn! (&key body) (fn! (&key body trailers)
(when (string-match-p "^\\(\\(Fix\\|Clos\\|Revert\\)ed\\|Reference[sd]\\|Refs\\):? " body) "Validate commit trailers."
(cons 'error "No present tense or imperative mood for a reference line"))) (let* ((keys (mapcar #'car doom-cli-commit-trailer-keys))
(key-re (regexp-opt keys t))
(fn! (&key refs) (lines
(and (seq-filter (lambda (ref) ;; Scan BODY because invalid trailers won't be in TRAILERS.
(string-match-p "^\\(\\(Fix\\|Close\\|Revert\\)\\|Ref\\): " ref)) (save-match-data
refs) (and (string-match "\n\\(\n[a-zA-Z-]+:? [^ ][^\n]+\\)+\n*\\'" body)
(cons 'error "Colon after reference line keyword; omit the colon on Fix, Close, Revert, and Ref lines"))) (split-string (match-string 0 body) "\n" t)))))
(dolist (line lines)
(fn! (&key refs) (unless (string-match-p (concat "^" key-re ":? [^ ]") line)
(catch 'found (fail! "Found %S, expected one of: %s"
(dolist (line refs) (truncate-string-to-width (string-trim line) 16 nil nil "")
(cl-destructuring-bind (type . ref) (split-string line " +") (string-join keys ", ")))
(unless (member type doom-cli-commit-ref-git-types) (when (and (string-match "^[^a-zA-Z-]+:? \\(.+\\)$" line)
(setq ref (string-join ref " ")) (string-match-p " " (match-string 1 line)))
(or (string-match "^\\(https?://.+\\|[^/]+/[^/]+\\)?\\(#[0-9]+\\|@[a-z0-9]+\\)" ref) (fail! "%S has multiple references, but should only have one per line"
(string-match "^https?://" ref) (truncate-string-to-width (string-trim line) 20 nil nil "")))
(and (string-match "^[a-z0-9]\\{12\\}$" ref) (when (or (string-match (concat "^" key-re "\\(?:e?[sd]\\|ing\\)? [^ ]") line)
(= (car (doom-call-process "git" "show" ref)) (string-match (concat "^\\([a-zA-Z-]+\\) [^ \n]+$") line))
0)) (fail! "%S missing colon after %S"
(throw 'found (truncate-string-to-width (string-trim line) 16 nil nil "")
(cons 'error (match-string 1 line))))
(format "%S is not a valid issue/PR, URL, or 12-char commit hash" (pcase-dolist (`(,key . ,value) trailers)
line))))))))) (if (string-match-p " " value)
(fail! "Found %S, but only one value allowed per trailer"
(truncate-string-to-width (concat key ": " value) 20 nil nil ""))
(when-let (allowed-types (cdr (assoc key doom-cli-commit-trailer-keys)))
(or (cl-loop for type in allowed-types
if (cdr (assq type doom-cli-commit-trailer-types))
if (string-match-p it value)
return t)
(fail! "%S expects one of %s, but got %S"
key allowed-types value)))))))
;; TODO Check that bump/revert SUBJECT list: 1) valid modules and 2) ;; TODO Check that bump/revert SUBJECT list: 1) valid modules and 2)
;; modules whose files are actually being touched. ;; modules whose files are actually being touched.
@ -199,12 +244,8 @@ Each function is N-arity and is passed a plist with the following keys:
(Boolean) If `t', the commit is declared to contain a breaking change. (Boolean) If `t', the commit is declared to contain a breaking change.
e.g. 'refactor!: this commit breaks everything' e.g. 'refactor!: this commit breaks everything'
:body :body
(String) Contains the whole BODY of a commit message. This includes the (String) Contains the whole BODY of a commit message, excluding the
subject line (the first line) and the footer. TRAILERS.
: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 :scopes
(List<Symbol>) Contains a list of scopes, as symbols. e.g. with (List<Symbol>) Contains a list of scopes, as symbols. e.g. with
'feat(org,lsp): so on and so forth', this contains '(org lsp). 'feat(org,lsp): so on and so forth', this contains '(org lsp).
@ -213,12 +254,16 @@ Each function is N-arity and is passed a plist with the following keys:
:summary :summary
(String) Contains the summary following the type and scopes. e.g. In (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. 'feat(org): fix X, Y, and Z' the summary is 'fix X, Y, and Z.
:trailers
(Map<String, String>) Contains an alist of 'KEY: VALUE' trailers, i.e. All
Fix, Ref, Close, Revert, etc lines with a valid value. This will be empty if
the formatting of a commit's trailers is invalid.
:type :type
(Symbol) The type of commit this is. E.g. `feat', `fix', `bump', etc. (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 Each function should call `fail!' or `warn!' one or more times, or `skip!'
cons cell whose CAR is the type of incident as a symbol (one of `error' or (immediately returns). Each of these lexical functions take the same arguments
`warn') and whose CDR is an explanation (string) for the result. as `format'.
Note: warnings are not considered failures.") Note: warnings are not considered failures.")
@ -235,36 +280,34 @@ Note: warnings are not considered failures.")
(defun doom-cli--ci-hook-pre-push (_remote _url) (defun doom-cli--ci-hook-pre-push (_remote _url)
(with-temp-buffer (with-temp-buffer
(let ((z40 "0000000000000000000000000000000000000000") (let ((z40 "0000000000000000000000000000000000000000")
line range errors) line error)
(while (setq line (ignore-errors (read-from-minibuffer ""))) (while (setq line (ignore-errors (read-from-minibuffer "")))
(catch 'continue (catch 'continue
(cl-destructuring-bind (local-ref local-sha remote-ref remote-sha) (cl-destructuring-bind (local-ref local-sha remote-ref remote-sha)
(split-string line " ") (split-string line " ")
(unless (or (string-match-p "^refs/heads/\\(master\\|main\\)$" remote-ref) (unless (or (string-match-p "^refs/heads/\\(master\\|main\\)$" remote-ref)
(equal local-sha z40)) (equal local-sha z40))
(throw 'continue t)) (throw 'continue t))
(setq (print-group!
range (if (equal remote-sha z40) (mapc (lambda (commit)
local-sha (seq-let (hash msg) (split-string commit "\t")
(format "%s..%s" remote-sha local-sha))) (setq error t)
(print! (info "%S commit in %s"
(dolist (type '("WIP" "squash!" "fixup!" "FIXUP")) (car (split-string msg " "))
(let ((commits (substring hash 0 12)))))
(split-string (split-string
(cdr (doom-call-process (cdr (doom-call-process
"git" "rev-list" "--grep" (concat "^" type) range)) "git" "rev-list"
"\n" t))) "--grep" (concat "^" (regexp-opt '("WIP" "squash!" "fixup!" "FIXUP") t) " ")
(dolist (commit commits) "--format=%H\t%s"
(push (cons type commit) errors)))) (if (equal remote-sha z40)
local-sha
(if (null errors) (format "%s..%s" remote-sha local-sha))))
(print! (success "No errors during push")) "\n" t))
(print! (error "Aborting push due to lingering WIP, squash!, or fixup! commits")) (when error
(print-group! (print! (error "Aborting push due to unrebased WIP, squash!, or fixup! commits"))
(dolist (error errors) (throw 'exit 1)))))))))
(print! (info "%s commit in %s" (car error) (cdr error)))))
(throw 'exit 1))))))))
;; ;;
@ -274,24 +317,24 @@ Note: warnings are not considered failures.")
(with-temp-buffer (with-temp-buffer
(save-excursion (insert commit-msg)) (save-excursion (insert commit-msg))
(append (append
(let ((end (save-excursion (let ((end
(or (and (re-search-forward (save-excursion
(format "\n\n%s " (if (re-search-forward "\n\\(\n[a-zA-Z-]+: [^ ][^\n]+\\)+\n*\\'" nil t)
(regexp-opt (append doom-cli-commit-ref-types (1+ (match-beginning 0))
doom-cli-commit-ref-git-types) (point-max)))))
t)) `(:subject ,(buffer-substring (point-min) (line-end-position))
nil t) :body ,(string-trim-right (buffer-substring (line-beginning-position 3) end))
(match-beginning 1)) :trailers ,(save-match-data
(point-max))))) (cl-loop with footer = (buffer-substring end (point-max))
`(:subject ,(buffer-substring (point-min) (line-end-position)) for line in (split-string footer "\n" t)
:body ,(buffer-substring (line-beginning-position 3) end) if (string-match "^\\([a-zA-Z-]+\\): \\(.+\\)$" line)
:refs ,(split-string (buffer-substring end (point-max)) "\n" t))) collect (cons (match-string 1 line) (match-string 2 line))))))
(save-match-data (save-match-data
(when (looking-at "^\\([a-zA-Z0-9_-]+\\)\\(!?\\)\\(?:(\\([^)]+\\))\\)?: \\([^\n]+\\)") (when (looking-at "^\\([a-zA-Z0-9_-]+\\)\\(!?\\)\\(?:(\\([^)]+\\))\\)?: \\([^\n]+\\)")
`(:type ,(intern (match-string 1)) `(:type ,(intern (match-string 1))
:bang ,(equal (match-string 2) "!") :bang ,(equal (match-string 2) "!")
:summary ,(match-string 4) :summary ,(match-string 4)
:scopes ,(ignore-errors (split-string (match-string 3) ","))))) :scopes ,(ignore-errors (split-string (match-string 3) ",")))))
(save-excursion (save-excursion
(let ((bump-re "\\(\\(?:https?://.+\\|[^/ \n]+\\)/[^/ \n]+@[a-z0-9]\\{12\\}\\)") (let ((bump-re "\\(\\(?:https?://.+\\|[^/ \n]+\\)/[^/ \n]+@[a-z0-9]\\{12\\}\\)")
bumps) bumps)
@ -323,62 +366,55 @@ Note: warnings are not considered failures.")
(cl-sort (delete-dups packages) #'string-lessp :key #'car))))) (cl-sort (delete-dups packages) #'string-lessp :key #'car)))))
(defun doom-cli--ci--lint (commits) (defun doom-cli--ci--lint (commits)
(let ((errors? 0) (let ((warnings 0)
(warnings? 0)) (failures 0))
(print! (start "Linting %d commits" (length commits))) (print! (start "Linting %d commits" (length commits)))
(print-group! (print-group!
(dolist (commit commits) (pcase-dolist (`(,ref . ,commitmsg) commits)
(let* ((plist (doom-cli--parse-commit (cdr commit))) (let* ((commit (doom-cli--parse-commit commitmsg))
(subject (plist-get plist :subject)) (shortref (substring ref 0 7))
warnings errors) (subject (plist-get commit :subject)))
(unless (string-match-p "^\\(?:\\(?:fixup\\|squash\\)!\\|FIXUP\\|WIP\\) " (letf! ((defun skip! (reason &rest args)
subject) (print! (warn "Skipped because: %s") (apply #'format reason args))
(dolist (fn doom-cli-commit-rules) (cl-return-from 'linter))
(pcase (apply fn plist) (defun warn! (reason &rest args)
(`(,type . ,msg) (cl-incf warnings)
(push msg (if (eq type 'error) errors warnings))))) (print! (warn "%s") (apply #'format reason args)))
(if (and (null errors) (null warnings)) (defun fail! (reason &rest args)
(print! (success "%s %s") (substring (car commit) 0 7) subject) (cl-incf failures)
(print! (start "%s %s") (substring (car commit) 0 7) subject)) (print! (error "%s") (apply #'format reason args))))
(print! (start "%s %s") shortref subject)
(print-group! (print-group!
(when errors (cl-block 'linter
(cl-incf errors?) (mapc (doom-rpartial #'apply commit)
(dolist (e (reverse errors)) doom-cli-commit-rules)))))))
(print! (error "%s" e)))) (let ((issues (+ warnings failures)))
(when warnings (if (= issues 0)
(cl-incf warnings?) (print! (success "There were no issues!"))
(dolist (e (reverse warnings)) (if (> warnings 0) (print! (warn "Warnings: %d" warnings)))
(print! (warn "%s" e))))))))) (if (> failures 0) (print! (warn "Failures: %d" failures)))
(when (> warnings? 0) (print! "\nSee https://docs.doomemacs.org/-/conventions/git-commits")
(print! (warn "Warnings: %d") warnings?)) (unless (zerop failures)
(when (> errors? 0) (throw 'exit 1)))
(print! (error "Failures: %d") errors?)) t)))
(if (not (or (> errors? 0) (> warnings? 0)))
(print! (success "There were no issues!"))
(terpri)
(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) (defun doom-cli--ci-lint-commits (from &optional to)
(with-temp-buffer (with-temp-buffer
(insert (insert
(cdr (doom-call-process (cdr (doom-call-process
"git" "log" "git" "log"
(format "%s..%s" from (or to "HEAD"))))) (format "%s...%s" from (or to (concat from "~1"))))))
(doom-cli--ci--lint (doom-cli--ci--read-commits)))) (doom-cli--ci--lint
(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))))