Skip to content

Commit 074b9eb

Browse files
committed
dev: add commit linter workflow
Experimenting with an internal linter. Only trouble is, if Doom's CLI is ever in a broken state, a contributor will have to know about --no-verify option for git commit. This can be improved post CLI rewrite.
1 parent ea1883c commit 074b9eb

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

.github/workflows/lint.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: Commit linter
2+
on:
3+
pull_request:
4+
jobs:
5+
lint-commits:
6+
runs-on: ubuntu-latest
7+
steps:
8+
- uses: actions/checkout@v2.3.1
9+
with:
10+
fetch-depth: 0
11+
- run: bin/doom ci lint-commits HEAD~1

core/cli/ci.el

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
;;; core/cli/ci.el -*- lexical-binding: t; -*-
2+
3+
(defcli! ci (&optional target &rest args)
4+
"TODO"
5+
(unless target
6+
(user-error "No CI target given"))
7+
(if-let (fn (intern-soft (format "doom-cli--ci-%s" target)))
8+
(apply fn args)
9+
(user-error "No known CI target: %S" target)))
10+
11+
12+
;;
13+
;;;
14+
15+
16+
(defun doom-cli--ci-deploy-hooks ()
17+
(let ((dir (doom-path doom-emacs-dir ".git/hooks"))
18+
(default-directory doom-emacs-dir))
19+
(make-directory dir 'parents)
20+
(dolist (hook '("commit-msg"))
21+
(let ((file (doom-path dir hook)))
22+
(with-temp-file file
23+
(insert "#!/usr/bin/env sh\n"
24+
(doom-path doom-emacs-dir "bin/doom")
25+
" --nocolor ci hook-" hook
26+
" \"$@\""))
27+
(set-file-modes file #o700)
28+
(print! (success "Created %s") (relpath file))))))
29+
30+
31+
;;
32+
;;; Git hooks
33+
34+
(defvar doom-cli-commit-rules
35+
(list (cons "^[^\n]\\{10,\\}\n"
36+
"Commit summary is too short (<10) and should be more descriptive")
37+
38+
(cons "^\\(revert!?:[^\n]\\{,72\\}\\|[^\n]\\{,80\\}\\)\n"
39+
"Commit summary too long; <=72 is ideal, 80 is max")
40+
41+
(cons (concat
42+
"^\\("
43+
(regexp-opt
44+
'("bump" "dev" "docs" "feat" "fix" "merge" "nit" "perf"
45+
"refactor" "release" "revert" "test" "tweak"))
46+
"\\)!?[^ :]*: ")
47+
"Invalid type")
48+
49+
(cons (lambda ()
50+
(looking-at "^\\(bump\\|revert\\|release\\|merge\\)!?([^)]+):"))
51+
"This commit type's scope goes after the colon, not before")
52+
53+
(cons (lambda ()
54+
(when (looking-at "[^ :!(]+!?(\\([^)]+\\)): ")
55+
(not
56+
(string-match
57+
(string-join
58+
(cons (concat
59+
"^" (regexp-opt
60+
(cl-loop for path
61+
in (cdr (doom-module-load-path (list doom-modules-dir)))
62+
for (_category . module)
63+
= (doom-module-from-path path)
64+
collect (symbol-name module))) "$")
65+
'("^&" "^cli$"))
66+
"\\|")
67+
(match-string 1)))))
68+
"Invalid scope")
69+
70+
(cons (lambda ()
71+
(when (looking-at "[^ :!(]+![(:]")
72+
(not (re-search-forward "^BREAKING CHANGE: .+" nil t))))
73+
"'!' present in type, but missing 'BREAKING CHANGE:' in body")
74+
75+
(cons (lambda ()
76+
(when (looking-at "[^ :!]+\\(([^)])\\)?: ")
77+
(re-search-forward "^BREAKING CHANGE: .+" nil t)))
78+
"'BREAKING CHANGE:' present in body, but missing '!' in type")
79+
80+
(cons (lambda ()
81+
(when (re-search-forward "^BREAKING CHANGE:" nil t)
82+
(not (looking-at " [^\n]+"))))
83+
"'BREAKING CHANGE:' present in body, but empty")
84+
85+
(cons (lambda ()
86+
(when (looking-at "bump: ")
87+
(let ((bump-re "^\\(https?://.+\\|[^/]+\\)/[^/]+@[a-z0-9]\\{7\\}"))
88+
(re-search-forward (concat "^" bump-re " -> " bump-re "$")
89+
nil t))))
90+
"Bump commit doesn't contain commit diff")
91+
92+
(cons (lambda ()
93+
(re-search-forward "^\\(\\(Fix\\|Clos\\|Revert\\)ed\\|Reference[sd]\\): "
94+
nil t))
95+
"Use present tense/imperative voice for references, without a colon")
96+
97+
;; TODO Check that bump/revert SUMMARY list: 1) valid modules and 2)
98+
;; modules whose files are actually being touched.
99+
100+
;; TODO Ensure your diff corraborates your SCOPE
101+
))
102+
103+
(defun doom-cli--ci-hook-commit-msg (file)
104+
(with-temp-buffer
105+
(insert-file-contents file)
106+
(let (errors)
107+
(dolist (rule doom-cli-commit-rules)
108+
(cl-destructuring-bind (pred . msg) rule
109+
(goto-char (point-min))
110+
(save-match-data
111+
(when (if (functionp pred)
112+
(funcall pred)
113+
(if (stringp pred)
114+
(not (re-search-forward pred nil t))
115+
(error "Invalid predicate: %S" pred)))
116+
(push msg errors)))))
117+
(when errors
118+
(print! (error "Your commit message failed to pass Doom's conventions, here's why:"))
119+
(dolist (error (reverse errors))
120+
(print-group! (print! (info error))))
121+
(terpri)
122+
(print! "See https://doomemacs.org/project.org#commit-message-formatting for details")
123+
(throw 'exit 0))
124+
t)))
125+
126+
127+
;;
128+
;;;
129+
130+
(defun doom-cli--ci-lint-commits (from &optional to)
131+
(with-temp-buffer
132+
(save-excursion
133+
(insert
134+
(cdr (doom-call-process
135+
"git" "log"
136+
(format "%s...%s" from (or to "HEAD"))))))
137+
(while (re-search-forward "^commit \\([a-z0-9]\\{40\\}\\)" nil t)
138+
(let ((commit (match-string 1))
139+
errors)
140+
(forward-line 4)
141+
(save-restriction
142+
(save-match-data
143+
(narrow-to-region
144+
(point) (save-excursion
145+
(if (re-search-forward "^commit \\([a-z0-9]\\{40\\}\\)" nil t)
146+
(match-beginning 0)
147+
(point-max))))
148+
(indent-rigidly (point-min) (point-max) -4))
149+
(save-excursion
150+
(print! (start "Commit %s") commit)
151+
(dolist (rule doom-cli-commit-rules)
152+
(cl-destructuring-bind (pred . msg) rule
153+
(goto-char (point-min))
154+
(save-match-data
155+
(when (if (functionp pred)
156+
(funcall pred)
157+
(if (stringp pred)
158+
(not (re-search-forward pred nil t))
159+
(error "Invalid predicate: %S" pred)))
160+
(push msg (alist-get commit errors nil nil #'equal)))))))
161+
(when errors
162+
(dolist (error (reverse errors))
163+
(print! (error "Commit %s") (car error))
164+
(print-group!
165+
(dolist (e (reverse (cdr error)))
166+
(print! (info e)))))
167+
(terpri)
168+
(print! "%d commit(s) failed the linter" (length errors))
169+
(terpri)
170+
(print! "See https://doomemacs.org/project.org#commit-message-formatting for details")
171+
(throw 'exit 1)))))
172+
t))

0 commit comments

Comments
 (0)