Skip to content

Commit 306a9ff

Browse files
committed
style: start using conventional commits
1 parent dd75696 commit 306a9ff

File tree

6 files changed

+297
-19
lines changed

6 files changed

+297
-19
lines changed

.github/workflows/lint.yml

+14
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ jobs:
3737
- name: Check for changes
3838
run: if [[ -n $(git status -s) ]]; then exit 1; fi
3939

40+
gitlint:
41+
name: Gitlint
42+
runs-on: ubuntu-latest
43+
steps:
44+
- uses: actions/checkout@v4
45+
- uses: actions/setup-python@v3
46+
with:
47+
python-version: '3.11'
48+
- name: Install gitlint
49+
run: pip install gitlint
50+
- name: Run gitlint
51+
run: |
52+
gitlint --commits "origin/main..HEAD"
53+
4054
case-sensitivity:
4155
name: File case sensitivity
4256
runs-on: ubuntu-latest

.gitlint

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[general]
2+
ignore=body-is-missing
3+
contrib=contrib-title-conventional-commits
4+
5+
verbosity = 3
6+
7+
ignore-merge-commits=true
8+
ignore-revert-commits=true
9+
ignore-fixup-commits=true
10+
ignore-fixup-amend-commits=true
11+
ignore-squash-commits=true
12+
13+
[contrib-title-conventional-commits]
14+
types=ci,docs,feat,fix,refactor,revert,style,test

.pre-commit-config.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ repos:
1111
language: system
1212
entry: make --silent documentation
1313
types: []
14+
- id: gitlint
15+
name: Gitlint
16+
language: system
17+
entry: gitlint --msg-filename
18+
stages: [commit-msg]
19+
types: []

CONTRIBUTING.md

+49-18
Original file line numberDiff line numberDiff line change
@@ -20,40 +20,71 @@ All well-intentioned, polite, and respectful contributions are always welcome! T
2020
## Commit messages
2121

2222
- Try to make commit message as concise as possible while giving enough information about nature of a change. Think about whether it will be easy to understand in one year time when browsing through commit history.
23-
- Use two part structure:
24-
- First part is a change overview in present tense, preferably in single line under 80 characters. Should end with a period. Usually should be enough.
2523

26-
**If commit affects only one particular module (as it usually should), prepend with "(mini.\<module-name\>) ".** If commit ensures something for all modules but not necessary touches all of them, use "(all) ".
24+
- Single commit should change either zero or one module, or affect all modules (i.e. enforcing some universal rule but not necessarily change files). Changes for two or more modules should be split in several module-specific commits.
25+
26+
- Use [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) style:
27+
- Messages should follow the following structure:
28+
29+
```
30+
<type>[optional scope][!]: <description>
31+
<empty line>
32+
[optional body]
33+
<empty line>
34+
[optional footer(s)]
35+
```
36+
37+
- `<type>` is **mandatory** and can be one of:
38+
- `ci` - change in how automation (Github actions, dual distribution scripts, etc.) is done.
39+
- `docs` - change in user-facing documentation (help, README, etc.).
40+
- `feat` - adding new user-facing feature.
41+
- `fix` - resolving user-facing issue.
42+
- `refactor` - change in code or documentation that should not affect users.
43+
- `style` - change in convention of how something should be done (formatting, wording, etc.) and its effects.
44+
- `test` - change in tests.
45+
- `[optional scope]`, if present, should be done in parenthesis `()`. If commit changes single module (as it usually should), using scope with module name is **mandatory**. If commit enforces something for all modules, use `ALL` scope.
46+
- Breaking change, if present, should be expressed with `!` before `:`.
47+
- `<description>` is a change overview in imperative, present tense ("change" not "changed" nor "changes"). Should result into first line under 72 characters. Should start with not capitalized word and NOT end with punctuation.
48+
- `[optional body]`, if present, should contain details and motivation about the change in plain language. Should be formatted to have maximum 80 characters in line.
49+
- `[optional footer(s)]`, if present, should be instructions to Git or Github. Use "Resolve #xxx" as separate entry if this commit resolves issue or PR.
2750

28-
- Second part is optional and should contain details about the change after empty line and "Details:". Use bullet list with `-`.
51+
- Use module's function and field names without module's name. Like `add()` and not `MiniSurround.add()`.
2952

30-
Use "Resolves #xxx" as separate entry if this commit resolves issue or PR.
53+
Examples:
3154

32-
- Use these prefixes after initial module name in described situations:
33-
- "FEATURE:" - if change implements new feature (like option or function).
34-
- "BREAKING:" - if change breaks current documented behavior.
35-
- "BREAKING FEATURE:" - if change introduces new feature while breaking current documented behavior.
36-
- "NEW MODULE:" - if change introduces new module (see 'MAINTAINING.md').
55+
```
56+
feat(deps): add folds in update confirmation buffer
57+
```
3758

38-
- Use module's function and field names without module's name. Like `add()` and not `MiniSurround.add()`.
59+
```
60+
fix(jump): make operator not delete one character if target is not found
3961
40-
Examples:
62+
One main goal is to do that in a dot-repeatable way, because this is very
63+
likely to be repeated after an unfortunate first try.
4164
65+
Resolve #688
4266
```
43-
Fix typo in 'README.md'.
67+
4468
```
69+
refactor(bracketed): do not source 'vim.treesitter' on `require()`
4570
71+
Although less explicit, this considerably reduces startup footprint of
72+
'mini.bracketed' in isolation.
4673
```
47-
(mini.animate) Update `cursor` to use virtual columns.
4874

49-
Details:
50-
- Resolves #258.
75+
```
76+
feat(hues)!: update verbatim text to be distinctive
5177
```
5278

5379
```
54-
(mini.comment) FEATURE: add `options.pad_comment_leaders` option.
80+
test(ALL): update screenshots to work on Nightly
5581
```
5682

83+
### Automation
84+
85+
- [Install `gitlint`](https://jorisroovers.com/gitlint/latest/installation/).
86+
- Lint manually with `gitlint` after the commit or better yet [install pre-commit](https://pre-commit.com/#install) and enable it with `pre-commit install` (from the root directory). This will auto-lint commit message before finalizing it.
87+
5788
## Generating help file
5889

5990
If your contribution updates annotations used to generate help file, please regenerate it. You can make this with one of the following (assuming current directory being project root):
@@ -76,7 +107,7 @@ If you have Windows or MacOS and want to contribute code related change, make yo
76107

77108
This project uses [StyLua](https://github.com/JohnnyMorganz/StyLua) version 0.14.0 for formatting Lua code. Before making changes to code, please:
78109

79-
- [Install StyLua](https://github.com/JohnnyMorganz/StyLua#installation). NOTE: use `v0.14.0`.
110+
- [Install StyLua](https://github.com/JohnnyMorganz/StyLua#installation). NOTE: use `v0.19.0`.
80111
- Format with it. Currently there are two ways to do this:
81112
- Manually run `stylua .` from the root directory of this project.
82113
- [Install pre-commit](https://pre-commit.com/#install) and enable it with `pre-commit install` (from the root directory). This will auto-format relevant code before making commits.

MAINTAINING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Usual workflow involves performing these steps after every commit in 'mini.nvim'
5656
- Update main README to mention new module in table of contents.
5757
- Update 'CHANGELOG.md' to mention introduction of new module.
5858
- Update 'CONTRIBUTING.md' to mention new highlight groups (if there are any).
59-
- Commit changes with message '(mini.xxx) NEW MODULE: initial commit.'. NOTE: it is cleaner to synchronize standalone repositories prior to this commit.
59+
- Commit changes with message 'feat(xxx): add NEW MODULE'. NOTE: it is cleaner to synchronize standalone repositories prior to this commit.
6060
- If there are new highlight groups, follow up with adding explicit support in color scheme modules.
6161
- Make standalone plugin:
6262
- Create new empty GitHub repository. Disable Issues and limit PRs.

scripts/lintcommit.lua

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
-- Validate commit message. Designed to be run as pre-commit hook and in CI.
2+
3+
local allowed_commit_types = { 'ci', 'docs', 'feat', 'fix', 'refactor', 'style', 'test' }
4+
5+
local allowed_scopes = { 'ALL' }
6+
for _, module in ipairs(vim.fn.readdir('lua/mini')) do
7+
if module ~= 'init.lua' then table.insert(allowed_scopes, module:match('^(.+)%.lua$')) end
8+
end
9+
10+
local validate_first_line = function(line)
11+
-- Allow starting with 'fixup' to disable commit linting
12+
if vim.startswith(line, 'fixup') then return true, nil end
13+
14+
-- Should match overall conventional commit spec
15+
local commit_type, scope, desc = string.match(line, '^([^(]+)(%b())!?: (.+)$')
16+
if commit_type == nil then
17+
commit_type, desc = string.match(line, '^([^!:]+)!?: (.+)$')
18+
end
19+
if commit_type == nil or desc == nil then
20+
return false, 'First line does not match conventional commit specification: ' .. vim.inspect(line)
21+
end
22+
23+
-- Commit type should be present and be from one of allowed
24+
if not vim.tbl_contains(allowed_commit_types, commit_type) then
25+
local one_of = table.concat(vim.tbl_map(vim.inspect, allowed_commit_types), ', ')
26+
return false, 'Commit type ' .. vim.inspect(commit_type) .. ' is not allowed. Use one of ' .. one_of .. '.'
27+
end
28+
29+
-- Scope, if present, should be from one of allowed
30+
if scope ~= nil then
31+
scope = scope:sub(2, -2)
32+
if not vim.tbl_contains(allowed_scopes, scope) then
33+
local one_of = table.concat(vim.tbl_map(vim.inspect, allowed_scopes), ', ')
34+
return false, 'Scope ' .. vim.inspect(scope) .. ' is not allowed. Use one of ' .. one_of .. '.'
35+
end
36+
end
37+
38+
-- Description should be present and properly formatted
39+
if string.find(desc, '^%w') == nil then
40+
return false, 'Description should start with alphanumeric character: ' .. vim.inspect(desc)
41+
end
42+
43+
if string.find(desc, '^%u%l') ~= nil then
44+
return false, 'Description should not start with capitalized word: ' .. vim.inspect(desc)
45+
end
46+
47+
if string.find(desc, '%p$') ~= nil then
48+
return false, 'Description should not end with punctuation: ' .. vim.inspect(desc)
49+
end
50+
51+
-- Main title should not be too long
52+
if vim.fn.strdisplaywidth(line) > 72 then
53+
return false, 'First line is longer than 72 characters: ' .. vim.inspect(desc)
54+
end
55+
56+
return true, nil
57+
end
58+
59+
local validate_body = function(parts)
60+
if #parts == 1 then return true, nil end
61+
62+
if parts[2] ~= '' then return false, 'Second line should be empty' end
63+
if parts[3] == nil then return false, 'If first line is not enough, body should be present' end
64+
if string.find(parts[3], '^%S') == nil then return false, 'First body line should not start with whitespace.' end
65+
66+
for i = 3, #parts do
67+
if vim.fn.strdisplaywidth(parts[i]) > 80 then
68+
return false, 'Body line is longer than 80 characters: ' .. vim.inspect(parts[i])
69+
end
70+
end
71+
72+
if string.find(parts[#parts], '^%s*$') ~= nil then return false, 'Body should not end with blank line.' end
73+
74+
return true, nil
75+
end
76+
77+
local validate_bad_wording = function(msg)
78+
local has_fix = msg:find('[Ff]ix #') or msg:find('[Ff]ixes #') or msg:find('[Ff]ixed #')
79+
local has_bad_close = msg:find('[Cc]lose[sd]? #') ~= nil
80+
local has_bad_resolve = msg:find('[Rr]esolve[sd] #') ~= nil
81+
if has_fix or has_bad_close or has_bad_resolve then
82+
return false, 'Commit message should use "Resolve #" GitHub keyword to resolve issue/PR.'
83+
end
84+
return true, nil
85+
end
86+
87+
_G.validate_commit_msg = function(msg)
88+
msg = msg:gsub('\n$', '')
89+
local parts = vim.split(msg, '\n')
90+
local is_valid, err_msg
91+
92+
-- Validate main (first) line
93+
is_valid, err_msg = validate_first_line(parts[1])
94+
if not is_valid then return is_valid, err_msg end
95+
96+
-- Validate body
97+
is_valid, err_msg = validate_body(parts)
98+
if not is_valid then return is_valid, err_msg end
99+
100+
-- No validation for footer
101+
102+
-- Should not contain bad wording
103+
is_valid, err_msg = validate_bad_wording(msg)
104+
if not is_valid then return is_valid, err_msg end
105+
106+
return true, nil
107+
end
108+
109+
-- Tests to be run interactively: `_G.test_cases_failed` should be empty.
110+
local test_cases = {
111+
-- First line
112+
['fixup'] = true,
113+
['fixup: commit message'] = true,
114+
['fixup! commit message'] = true,
115+
116+
['ci: normal message'] = true,
117+
['docs: normal message'] = true,
118+
['feat: normal message'] = true,
119+
['fix: normal message'] = true,
120+
['refactor: normal message'] = true,
121+
['style: normal message'] = true,
122+
['test: normal message'] = true,
123+
124+
['feat(ai): message with scope'] = true,
125+
['feat!: message with breaking change'] = true,
126+
['feat(ai)!: message with scope and breaking change'] = true,
127+
['style(ALL): style all modules'] = true,
128+
129+
['unknown: unknown type'] = false,
130+
['feat(unknown): unknown scope'] = false,
131+
['refactor(): empty scope'] = false,
132+
['ci( ): whitespace as scope'] = false,
133+
134+
['ci no colon after type'] = false,
135+
[': no type before colon 1'] = false,
136+
[' : no type before colon 2'] = false,
137+
[' : no type before colon 3'] = false,
138+
['ci:'] = false,
139+
['ci: '] = false,
140+
['ci: '] = false,
141+
142+
['feat: message with : in it'] = true,
143+
['feat(ai): message with : in it'] = true,
144+
145+
['test: extra space after colon'] = false,
146+
['ci: tab after colon'] = false,
147+
['ci:no space after colon'] = false,
148+
['ci : extra space before colon'] = false,
149+
150+
['ci: period at end of sentence.'] = false,
151+
['ci: punctuation at end of sentence!'] = false,
152+
['ci: Capitalized first word'] = false,
153+
['ci: UPPER_CASE First Word'] = true,
154+
['ci: very very very very very very very very very very looooong first line'] = false,
155+
156+
-- Body
157+
['ci: desc\n\nBody'] = true,
158+
['ci: desc\n\nBody\n\nwith\n \nempty and blank lines'] = true,
159+
160+
['ci: desc\nSecond line is not empty'] = false,
161+
['ci: desc\n\n First body line starts with whitespace'] = false,
162+
['ci: desc\n\nBody\nwith\nVery very very very very very very very very very very very very loong first line'] = false,
163+
164+
['ci: only two lines\n\n'] = false,
165+
['ci: desc\n\nLast line is empty\n\n'] = false,
166+
['ci: desc\n\nLast line is blank\n '] = false,
167+
168+
-- Footer
169+
-- No validation for footer
170+
171+
-- Bad wordings
172+
['ci: this has Fixed #1'] = false,
173+
['ci: this has fixed #1'] = false,
174+
['ci: this Fixes #1'] = false,
175+
['ci: this fixes #1'] = false,
176+
['ci: this will Fix #1'] = false,
177+
['ci: this will fix #1'] = false,
178+
['ci: this has Closed #1'] = false,
179+
['ci: this has closed #1'] = false,
180+
['ci: this Closes #1'] = false,
181+
['ci: this closes #1'] = false,
182+
['ci: this will Close #1'] = false,
183+
['ci: this will close #1'] = false,
184+
['ci: this has Resolved #1'] = false,
185+
['ci: this has resolved #1'] = false,
186+
['ci: this Resolves #1'] = false,
187+
['ci: this resolves #1'] = false,
188+
189+
['ci: desc\n\nthis has Fixed #1'] = false,
190+
['ci: desc\n\nthis has fixed #1'] = false,
191+
['ci: desc\n\nthis Fixes #1'] = false,
192+
['ci: desc\n\nthis fixes #1'] = false,
193+
['ci: desc\n\nthis will Fix #1'] = false,
194+
['ci: desc\n\nthis will fix #1'] = false,
195+
['ci: desc\n\nthis has Closed #1'] = false,
196+
['ci: desc\n\nthis has closed #1'] = false,
197+
['ci: desc\n\nthis Closes #1'] = false,
198+
['ci: desc\n\nthis closes #1'] = false,
199+
['ci: desc\n\nthis will Close #1'] = false,
200+
['ci: desc\n\nthis will close #1'] = false,
201+
['ci: desc\n\nthis has Resolved #1'] = false,
202+
['ci: desc\n\nthis has resolved #1'] = false,
203+
['ci: desc\n\nthis Resolves #1'] = false,
204+
['ci: desc\n\nthis resolves #1'] = false,
205+
}
206+
207+
_G.test_cases_failed = {}
208+
for message, expected in pairs(test_cases) do
209+
local is_valid = _G.validate_commit_msg(message)
210+
if is_valid ~= expected then
211+
table.insert(_G.test_cases_failed, { msg = message, expected = expected, actual = is_valid })
212+
end
213+
end

0 commit comments

Comments
 (0)