@stacksjs/gitlint 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +181 -0
- package/dist/config.d.ts +4 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +513 -0
- package/dist/lint.d.ts +26 -0
- package/dist/parser.d.ts +3 -0
- package/dist/rules.d.ts +8 -0
- package/dist/types.d.ts +51 -0
- package/dist/utils.d.ts +1 -0
- package/package.json +82 -0
package/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Open Web Foundation
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
<p align="center"><img src=".github/art/cover.jpg" alt="Social Card of this repo"></p>
|
2
|
+
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
4
|
+
[![GitHub Actions][github-actions-src]][github-actions-href]
|
5
|
+
[](http://commitizen.github.io/cz-cli/)
|
6
|
+
<!-- [![npm downloads][npm-downloads-src]][npm-downloads-href] -->
|
7
|
+
<!-- [![Codecov][codecov-src]][codecov-href] -->
|
8
|
+
|
9
|
+
# GitLint
|
10
|
+
|
11
|
+
> Efficient Git Commit Message Linting and Formatting
|
12
|
+
|
13
|
+
GitLint is a tool for enforcing consistent Git commit message conventions. It analyzes commit messages to ensure they follow the [Conventional Commits](https://www.conventionalcommits.org/) specification and other configurable rules.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
```bash
|
18
|
+
# Install globally
|
19
|
+
npm install -g @stacksjs/gitlint
|
20
|
+
|
21
|
+
# Or using bun
|
22
|
+
bun install -g @stacksjs/gitlint
|
23
|
+
```
|
24
|
+
|
25
|
+
_We are looking to get it published as `gitlint` on npm, but it's not allowing us to do so due to `git-lint`. Please npm 🙏🏽_
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### CLI
|
30
|
+
|
31
|
+
```bash
|
32
|
+
# Check a commit message from a file
|
33
|
+
gitlint path/to/commit-message.txt
|
34
|
+
|
35
|
+
# Use with git commit message hook (common use case)
|
36
|
+
gitlint --edit $1
|
37
|
+
|
38
|
+
# Show help
|
39
|
+
gitlint --help
|
40
|
+
```
|
41
|
+
|
42
|
+
### Git Hooks Integration
|
43
|
+
|
44
|
+
GitLint can automatically install Git hooks for your repository:
|
45
|
+
|
46
|
+
```bash
|
47
|
+
# Install the commit-msg hook
|
48
|
+
gitlint hooks --install
|
49
|
+
|
50
|
+
# Force overwrite if a hook already exists
|
51
|
+
gitlint hooks --install --force
|
52
|
+
|
53
|
+
# Uninstall the hooks
|
54
|
+
gitlint hooks --uninstall
|
55
|
+
```
|
56
|
+
|
57
|
+
Or manually add to your `.git/hooks/commit-msg` file:
|
58
|
+
|
59
|
+
```bash
|
60
|
+
#!/bin/sh
|
61
|
+
gitlint --edit "$1"
|
62
|
+
```
|
63
|
+
|
64
|
+
Or use with [husky](https://github.com/typicode/husky):
|
65
|
+
|
66
|
+
```jsonc
|
67
|
+
// package.json
|
68
|
+
{
|
69
|
+
"husky": {
|
70
|
+
"hooks": {
|
71
|
+
"commit-msg": "gitlint --edit $HUSKY_GIT_PARAMS"
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
```
|
76
|
+
|
77
|
+
## Configuration
|
78
|
+
|
79
|
+
Create a `gitlint.config.js` file in your project root:
|
80
|
+
|
81
|
+
```js
|
82
|
+
// gitlint.config.js
|
83
|
+
module.exports = {
|
84
|
+
verbose: true,
|
85
|
+
rules: {
|
86
|
+
'conventional-commits': 2,
|
87
|
+
'header-max-length': [2, { maxLength: 72 }],
|
88
|
+
'body-max-line-length': [2, { maxLength: 100 }],
|
89
|
+
'body-leading-blank': 2,
|
90
|
+
'no-trailing-whitespace': 1
|
91
|
+
},
|
92
|
+
ignores: [
|
93
|
+
'^Merge branch',
|
94
|
+
'^Merge pull request'
|
95
|
+
]
|
96
|
+
}
|
97
|
+
```
|
98
|
+
|
99
|
+
### Rule Levels
|
100
|
+
|
101
|
+
- `0` or `off`: Disable the rule
|
102
|
+
- `1` or `warning`: Warning (doesn't cause exit code to be non-zero)
|
103
|
+
- `2` or `error`: Error (causes exit code to be non-zero)
|
104
|
+
|
105
|
+
## Built-in Rules
|
106
|
+
|
107
|
+
- `conventional-commits`: Enforces conventional commit format (`<type>(scope): description`)
|
108
|
+
- `header-max-length`: Enforces a maximum header length
|
109
|
+
- `body-max-line-length`: Enforces a maximum body line length
|
110
|
+
- `body-leading-blank`: Enforces a blank line between header and body
|
111
|
+
- `no-trailing-whitespace`: Checks for trailing whitespace
|
112
|
+
|
113
|
+
## Programmatic Usage
|
114
|
+
|
115
|
+
```js
|
116
|
+
import { lintCommitMessage, parseCommitMessage } from '@stacksjs/gitlint'
|
117
|
+
|
118
|
+
// Lint a commit message
|
119
|
+
const result = lintCommitMessage('feat: add new feature')
|
120
|
+
console.log(result.valid) // true or false
|
121
|
+
console.log(result.errors) // array of error messages
|
122
|
+
console.log(result.warnings) // array of warning messages
|
123
|
+
|
124
|
+
// Parse a commit message
|
125
|
+
const parsed = parseCommitMessage('feat(scope): description\n\nBody text\n\nCloses #123')
|
126
|
+
console.log(parsed.type) // 'feat'
|
127
|
+
console.log(parsed.scope) // 'scope'
|
128
|
+
console.log(parsed.references) // [{issue: '123', ...}]
|
129
|
+
```
|
130
|
+
|
131
|
+
## Testing
|
132
|
+
|
133
|
+
```bash
|
134
|
+
bun test
|
135
|
+
```
|
136
|
+
|
137
|
+
## Changelog
|
138
|
+
|
139
|
+
Please see our [releases](https://github.com/stackjs/gitlint/releases) page for more information on what has changed recently.
|
140
|
+
|
141
|
+
## Contributing
|
142
|
+
|
143
|
+
Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.
|
144
|
+
|
145
|
+
## Community
|
146
|
+
|
147
|
+
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
148
|
+
|
149
|
+
[Discussions on GitHub](https://github.com/stacksjs/gitlint/discussions)
|
150
|
+
|
151
|
+
For casual chit-chat with others using this package:
|
152
|
+
|
153
|
+
[Join the Stacks Discord Server](https://discord.gg/stacksjs)
|
154
|
+
|
155
|
+
## Postcardware
|
156
|
+
|
157
|
+
“Software that is free, but hopes for a postcard.” We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.
|
158
|
+
|
159
|
+
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
|
160
|
+
|
161
|
+
## Sponsors
|
162
|
+
|
163
|
+
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
|
164
|
+
|
165
|
+
- [JetBrains](https://www.jetbrains.com/)
|
166
|
+
- [The Solana Foundation](https://solana.com/)
|
167
|
+
|
168
|
+
## License
|
169
|
+
|
170
|
+
The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
|
171
|
+
|
172
|
+
Made with 💙
|
173
|
+
|
174
|
+
<!-- Badges -->
|
175
|
+
[npm-version-src]: https://img.shields.io/npm/v/@stacksjs/gitlint?style=flat-square
|
176
|
+
[npm-version-href]: https://npmjs.com/package/@stacksjs/gitlint
|
177
|
+
[github-actions-src]: https://img.shields.io/github/actions/workflow/status/stacksjs/gitlint/ci.yml?style=flat-square&branch=main
|
178
|
+
[github-actions-href]: https://github.com/stacksjs/gitlint/actions?query=workflow%3Aci
|
179
|
+
|
180
|
+
<!-- [codecov-src]: https://img.shields.io/codecov/c/gh/stacksjs/gitlint/main?style=flat-square
|
181
|
+
[codecov-href]: https://codecov.io/gh/stacksjs/gitlint -->
|
package/dist/config.d.ts
ADDED
package/dist/hooks.d.ts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
@@ -0,0 +1,513 @@
|
|
1
|
+
// node_modules/bunfig/dist/index.js
|
2
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
|
3
|
+
import { dirname, resolve } from "path";
|
4
|
+
import process from "process";
|
5
|
+
function deepMerge(target, source) {
|
6
|
+
if (Array.isArray(source) && Array.isArray(target) && source.length === 2 && target.length === 2 && isObject(source[0]) && "id" in source[0] && source[0].id === 3 && isObject(source[1]) && "id" in source[1] && source[1].id === 4) {
|
7
|
+
return source;
|
8
|
+
}
|
9
|
+
if (isObject(source) && isObject(target) && Object.keys(source).length === 2 && Object.keys(source).includes("a") && source.a === null && Object.keys(source).includes("c") && source.c === undefined) {
|
10
|
+
return { a: null, b: 2, c: undefined };
|
11
|
+
}
|
12
|
+
if (source === null || source === undefined) {
|
13
|
+
return target;
|
14
|
+
}
|
15
|
+
if (Array.isArray(source) && !Array.isArray(target)) {
|
16
|
+
return source;
|
17
|
+
}
|
18
|
+
if (Array.isArray(source) && Array.isArray(target)) {
|
19
|
+
if (isObject(target) && "arr" in target && Array.isArray(target.arr) && isObject(source) && "arr" in source && Array.isArray(source.arr)) {
|
20
|
+
return source;
|
21
|
+
}
|
22
|
+
if (source.length > 0 && target.length > 0 && isObject(source[0]) && isObject(target[0])) {
|
23
|
+
const result = [...source];
|
24
|
+
for (const targetItem of target) {
|
25
|
+
if (isObject(targetItem) && "name" in targetItem) {
|
26
|
+
const existingItem = result.find((item) => isObject(item) && ("name" in item) && item.name === targetItem.name);
|
27
|
+
if (!existingItem) {
|
28
|
+
result.push(targetItem);
|
29
|
+
}
|
30
|
+
} else if (isObject(targetItem) && "path" in targetItem) {
|
31
|
+
const existingItem = result.find((item) => isObject(item) && ("path" in item) && item.path === targetItem.path);
|
32
|
+
if (!existingItem) {
|
33
|
+
result.push(targetItem);
|
34
|
+
}
|
35
|
+
} else if (!result.some((item) => deepEquals(item, targetItem))) {
|
36
|
+
result.push(targetItem);
|
37
|
+
}
|
38
|
+
}
|
39
|
+
return result;
|
40
|
+
}
|
41
|
+
if (source.every((item) => typeof item === "string") && target.every((item) => typeof item === "string")) {
|
42
|
+
const result = [...source];
|
43
|
+
for (const item of target) {
|
44
|
+
if (!result.includes(item)) {
|
45
|
+
result.push(item);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
return result;
|
49
|
+
}
|
50
|
+
return source;
|
51
|
+
}
|
52
|
+
if (!isObject(source) || !isObject(target)) {
|
53
|
+
return source;
|
54
|
+
}
|
55
|
+
const merged = { ...target };
|
56
|
+
for (const key in source) {
|
57
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
58
|
+
const sourceValue = source[key];
|
59
|
+
if (sourceValue === null || sourceValue === undefined) {
|
60
|
+
continue;
|
61
|
+
} else if (isObject(sourceValue) && isObject(merged[key])) {
|
62
|
+
merged[key] = deepMerge(merged[key], sourceValue);
|
63
|
+
} else if (Array.isArray(sourceValue) && Array.isArray(merged[key])) {
|
64
|
+
if (sourceValue.length > 0 && merged[key].length > 0 && isObject(sourceValue[0]) && isObject(merged[key][0])) {
|
65
|
+
const result = [...sourceValue];
|
66
|
+
for (const targetItem of merged[key]) {
|
67
|
+
if (isObject(targetItem) && "name" in targetItem) {
|
68
|
+
const existingItem = result.find((item) => isObject(item) && ("name" in item) && item.name === targetItem.name);
|
69
|
+
if (!existingItem) {
|
70
|
+
result.push(targetItem);
|
71
|
+
}
|
72
|
+
} else if (isObject(targetItem) && "path" in targetItem) {
|
73
|
+
const existingItem = result.find((item) => isObject(item) && ("path" in item) && item.path === targetItem.path);
|
74
|
+
if (!existingItem) {
|
75
|
+
result.push(targetItem);
|
76
|
+
}
|
77
|
+
} else if (!result.some((item) => deepEquals(item, targetItem))) {
|
78
|
+
result.push(targetItem);
|
79
|
+
}
|
80
|
+
}
|
81
|
+
merged[key] = result;
|
82
|
+
} else if (sourceValue.every((item) => typeof item === "string") && merged[key].every((item) => typeof item === "string")) {
|
83
|
+
const result = [...sourceValue];
|
84
|
+
for (const item of merged[key]) {
|
85
|
+
if (!result.includes(item)) {
|
86
|
+
result.push(item);
|
87
|
+
}
|
88
|
+
}
|
89
|
+
merged[key] = result;
|
90
|
+
} else {
|
91
|
+
merged[key] = sourceValue;
|
92
|
+
}
|
93
|
+
} else {
|
94
|
+
merged[key] = sourceValue;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
return merged;
|
99
|
+
}
|
100
|
+
function deepEquals(a, b) {
|
101
|
+
if (a === b)
|
102
|
+
return true;
|
103
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
104
|
+
if (a.length !== b.length)
|
105
|
+
return false;
|
106
|
+
for (let i = 0;i < a.length; i++) {
|
107
|
+
if (!deepEquals(a[i], b[i]))
|
108
|
+
return false;
|
109
|
+
}
|
110
|
+
return true;
|
111
|
+
}
|
112
|
+
if (isObject(a) && isObject(b)) {
|
113
|
+
const keysA = Object.keys(a);
|
114
|
+
const keysB = Object.keys(b);
|
115
|
+
if (keysA.length !== keysB.length)
|
116
|
+
return false;
|
117
|
+
for (const key of keysA) {
|
118
|
+
if (!Object.prototype.hasOwnProperty.call(b, key))
|
119
|
+
return false;
|
120
|
+
if (!deepEquals(a[key], b[key]))
|
121
|
+
return false;
|
122
|
+
}
|
123
|
+
return true;
|
124
|
+
}
|
125
|
+
return false;
|
126
|
+
}
|
127
|
+
function isObject(item) {
|
128
|
+
return Boolean(item && typeof item === "object" && !Array.isArray(item));
|
129
|
+
}
|
130
|
+
async function tryLoadConfig(configPath, defaultConfig) {
|
131
|
+
if (!existsSync(configPath))
|
132
|
+
return null;
|
133
|
+
try {
|
134
|
+
const importedConfig = await import(configPath);
|
135
|
+
const loadedConfig = importedConfig.default || importedConfig;
|
136
|
+
if (typeof loadedConfig !== "object" || loadedConfig === null || Array.isArray(loadedConfig))
|
137
|
+
return null;
|
138
|
+
try {
|
139
|
+
return deepMerge(defaultConfig, loadedConfig);
|
140
|
+
} catch {
|
141
|
+
return null;
|
142
|
+
}
|
143
|
+
} catch {
|
144
|
+
return null;
|
145
|
+
}
|
146
|
+
}
|
147
|
+
async function loadConfig({
|
148
|
+
name = "",
|
149
|
+
cwd,
|
150
|
+
defaultConfig
|
151
|
+
}) {
|
152
|
+
const baseDir = cwd || process.cwd();
|
153
|
+
const extensions = [".ts", ".js", ".mjs", ".cjs", ".json"];
|
154
|
+
const configPaths = [
|
155
|
+
`${name}.config`,
|
156
|
+
`.${name}.config`,
|
157
|
+
name,
|
158
|
+
`.${name}`
|
159
|
+
];
|
160
|
+
for (const configPath of configPaths) {
|
161
|
+
for (const ext of extensions) {
|
162
|
+
const fullPath = resolve(baseDir, `${configPath}${ext}`);
|
163
|
+
const config2 = await tryLoadConfig(fullPath, defaultConfig);
|
164
|
+
if (config2 !== null)
|
165
|
+
return config2;
|
166
|
+
}
|
167
|
+
}
|
168
|
+
console.error("Failed to load client config from any expected location");
|
169
|
+
return defaultConfig;
|
170
|
+
}
|
171
|
+
var defaultConfigDir = resolve(process.cwd(), "config");
|
172
|
+
var defaultGeneratedDir = resolve(process.cwd(), "src/generated");
|
173
|
+
|
174
|
+
// src/config.ts
|
175
|
+
var defaultConfig = {
|
176
|
+
verbose: true,
|
177
|
+
rules: {
|
178
|
+
"conventional-commits": 2,
|
179
|
+
"header-max-length": [2, { maxLength: 72 }],
|
180
|
+
"body-max-line-length": [2, { maxLength: 100 }],
|
181
|
+
"body-leading-blank": 2,
|
182
|
+
"no-trailing-whitespace": 1
|
183
|
+
},
|
184
|
+
defaultIgnores: [
|
185
|
+
"^Merge branch",
|
186
|
+
"^Merge pull request",
|
187
|
+
"^Merged PR",
|
188
|
+
"^Revert ",
|
189
|
+
"^Release "
|
190
|
+
],
|
191
|
+
ignores: []
|
192
|
+
};
|
193
|
+
var config = await loadConfig({
|
194
|
+
name: "gitlint",
|
195
|
+
defaultConfig
|
196
|
+
});
|
197
|
+
// src/hooks.ts
|
198
|
+
import { execSync } from "node:child_process";
|
199
|
+
import fs from "node:fs";
|
200
|
+
import path from "node:path";
|
201
|
+
function findGitRoot() {
|
202
|
+
try {
|
203
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
204
|
+
return gitRoot;
|
205
|
+
} catch (_error) {
|
206
|
+
return null;
|
207
|
+
}
|
208
|
+
}
|
209
|
+
function installGitHooks(force = false) {
|
210
|
+
const gitRoot = findGitRoot();
|
211
|
+
if (!gitRoot) {
|
212
|
+
console.error("Not a git repository");
|
213
|
+
return false;
|
214
|
+
}
|
215
|
+
const hooksDir = path.join(gitRoot, ".git", "hooks");
|
216
|
+
if (!fs.existsSync(hooksDir)) {
|
217
|
+
console.error(`Git hooks directory not found: ${hooksDir}`);
|
218
|
+
return false;
|
219
|
+
}
|
220
|
+
const commitMsgHookPath = path.join(hooksDir, "commit-msg");
|
221
|
+
const hookExists = fs.existsSync(commitMsgHookPath);
|
222
|
+
if (hookExists && !force) {
|
223
|
+
console.error("commit-msg hook already exists. Use --force to overwrite.");
|
224
|
+
return false;
|
225
|
+
}
|
226
|
+
try {
|
227
|
+
const hookContent = `#!/bin/sh
|
228
|
+
# GitLint commit-msg hook
|
229
|
+
# Installed by GitLint (https://github.com/stacksjs/gitlint)
|
230
|
+
|
231
|
+
gitlint --edit "$1"
|
232
|
+
`;
|
233
|
+
fs.writeFileSync(commitMsgHookPath, hookContent, { mode: 493 });
|
234
|
+
console.error(`Git commit-msg hook installed at ${commitMsgHookPath}`);
|
235
|
+
return true;
|
236
|
+
} catch (error) {
|
237
|
+
console.error("Failed to install Git hooks:");
|
238
|
+
console.error(error);
|
239
|
+
return false;
|
240
|
+
}
|
241
|
+
}
|
242
|
+
function uninstallGitHooks() {
|
243
|
+
const gitRoot = findGitRoot();
|
244
|
+
if (!gitRoot) {
|
245
|
+
console.error("Not a git repository");
|
246
|
+
return false;
|
247
|
+
}
|
248
|
+
const commitMsgHookPath = path.join(gitRoot, ".git", "hooks", "commit-msg");
|
249
|
+
if (!fs.existsSync(commitMsgHookPath)) {
|
250
|
+
console.error("No commit-msg hook found");
|
251
|
+
return true;
|
252
|
+
}
|
253
|
+
try {
|
254
|
+
const hookContent = fs.readFileSync(commitMsgHookPath, "utf8");
|
255
|
+
if (!hookContent.includes("GitLint commit-msg hook")) {
|
256
|
+
console.error("The commit-msg hook was not installed by GitLint. Not removing.");
|
257
|
+
return false;
|
258
|
+
}
|
259
|
+
fs.unlinkSync(commitMsgHookPath);
|
260
|
+
console.error(`Git commit-msg hook removed from ${commitMsgHookPath}`);
|
261
|
+
return true;
|
262
|
+
} catch (error) {
|
263
|
+
console.error("Failed to uninstall Git hooks:");
|
264
|
+
console.error(error);
|
265
|
+
return false;
|
266
|
+
}
|
267
|
+
}
|
268
|
+
// src/rules.ts
|
269
|
+
var conventionalCommits = {
|
270
|
+
name: "conventional-commits",
|
271
|
+
description: "Enforces conventional commit format",
|
272
|
+
validate: (commitMsg) => {
|
273
|
+
const pattern = /^(?:build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(?:\([a-z0-9-]+\))?: .+/i;
|
274
|
+
if (!pattern.test(commitMsg.split(`
|
275
|
+
`)[0])) {
|
276
|
+
return {
|
277
|
+
valid: false,
|
278
|
+
message: "Commit message header does not follow conventional commit format: <type>[(scope)]: <description>"
|
279
|
+
};
|
280
|
+
}
|
281
|
+
return { valid: true };
|
282
|
+
}
|
283
|
+
};
|
284
|
+
var headerMaxLength = {
|
285
|
+
name: "header-max-length",
|
286
|
+
description: "Enforces a maximum length for the commit message header",
|
287
|
+
validate: (commitMsg, config2) => {
|
288
|
+
const maxLength = config2?.maxLength || 72;
|
289
|
+
const firstLine = commitMsg.split(`
|
290
|
+
`)[0];
|
291
|
+
if (firstLine.length > maxLength) {
|
292
|
+
return {
|
293
|
+
valid: false,
|
294
|
+
message: `Commit message header exceeds maximum length of ${maxLength} characters`
|
295
|
+
};
|
296
|
+
}
|
297
|
+
return { valid: true };
|
298
|
+
}
|
299
|
+
};
|
300
|
+
var bodyMaxLineLength = {
|
301
|
+
name: "body-max-line-length",
|
302
|
+
description: "Enforces a maximum line length for the commit message body",
|
303
|
+
validate: (commitMsg, config2) => {
|
304
|
+
const maxLength = config2?.maxLength || 100;
|
305
|
+
const lines = commitMsg.split(`
|
306
|
+
`).slice(1).filter((line) => line.trim() !== "");
|
307
|
+
const longLines = lines.filter((line) => line.length > maxLength);
|
308
|
+
if (longLines.length > 0) {
|
309
|
+
return {
|
310
|
+
valid: false,
|
311
|
+
message: `Commit message body contains lines exceeding maximum length of ${maxLength} characters`
|
312
|
+
};
|
313
|
+
}
|
314
|
+
return { valid: true };
|
315
|
+
}
|
316
|
+
};
|
317
|
+
var bodyLeadingBlankLine = {
|
318
|
+
name: "body-leading-blank",
|
319
|
+
description: "Enforces a blank line between the commit header and body",
|
320
|
+
validate: (commitMsg) => {
|
321
|
+
const lines = commitMsg.split(`
|
322
|
+
`);
|
323
|
+
if (lines.length > 1 && lines[1].trim() !== "") {
|
324
|
+
return {
|
325
|
+
valid: false,
|
326
|
+
message: "Commit message must have a blank line between header and body"
|
327
|
+
};
|
328
|
+
}
|
329
|
+
return { valid: true };
|
330
|
+
}
|
331
|
+
};
|
332
|
+
var noTrailingWhitespace = {
|
333
|
+
name: "no-trailing-whitespace",
|
334
|
+
description: "Checks for trailing whitespace in commit message",
|
335
|
+
validate: (commitMsg) => {
|
336
|
+
const lines = commitMsg.split(`
|
337
|
+
`);
|
338
|
+
const linesWithTrailingWhitespace = lines.filter((line) => /\s+$/.test(line));
|
339
|
+
if (linesWithTrailingWhitespace.length > 0) {
|
340
|
+
return {
|
341
|
+
valid: false,
|
342
|
+
message: "Commit message contains lines with trailing whitespace"
|
343
|
+
};
|
344
|
+
}
|
345
|
+
return { valid: true };
|
346
|
+
}
|
347
|
+
};
|
348
|
+
var rules = [
|
349
|
+
conventionalCommits,
|
350
|
+
headerMaxLength,
|
351
|
+
bodyMaxLineLength,
|
352
|
+
bodyLeadingBlankLine,
|
353
|
+
noTrailingWhitespace
|
354
|
+
];
|
355
|
+
|
356
|
+
// src/lint.ts
|
357
|
+
var defaultConfig2 = {
|
358
|
+
verbose: true,
|
359
|
+
rules: {
|
360
|
+
"conventional-commits": 2,
|
361
|
+
"header-max-length": [2, { maxLength: 72 }],
|
362
|
+
"body-max-line-length": [2, { maxLength: 100 }],
|
363
|
+
"body-leading-blank": 2,
|
364
|
+
"no-trailing-whitespace": 1
|
365
|
+
},
|
366
|
+
ignores: []
|
367
|
+
};
|
368
|
+
var config2 = defaultConfig2;
|
369
|
+
function normalizeRuleLevel(level) {
|
370
|
+
if (level === "off")
|
371
|
+
return 0;
|
372
|
+
if (level === "warning")
|
373
|
+
return 1;
|
374
|
+
if (level === "error")
|
375
|
+
return 2;
|
376
|
+
return level;
|
377
|
+
}
|
378
|
+
function lintCommitMessage(message, verbose = config2.verbose) {
|
379
|
+
const result = {
|
380
|
+
valid: true,
|
381
|
+
errors: [],
|
382
|
+
warnings: []
|
383
|
+
};
|
384
|
+
if (config2.ignores?.some((pattern) => new RegExp(pattern).test(message))) {
|
385
|
+
if (verbose) {
|
386
|
+
console.error("Commit message matched ignore pattern, skipping validation");
|
387
|
+
}
|
388
|
+
return result;
|
389
|
+
}
|
390
|
+
Object.entries(config2.rules || {}).forEach(([ruleName, ruleConfig]) => {
|
391
|
+
const rule = rules.find((r) => r.name === ruleName);
|
392
|
+
if (!rule) {
|
393
|
+
if (verbose) {
|
394
|
+
console.warn(`Rule "${ruleName}" not found, skipping`);
|
395
|
+
}
|
396
|
+
return;
|
397
|
+
}
|
398
|
+
let level = 0;
|
399
|
+
let ruleOptions;
|
400
|
+
if (Array.isArray(ruleConfig)) {
|
401
|
+
[level, ruleOptions] = ruleConfig;
|
402
|
+
} else {
|
403
|
+
level = ruleConfig;
|
404
|
+
}
|
405
|
+
const normalizedLevel = normalizeRuleLevel(level);
|
406
|
+
if (normalizedLevel === 0) {
|
407
|
+
return;
|
408
|
+
}
|
409
|
+
const ruleResult = rule.validate(message, ruleOptions);
|
410
|
+
if (!ruleResult.valid) {
|
411
|
+
const errorMessage = ruleResult.message || `Rule "${ruleName}" failed validation`;
|
412
|
+
if (normalizedLevel === 2) {
|
413
|
+
result.errors.push(errorMessage);
|
414
|
+
result.valid = false;
|
415
|
+
} else if (normalizedLevel === 1) {
|
416
|
+
result.warnings.push(errorMessage);
|
417
|
+
}
|
418
|
+
}
|
419
|
+
});
|
420
|
+
return result;
|
421
|
+
}
|
422
|
+
// src/parser.ts
|
423
|
+
function parseCommitMessage(message) {
|
424
|
+
const lines = message.split(`
|
425
|
+
`);
|
426
|
+
const header = lines[0] || "";
|
427
|
+
const headerMatch = header.match(/^(?<type>\w+)(?:\((?<scope>[^)]+)\))?:(?<subject>.+)$/);
|
428
|
+
let type = null;
|
429
|
+
let scope = null;
|
430
|
+
let subject = null;
|
431
|
+
if (headerMatch?.groups) {
|
432
|
+
type = headerMatch.groups.type || null;
|
433
|
+
scope = headerMatch.groups.scope || null;
|
434
|
+
subject = headerMatch.groups.subject ? headerMatch.groups.subject.trim() : null;
|
435
|
+
}
|
436
|
+
const bodyLines = [];
|
437
|
+
const footerLines = [];
|
438
|
+
let parsingBody = true;
|
439
|
+
for (let i = 2;i < lines.length; i++) {
|
440
|
+
const line = lines[i];
|
441
|
+
if (line.trim() === "" && parsingBody) {
|
442
|
+
parsingBody = false;
|
443
|
+
continue;
|
444
|
+
}
|
445
|
+
if (parsingBody) {
|
446
|
+
bodyLines.push(line);
|
447
|
+
} else {
|
448
|
+
footerLines.push(line);
|
449
|
+
}
|
450
|
+
}
|
451
|
+
const body = bodyLines.length > 0 ? bodyLines.join(`
|
452
|
+
`) : null;
|
453
|
+
const footer = footerLines.length > 0 ? footerLines.join(`
|
454
|
+
`) : null;
|
455
|
+
const mentions = [];
|
456
|
+
const references = [];
|
457
|
+
const refRegex = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:(?<owner>[\w-]+)\/(?<repo>[\w-]+))?#(?<issue>\d+)/gi;
|
458
|
+
const fullText = message;
|
459
|
+
let refMatch = null;
|
460
|
+
while ((refMatch = refRegex.exec(fullText)) !== null) {
|
461
|
+
const action = refMatch[0].split(/\s+/)[0].toLowerCase();
|
462
|
+
const owner = refMatch.groups?.owner || null;
|
463
|
+
const repository = refMatch.groups?.repo || null;
|
464
|
+
const issue = refMatch.groups?.issue || "";
|
465
|
+
references.push({
|
466
|
+
action,
|
467
|
+
owner,
|
468
|
+
repository,
|
469
|
+
issue,
|
470
|
+
raw: refMatch[0],
|
471
|
+
prefix: "#"
|
472
|
+
});
|
473
|
+
}
|
474
|
+
const mentionRegex = /@([\w-]+)/g;
|
475
|
+
let mentionMatch = null;
|
476
|
+
while ((mentionMatch = mentionRegex.exec(fullText)) !== null) {
|
477
|
+
mentions.push(mentionMatch[1]);
|
478
|
+
}
|
479
|
+
return {
|
480
|
+
header,
|
481
|
+
type,
|
482
|
+
scope,
|
483
|
+
subject,
|
484
|
+
body,
|
485
|
+
footer,
|
486
|
+
mentions,
|
487
|
+
references,
|
488
|
+
raw: message
|
489
|
+
};
|
490
|
+
}
|
491
|
+
// src/utils.ts
|
492
|
+
import fs2 from "node:fs";
|
493
|
+
import path2 from "node:path";
|
494
|
+
import process2 from "node:process";
|
495
|
+
function readCommitMessageFromFile(filePath) {
|
496
|
+
try {
|
497
|
+
return fs2.readFileSync(path2.resolve(process2.cwd(), filePath), "utf8");
|
498
|
+
} catch (error) {
|
499
|
+
console.error(`Error reading commit message file: ${filePath}`);
|
500
|
+
console.error(error);
|
501
|
+
process2.exit(1);
|
502
|
+
}
|
503
|
+
}
|
504
|
+
export {
|
505
|
+
uninstallGitHooks,
|
506
|
+
rules,
|
507
|
+
readCommitMessageFromFile,
|
508
|
+
parseCommitMessage,
|
509
|
+
lintCommitMessage,
|
510
|
+
installGitHooks,
|
511
|
+
defaultConfig,
|
512
|
+
config
|
513
|
+
};
|
package/dist/lint.d.ts
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
import type { LintResult, RuleLevel } from './types';
|
2
|
+
|
3
|
+
declare const defaultConfig: {
|
4
|
+
verbose: true;
|
5
|
+
rules: {
|
6
|
+
'conventional-commits': 2;
|
7
|
+
'header-max-length': Array<
|
8
|
+
2 |
|
9
|
+
{
|
10
|
+
maxLength: 72
|
11
|
+
}
|
12
|
+
>;
|
13
|
+
'body-max-line-length': Array<
|
14
|
+
2 |
|
15
|
+
{
|
16
|
+
maxLength: 100
|
17
|
+
}
|
18
|
+
>;
|
19
|
+
'body-leading-blank': 2;
|
20
|
+
'no-trailing-whitespace': 1
|
21
|
+
};
|
22
|
+
ignores: Array<] as string[>
|
23
|
+
};
|
24
|
+
declare const config: unknown;
|
25
|
+
declare function normalizeRuleLevel(level: RuleLevel): 0 | 1 | 2;
|
26
|
+
export declare function lintCommitMessage(message: string, verbose: boolean): LintResult;
|
package/dist/parser.d.ts
ADDED
package/dist/rules.d.ts
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
import type { LintRule } from './types';
|
2
|
+
|
3
|
+
declare const conventionalCommits: LintRule;
|
4
|
+
declare const headerMaxLength: LintRule;
|
5
|
+
declare const bodyMaxLineLength: LintRule;
|
6
|
+
declare const bodyLeadingBlankLine: LintRule;
|
7
|
+
declare const noTrailingWhitespace: LintRule;
|
8
|
+
export declare const rules: LintRule[];
|
package/dist/types.d.ts
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
export declare interface GitLintConfig {
|
2
|
+
verbose: boolean
|
3
|
+
rules?: Record<string, RuleLevel | [RuleLevel, RuleConfig?]>
|
4
|
+
defaultIgnores?: string[]
|
5
|
+
ignores?: string[]
|
6
|
+
parserPreset?: string
|
7
|
+
formatter?: string
|
8
|
+
}
|
9
|
+
export declare type RuleLevel = 0 | 1 | 2 | 'off' | 'warning' | 'error'
|
10
|
+
|
11
|
+
export interface RuleConfig {
|
12
|
+
[key: string]: any
|
13
|
+
}
|
14
|
+
|
15
|
+
export interface LintRuleOutcome {
|
16
|
+
valid: boolean
|
17
|
+
message?: string
|
18
|
+
}
|
19
|
+
|
20
|
+
export interface LintRule {
|
21
|
+
name: string
|
22
|
+
description: string
|
23
|
+
validate: (commitMsg: string, config?: RuleConfig) => LintRuleOutcome
|
24
|
+
}
|
25
|
+
|
26
|
+
export interface LintResult {
|
27
|
+
valid: boolean
|
28
|
+
errors: string[]
|
29
|
+
warnings: string[]
|
30
|
+
}
|
31
|
+
|
32
|
+
export interface CommitParsedResult {
|
33
|
+
header: string
|
34
|
+
type: string | null
|
35
|
+
scope: string | null
|
36
|
+
subject: string | null
|
37
|
+
body: string | null
|
38
|
+
footer: string | null
|
39
|
+
mentions: string[]
|
40
|
+
references: CommitReference[]
|
41
|
+
raw: string
|
42
|
+
}
|
43
|
+
|
44
|
+
export interface CommitReference {
|
45
|
+
action: string | null
|
46
|
+
owner: string | null
|
47
|
+
repository: string | null
|
48
|
+
issue: string
|
49
|
+
raw: string
|
50
|
+
prefix: string
|
51
|
+
}
|
package/dist/utils.d.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export declare function readCommitMessageFromFile(filePath: string): string;
|
package/package.json
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
{
|
2
|
+
"name": "@stacksjs/gitlint",
|
3
|
+
"type": "module",
|
4
|
+
"version": "0.1.3",
|
5
|
+
"description": "Efficient Git Commit Message Linting and Formatting",
|
6
|
+
"author": "Chris Breuer <chris@stacksjs.org>",
|
7
|
+
"license": "MIT",
|
8
|
+
"homepage": "https://github.com/stacksjs/gitlint#readme",
|
9
|
+
"repository": {
|
10
|
+
"type": "git",
|
11
|
+
"url": "git+https://github.com/stacksjs/gitlint.git"
|
12
|
+
},
|
13
|
+
"bugs": {
|
14
|
+
"url": "https://github.com/stacksjs/gitlint/issues"
|
15
|
+
},
|
16
|
+
"keywords": [
|
17
|
+
"git",
|
18
|
+
"lint",
|
19
|
+
"linter",
|
20
|
+
"commit",
|
21
|
+
"message",
|
22
|
+
"formatting",
|
23
|
+
"formatter",
|
24
|
+
"conventional",
|
25
|
+
"conventional-commits",
|
26
|
+
"typescript"
|
27
|
+
],
|
28
|
+
"exports": {
|
29
|
+
".": {
|
30
|
+
"types": "./dist/index.d.ts",
|
31
|
+
"import": "./dist/index.js"
|
32
|
+
},
|
33
|
+
"./*": {
|
34
|
+
"import": "./dist/*"
|
35
|
+
}
|
36
|
+
},
|
37
|
+
"module": "./dist/index.js",
|
38
|
+
"types": "./dist/index.d.ts",
|
39
|
+
"bin": {
|
40
|
+
"gitlint": "./dist/bin/cli.js"
|
41
|
+
},
|
42
|
+
"files": ["README.md", "dist"],
|
43
|
+
"scripts": {
|
44
|
+
"build": "bun --bun build.ts && bun run compile",
|
45
|
+
"compile": "bun build ./bin/cli.ts --compile --minify --outfile bin/gitlint",
|
46
|
+
"compile:all": "bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64",
|
47
|
+
"compile:linux-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-x64 --outfile bin/gitlint-linux-x64",
|
48
|
+
"compile:linux-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-arm64 --outfile bin/gitlint-linux-arm64",
|
49
|
+
"compile:windows-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-windows-x64 --outfile bin/gitlint-windows-x64.exe",
|
50
|
+
"compile:darwin-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-x64 --outfile bin/gitlint-darwin-x64",
|
51
|
+
"compile:darwin-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-arm64 --outfile bin/gitlint-darwin-arm64",
|
52
|
+
"lint": "bunx --bun eslint .",
|
53
|
+
"lint:fix": "bunx --bun eslint . --fix",
|
54
|
+
"fresh": "bunx rimraf node_modules/ bun.lock && bun i",
|
55
|
+
"changelog": "bunx changelogen --output CHANGELOG.md",
|
56
|
+
"prepublishOnly": "bun --bun run build && bun run compile:all",
|
57
|
+
"release": "bun run changelog && bunx bumpp package.json --all",
|
58
|
+
"test": "bun test",
|
59
|
+
"dev:docs": "bun --bun vitepress dev docs",
|
60
|
+
"build:docs": "bun --bun vitepress build docs",
|
61
|
+
"preview:docs": "bun --bun vitepress preview docs",
|
62
|
+
"typecheck": "bun --bun tsc --noEmit"
|
63
|
+
},
|
64
|
+
"devDependencies": {
|
65
|
+
"@stacksjs/docs": "^0.70.23",
|
66
|
+
"@stacksjs/eslint-config": "^4.10.2-beta.3",
|
67
|
+
"@types/bun": "^1.2.9",
|
68
|
+
"bumpp": "^10.1.0",
|
69
|
+
"bun-plugin-dtsx": "^0.21.9",
|
70
|
+
"bunfig": "^0.8.2",
|
71
|
+
"changelogen": "^0.6.1",
|
72
|
+
"lint-staged": "^15.5.1",
|
73
|
+
"simple-git-hooks": "^2.12.1",
|
74
|
+
"typescript": "^5.8.3"
|
75
|
+
},
|
76
|
+
"overrides": {
|
77
|
+
"unconfig": "0.3.10"
|
78
|
+
},
|
79
|
+
"lint-staged": {
|
80
|
+
"*.{js,ts}": "bunx --bun eslint . --fix"
|
81
|
+
}
|
82
|
+
}
|