├── pnpm-workspace.yaml
├── commitlinterrc.json
├── .husky
├── pre-commit
├── commit-msg
└── pre-push
├── assets
├── demo-3.png
├── demo-4.png
├── demo-6.png
├── demo-6-zh-cn.png
├── demo-4-compressed.png
├── demo-6-compressed.png
├── demo-7-compressed.png
├── demo-6-zh-cn-compressed.png
├── detailed-config-wx-compressed.png
└── docs.md
├── .vscode
└── settings.json
├── .gitignore
├── packages
├── git-commit-msg-linter
│ ├── index.d.ts
│ ├── utils.js
│ ├── constants.js
│ ├── typings.d.ts
│ ├── test
│ │ └── e2e.sh
│ ├── index.js
│ ├── README-zh-CN.md
│ ├── package.json
│ ├── uninstall.js
│ ├── install.js
│ ├── cli
│ │ └── validate.js
│ ├── core
│ │ └── validate.js
│ └── README.md
├── commit-msg-linter
│ ├── index.js
│ ├── package.json
│ └── commit-msg-linter.js
└── commit-msg-linter-test
│ └── package.json
├── .editorconfig
├── .eslintrc.js
├── package.json
├── README-zh-CN.md
├── LICENSE
└── README.md
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 |
--------------------------------------------------------------------------------
/commitlinterrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "max-len": 90,
3 | "lang": "zh-CN",
4 | "debug": false
5 | }
6 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/assets/demo-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/legend80s/git-commit-msg-linter/HEAD/assets/demo-3.png
--------------------------------------------------------------------------------
/assets/demo-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/legend80s/git-commit-msg-linter/HEAD/assets/demo-4.png
--------------------------------------------------------------------------------
/assets/demo-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/legend80s/git-commit-msg-linter/HEAD/assets/demo-6.png
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | .git/hooks/commit-msg $1
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "commitlinterrc",
4 | "EDITMSG"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/assets/demo-6-zh-cn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/legend80s/git-commit-msg-linter/HEAD/assets/demo-6-zh-cn.png
--------------------------------------------------------------------------------
/assets/demo-4-compressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/legend80s/git-commit-msg-linter/HEAD/assets/demo-4-compressed.png
--------------------------------------------------------------------------------
/assets/demo-6-compressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/legend80s/git-commit-msg-linter/HEAD/assets/demo-6-compressed.png
--------------------------------------------------------------------------------
/assets/demo-7-compressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/legend80s/git-commit-msg-linter/HEAD/assets/demo-7-compressed.png
--------------------------------------------------------------------------------
/assets/demo-6-zh-cn-compressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/legend80s/git-commit-msg-linter/HEAD/assets/demo-6-zh-cn-compressed.png
--------------------------------------------------------------------------------
/assets/detailed-config-wx-compressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/legend80s/git-commit-msg-linter/HEAD/assets/detailed-config-wx-compressed.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | *.tgz
4 | git-commit-msg-linter.code-workspace
5 | .pnpm-debug.log
6 | pnpm-lock.yaml
7 | package-lock.json
8 | yarn.lock
9 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/index.d.ts:
--------------------------------------------------------------------------------
1 | export interface InstallOptions {
2 | silent?: boolean
3 | }
4 |
5 | export function install(options?: InstallOptions): void
6 |
7 | export function uninstall(): void
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * exit current process with success code 0
3 | * instead of destroying the whole npm install process.
4 | */
5 | module.exports.bailOut = function bailOut() {
6 | process.exit(0);
7 | };
8 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'airbnb-base',
3 | rules: {
4 | 'max-len': ['off', { ignoreComments: true }],
5 | 'no-unused-expressions': ['error', { allowShortCircuit: true }],
6 | 'no-use-before-define': ['error', { functions: false }],
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/packages/commit-msg-linter/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | function getLinterPath() {
4 | // fix https://github.com/legend80s/git-commit-msg-linter/issues/38
5 | return path.resolve(__dirname, 'commit-msg-linter.js').replace(/\\/g, '/');
6 | }
7 |
8 | exports.getLinterPath = getLinterPath;
9 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | echo '[GCML] prepush hook run.'
5 |
6 | if [[ $publishing == '1' ]]; then
7 | # do nothing during publishing because npm test has run in the `preversion` phase
8 | echo ''
9 | else
10 | cd packages/git-commit-msg-linter
11 | npm test
12 | fi
13 |
--------------------------------------------------------------------------------
/packages/commit-msg-linter-test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "commit-msg-linter-test",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC"
13 | }
14 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/constants.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 | const path = require('path');
3 | const json = require('./package.json');
4 |
5 | const PACKAGE_NAME = json.name;
6 | const COMMIT_MSG_HOOK_FILE = 'commit-msg';
7 |
8 | exports.PACKAGE_NAME = PACKAGE_NAME;
9 | exports.COMMIT_MSG_HOOK_FILE = COMMIT_MSG_HOOK_FILE;
10 |
11 | exports.PACKAGE_NAME_LABEL = `[${chalk.yellow(PACKAGE_NAME)}]`;
12 | exports.COMMIT_MSG_LABEL = chalk.bold(COMMIT_MSG_HOOK_FILE);
13 |
14 | exports.PROJECT_ROOT = path.resolve(__dirname, '..', '..');
15 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/typings.d.ts:
--------------------------------------------------------------------------------
1 | export interface IConfigLinterRC {
2 | types: string[];
3 | 'max-len': number;
4 | 'min-len': number;
5 | debug: boolean;
6 | showInvalidHeader: boolean;
7 |
8 | /**
9 | * default false
10 | */
11 | scopeRequired: boolean;
12 | validScopes: string | string[];
13 |
14 | /** default false */
15 | englishOnly: boolean;
16 | example: string;
17 |
18 | scopeDescriptions: string;
19 | invalidScopeDescriptions: string;
20 |
21 | subjectDescriptions: string;
22 | invalidSubjectDescriptions: string;
23 |
24 | postSubjectDescriptions: string[];
25 | lang: string;
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "workspaces": [
4 | "packages/*"
5 | ],
6 | "scripts": {
7 | "pub:patch": "cd packages/git-commit-msg-linter && npm run pub:patch && cd ../..",
8 | "pub:minor": "cd packages/git-commit-msg-linter && npm run pub:minor && cd ../..",
9 | "pub:major": "cd packages/git-commit-msg-linter && npm run pub:major && cd ../..",
10 | "lint": "eslint **/*.js",
11 | "lint:fix": "eslint --fix **/*.js"
12 | },
13 | "devDependencies": {
14 | "eslint": "^7.28.0",
15 | "eslint-config-airbnb-base": "^14.2.1",
16 | "eslint-plugin-import": "^2.23.4",
17 | "git-commit-msg-linter": "workspace:*",
18 | "husky": "^8.0.3",
19 | "lint-staged": "^13.1.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/test/e2e.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cd ../commit-msg-linter-test
4 | echo "test: here we are: $(pwd)."
5 |
6 | # clean hooks before testing
7 | # rm .git/hooks/commit-msg
8 |
9 | # npm i ../git-commit-msg-linter --save-dev
10 |
11 | # `install` not run when npm install a local directory,
12 | # so we run it manually.
13 | # node node_modules/git-commit-msg-linter/install.js
14 |
15 | # echo $(date '+%F %T') >> README.md
16 |
17 | # out=$(git commit -am "test" 2>&1)
18 |
19 | # # echo out1=$out
20 |
21 | # substr1='Invalid Git Commit Message'
22 | # substr2='Invalid length: Length 4'
23 |
24 | # if [[ "$out" == *"$substr1"* && "$out" == *"$substr2"* ]]; then
25 | # echo '✅ SUCESS'
26 | # exit 0
27 | # else
28 | # echo '❌ FAILED'
29 | # exit 1
30 | # fi
31 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/index.js:
--------------------------------------------------------------------------------
1 | const cp = require('child_process');
2 | const path = require('path');
3 |
4 | const INSTALL_SCRIPT = path.join(__dirname, 'install.js');
5 | const UNINSTALL_SCRIPT = path.join(__dirname, 'uninstall.js');
6 |
7 | function install(options = {}) {
8 | const result = cp.spawnSync(
9 | 'node',
10 | [
11 | INSTALL_SCRIPT,
12 | ...(options.silent ? ['--silent'] : []),
13 | ],
14 | { stdio: 'inherit' },
15 | );
16 | if (result.status !== 0) {
17 | throw new Error('Install git-commit-msg-linter failed!');
18 | }
19 | }
20 |
21 | function uninstall() {
22 | const result = cp.spawnSync('node', [UNINSTALL_SCRIPT], { stdio: 'inherit' });
23 | if (result.status !== 0) {
24 | throw new Error('Uninstall git-commit-msg-linter failed!');
25 | }
26 | }
27 |
28 | exports.install = install;
29 | exports.uninstall = uninstall;
30 |
--------------------------------------------------------------------------------
/README-zh-CN.md:
--------------------------------------------------------------------------------
1 | # git-commit-msg-linter
2 |
3 |
4 |
5 |
6 |
7 | > 👀 规范开发者的每一行提交信息,为团队定制专属的 Git 提交信息规范
8 |
9 | ## 安装
10 |
11 | ```sh
12 | npm install git-commit-msg-linter --save-dev
13 | ```
14 |
15 | **只需安装无需配置**,提交信息已处于 lint 状态,现在去提交代码试试。
16 |
17 | ## 设置提示语言
18 |
19 | 默认使用系统设置语言(`$ node -p 'Intl.DateTimeFormat().resolvedOptions().locale'`),可通过以下两种方式自定义语言,支持中文(zh-CN)、英文(en-US)葡萄牙语(pt-BR)和 es-ES。以下配置顺序优先级从高到低:
20 |
21 | ### 通过 commitlinterrc.json 设置
22 |
23 | ```json
24 | {
25 | "lang": "zh-CN"
26 | }
27 | ```
28 |
29 | ### 通过环境变量设置
30 |
31 | ```sh
32 | echo 'export COMMIT_MSG_LINTER_LANG=zh-CN' >> ~/.zshrc
33 | ```
34 |
35 | profile 文件可以是 `.bash_profile`, `.zshrc` 等。
36 |
37 | ## 优点
38 |
39 | 1. 可视化,低学习成本
40 | 2. 零配置,易上手
41 | 3. 错误提交对症提示,对不熟悉提交信息规范习惯的开发者友好
42 | 4. i18,支持中文、英文、葡萄牙
43 | 5. 可自定义团队规范
44 | 6. 使用模糊匹配自动纠正 type
45 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/README-zh-CN.md:
--------------------------------------------------------------------------------
1 | # git-commit-msg-linter
2 |
3 |
4 |
5 |
6 |
7 | > 👀 规范开发者的每一行提交信息,为团队定制专属的 Git 提交信息规范
8 |
9 | ## 安装
10 |
11 | ```sh
12 | npm install git-commit-msg-linter --save-dev
13 | ```
14 |
15 | **只需安装无需配置**,提交信息已处于 lint 状态,现在去提交代码试试。
16 |
17 | ## 设置提示语言
18 |
19 | 默认使用系统设置语言(`$ node -p 'Intl.DateTimeFormat().resolvedOptions().locale'`),可通过以下两种方式自定义语言,支持中文(zh-CN)、英文(en-US)葡萄牙语(pt-BR)和 es-ES。以下配置顺序优先级从高到低:
20 |
21 | ### 通过 commitlinterrc.json 设置
22 |
23 | ```json
24 | {
25 | "lang": "zh-CN"
26 | }
27 | ```
28 |
29 | ### 通过环境变量设置
30 |
31 | ```sh
32 | echo 'export COMMIT_MSG_LINTER_LANG=zh-CN' >> ~/.zshrc
33 | ```
34 |
35 | profile 文件可以是 `.bash_profile`, `.zshrc` 等。
36 |
37 | ## 优点
38 |
39 | 1. 可视化,低学习成本
40 | 2. 零配置,易上手
41 | 3. 错误提交对症提示,对不熟悉提交信息规范习惯的开发者友好
42 | 4. i18,支持中文、英文、葡萄牙
43 | 5. 可自定义团队规范
44 | 6. 使用模糊匹配自动纠正 type
45 |
--------------------------------------------------------------------------------
/packages/commit-msg-linter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "commit-msg-linter",
3 | "version": "1.1.0",
4 | "description": "git commit message linter",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/legend80s/commit-msg-linter.git"
9 | },
10 | "scripts": {
11 | "pub:patch": "npm version patch",
12 | "pub:minor": "npm version minor",
13 | "pub:major": "npm version major",
14 | "postversion": "npm publish",
15 | "postpublish": "git commit -am 'chore: update version' && npm run push",
16 | "push": "publishing=1 git push && publishing=1 git push --tags"
17 | },
18 | "keywords": [],
19 | "author": "legend80s",
20 | "license": "MIT",
21 | "engines": {
22 | "node": ">= 14.0.0"
23 | },
24 | "dependencies": {
25 | "did-you-mean": "^0.0.1",
26 | "supports-color": "^8.1.1"
27 | },
28 | "bugs": {
29 | "url": "https://github.com/legend80s/commit-msg-linter/issues"
30 | },
31 | "homepage": "https://github.com/legend80s/commit-msg-linter#readme"
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019
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.
22 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "git-commit-msg-linter",
3 | "version": "5.0.8",
4 | "description": "git commit message lint hook",
5 | "main": "index.js",
6 | "bin": {
7 | "commit-msg-linter": "cli/validate.js"
8 | },
9 | "files": [
10 | "cli/validate.js",
11 | "core/validate.js",
12 | "index.js",
13 | "index.d.ts",
14 | "commit-msg-linter.js",
15 | "constants.js",
16 | "install.js",
17 | "uninstall.js",
18 | "lang.js",
19 | "utils.js"
20 | ],
21 | "scripts": {
22 | "pub:patch": "npm version patch",
23 | "pub:minor": "npm version minor",
24 | "pub:major": "npm version major",
25 | "install": "node install.js",
26 | "uninstall": "node uninstall.js",
27 | "postversion": "npm publish",
28 | "postpublish": "git commit -am 'chore: update version' && npm run push",
29 | "push": "publishing=1 git push && publishing=1 git push --tags",
30 | "test": "bash ./test/e2e.sh"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/legend80s/commit-msg-linter.git"
35 | },
36 | "keywords": [],
37 | "author": "legend80s",
38 | "license": "MIT",
39 | "engines": {
40 | "node": ">= 14.0.0"
41 | },
42 | "dependencies": {
43 | "chalk": "^2.4.2",
44 | "commit-msg-linter": "^1.0.0"
45 | },
46 | "lint-staged": {
47 | "*.js": "eslint"
48 | },
49 | "bugs": {
50 | "url": "https://github.com/legend80s/commit-msg-linter/issues"
51 | },
52 | "homepage": "https://github.com/legend80s/commit-msg-linter#readme"
53 | }
54 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/uninstall.js:
--------------------------------------------------------------------------------
1 | /**
2 | * When uninstalling, the `commit-msg` file will be restored
3 | * and the `commit-msg.old` will be removed.
4 | */
5 |
6 | /* eslint-disable no-console */
7 |
8 | const fs = require('fs');
9 | const path = require('path');
10 |
11 | const { bailOut } = require('./utils');
12 | const {
13 | COMMIT_MSG_HOOK_FILE,
14 | PACKAGE_NAME_LABEL,
15 | COMMIT_MSG_LABEL,
16 | PROJECT_ROOT,
17 | } = require('./constants');
18 |
19 | const exists = fs.existsSync || path.existsSync;
20 | const git = path.resolve(PROJECT_ROOT, '.git');
21 |
22 | // Location of hook file, if it exists
23 | const commitMsgFile = path.resolve(git, 'hooks', COMMIT_MSG_HOOK_FILE);
24 |
25 | // Bail out if we don't have commit-msg file, it might be removed manually.
26 | if (!exists(commitMsgFile)) {
27 | console.info(
28 | `${PACKAGE_NAME_LABEL}: Not found any ${COMMIT_MSG_LABEL} hook, no need to clean the battle field`,
29 | );
30 |
31 | bailOut();
32 | }
33 |
34 | // If we don't have an old commit-msg file, we should just remove the commit-msg hook.
35 | if (!exists(`${commitMsgFile}.old`)) {
36 | // only remove what we created
37 | isFileWeCreated(commitMsgFile) && fs.unlinkSync(commitMsgFile);
38 | } else {
39 | // But if we do have an old one it must restored. *DON'T BE EVIL*.
40 | fs.copyFileSync(`${commitMsgFile}.old`, commitMsgFile);
41 |
42 | fs.chmodSync(commitMsgFile, '755');
43 | fs.unlinkSync(`${commitMsgFile}.old`);
44 | }
45 |
46 | function isFileWeCreated(fp) {
47 | const commitMsgFileContent = fs.readFileSync(fp, 'utf-8');
48 | const ID = 'commit-msg-linter';
49 |
50 | return commitMsgFileContent.includes(ID);
51 | }
52 |
--------------------------------------------------------------------------------
/assets/docs.md:
--------------------------------------------------------------------------------
1 | # git-commit-msg-linter
2 |
3 | ## How it works
4 |
5 | After installed, it will copy the executable file `{PROJECT_ROOT}/.git/hooks/commit-msg` if it exists to `{PROJECT_ROOT}/.git/hooks/commit-msg.old` then the `commit-msg` will be overwritten by our linting rules.
6 |
7 | Before uninstalling, the `commit-msg` file will be restored and the `commit-msg.old` will be removed.
8 |
9 | ## defaults
10 |
11 | The default `type`s includes **feat**, **fix**, **docs**, **style**, **refactor**, **test**, **chore**, **perf**, **ci** and **temp**.
12 |
13 | The default `max-len` is 100 which means the commit message cannot be longer than 100 characters.
14 |
15 | ## commitlinterrc.json
16 |
17 | Except for default types, you can add, overwrite or forbid certain types and so does the `max-len`.
18 |
19 | For example if you have this `commitlinterrc.json` file below in the root directory of your project:
20 |
21 | ```json
22 | {
23 | "types": {
24 | "feat": "ユーザーが知覚できる新機能",
25 | "build": "ビルドシステムまたは外部の依存関係に影響する変更(スコープの例:gulp、broccoli、npm)",
26 | "deps": "依存関係を追加、アップグレード、削除",
27 | "temp": false,
28 | "chore": false
29 | },
30 | "max-len": 80,
31 | "debug": true
32 | }
33 | ```
34 |
35 | which means:
36 |
37 | - Modify existing type `feat`'s description to "ユーザーが知覚できる新機能".
38 | - Add two new types: `build` and `deps`.
39 | - `temp` is not allowed.
40 | - `chore` is forbidden as `build` covers the same scope.
41 | - Maximum length of a commit message is adjusted to 80.
42 | - Display verbose information about the commit message.
43 |
44 | ## FAQs
45 |
46 | 1. Why not [conventional-changelog/commitlint](https://github.com/conventional-changelog/commitlint)?
47 |
48 | > - Configuration is relatively complex.
49 | >
50 | > - No description for type, unfriendly to commit newbies. Because every time your are wondering which type should I use, you must jump out of you commit context to seek documentation in the wild web.
51 | >
52 | > - To modify type description is also not supported. Unfriendly to non-english speakers. For example, all my team members are Japanese, isn't it more productive to change all the descriptions to Japanese?
53 | >
54 | > - To add more types is also impossible. This is unacceptable for project with different types already existed.
55 |
56 | ## TODO
57 |
58 | - [x] Existing rule can be overwritten and new ones can be added through `commitlinterrc.json`.
59 | - [ ] `is-english-only` should be configurable through `commitlinterrc.json`, default `false`.
60 | - [x] `max-len` should be configurable through `commitlinterrc.json`, default `100`.
61 | - [x] First letter of `subject` must be a lowercase one.
62 | - [x] `subject` must not end with dot.
63 | - [x] Empty `scope` parenthesis not allowed.
64 | - [x] `scope` parenthesis must be of English which means full-width ones are not allowed.
65 | - [ ] Keep a space between Chinese and English character.
66 | - [x] Fix git merge commit not valid.
67 | - [x] Enable showing verbose information for debugging.
68 | - [ ] Interactive correcting and suggestion.
69 | - [x] No backup when `commit-msg.old` existed.
70 | - [x] Display commit message on invalid error.
71 |
72 | ## Reference guidelines
73 |
74 | 1. [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines)
75 | 2. [Angular.js Git Commit Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines)
76 | 3. [Google AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#heading=h.uyo6cb12dt6w)
77 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/install.js:
--------------------------------------------------------------------------------
1 | /**
2 | * When installing, it will copy the executable file `{PROJECT}/.git/hooks/commit-msg` if it exists
3 | * to `{PROJECT}/.git/hooks/commit-msg.old`
4 | * then the `commit-msg` will be overwritten by our linting rules.
5 | */
6 |
7 | /* eslint-disable no-console */
8 |
9 | const fs = require('fs');
10 | const path = require('path');
11 | const chalk = require('chalk');
12 | const linter = require('commit-msg-linter');
13 |
14 | const pkg = require('./package.json');
15 |
16 | const silent = process.argv.slice(2).some((arg) => arg.includes('--silent'));
17 |
18 | function log(...args) {
19 | if (silent) {
20 | return;
21 | }
22 |
23 | console.info(...args);
24 | }
25 |
26 | const { bailOut } = require('./utils');
27 | const {
28 | COMMIT_MSG_LABEL,
29 | PACKAGE_NAME_LABEL,
30 | COMMIT_MSG_HOOK_FILE,
31 | PACKAGE_NAME,
32 | PROJECT_ROOT,
33 | } = require('./constants');
34 |
35 | log('[git-commit-msg-linter]: Install starting...');
36 |
37 | const exists = fs.existsSync;
38 |
39 | const projectRootList = [
40 | process.cwd(),
41 | PROJECT_ROOT,
42 |
43 | // for pnpm: not a elegant solution 😓
44 | path.resolve(__dirname, '../../..'),
45 | path.resolve(__dirname, '../../../..'),
46 | path.resolve(__dirname, '../../../../..'),
47 | ];
48 |
49 | const git = guessGitDirectory(projectRootList);
50 |
51 | // Bail out if we don't have an `.git` folder as the hooks will not get triggered.
52 | if (!git) {
53 | console.error(`${PACKAGE_NAME_LABEL}: ${chalk.red('.git folder not found in')}`);
54 | console.error(projectRootList);
55 | console.error(`${PACKAGE_NAME_LABEL}: ${chalk.red(`${PACKAGE_NAME} won't be installed`)}`);
56 |
57 | bailOut();
58 | }
59 |
60 | const hooks = path.resolve(git, 'hooks');
61 | const commitMsgHookFile = path.resolve(hooks, COMMIT_MSG_HOOK_FILE);
62 | const backup = `${commitMsgHookFile}.old`;
63 |
64 | // If we do have `.git` folder create a `hooks` folder under it if it doesn't exist.
65 | if (!exists(hooks)) { fs.mkdirSync(hooks); }
66 |
67 | // If there's an existing `commit-msg` hook file back it up instead of
68 | // overwriting it and losing it completely as it might contain something important.
69 | if (exists(commitMsgHookFile) && !fs.lstatSync(commitMsgHookFile).isSymbolicLink()) {
70 | log(`${PACKAGE_NAME_LABEL}:`);
71 | log(`${PACKAGE_NAME_LABEL}: An existing git ${COMMIT_MSG_LABEL} hook detected`);
72 |
73 | // Only backup when "commit-msg.old" not exists, otherwise the original content will be lost.
74 | if (exists(backup) || isFileWeCreated(commitMsgHookFile)) {
75 | // NO NEED TO BACKUP
76 | } else {
77 | fs.writeFileSync(backup, fs.readFileSync(commitMsgHookFile));
78 |
79 | const old = chalk.bold(`${COMMIT_MSG_HOOK_FILE}.old`);
80 | log(`${PACKAGE_NAME_LABEL}: Old ${COMMIT_MSG_LABEL} hook backed up to ${old}`);
81 | log(`${PACKAGE_NAME_LABEL}:`);
82 | }
83 | }
84 |
85 | function getTag() {
86 | const time = new Date().toLocaleString();
87 |
88 | return `${pkg.name}@${pkg.version} ${time}`;
89 | }
90 |
91 | const rules = `
92 | #!/usr/bin/env bash
93 | #
94 | # ${getTag()}
95 | # id=commit-msg-linter - THE id COMMENT SHOULD NOT BE DELETED OR MODIFIED!
96 | # It's used to check whether this commit-msg hook file is created by us,
97 | # if it is then we can remove it confidently on uninstallation.
98 |
99 | cat "${linter.getLinterPath()}" | node --input-type=commonjs
100 | `.trimStart();
101 |
102 | // It could be that we do not have rights to this folder which could cause the
103 | // installation of this module to completely failure.
104 | // We should just output the error instead breaking the whole npm install process.
105 | try { fs.writeFileSync(commitMsgHookFile, rules); } catch (e) {
106 | console.error(`${PACKAGE_NAME_LABEL}: ${chalk.red('Failed to create the hook file in your .git/hooks folder because:')}`);
107 | console.error(`${PACKAGE_NAME_LABEL}: ${chalk.red(e.message)}`);
108 | console.error(`${PACKAGE_NAME_LABEL}: ${chalk.red('The hook was not installed.')}`);
109 | }
110 |
111 | try { fs.chmodSync(commitMsgHookFile, '777'); } catch (e) {
112 | const alert = chalk.red(`chmod 0777 the ${COMMIT_MSG_LABEL} file in your .git/hooks folder because:`);
113 |
114 | console.error(`${PACKAGE_NAME_LABEL}:`);
115 | console.error(`${PACKAGE_NAME_LABEL}: ${alert}`);
116 | console.error(`${PACKAGE_NAME_LABEL}: ${chalk.red(e.message)}`);
117 | console.error(`${PACKAGE_NAME_LABEL}:`);
118 | }
119 |
120 | log(chalk.green(`[${PACKAGE_NAME}]: Installed successfully.`));
121 |
122 | /**
123 | * @param {string[]} projectDirectories
124 | * @returns {string}
125 | */
126 | function guessGitDirectory(projectDirectories) {
127 | return projectDirectories
128 | .map((projectRoot) => path.resolve(projectRoot, '.git'))
129 | .find((gitDirectory) => exists(gitDirectory) && fs.lstatSync(gitDirectory).isDirectory());
130 | }
131 |
132 | function isFileWeCreated(fp) {
133 | const content = fs.readFileSync(fp, 'utf-8');
134 | const ID = 'commit-msg-linter';
135 |
136 | return content.includes(ID);
137 | }
138 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/cli/validate.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /* eslint-disable no-console */
3 |
4 | // https://github.com/legend80s/commit-msg-linter/issues/21
5 | /* eslint-disable no-undef */
6 |
7 | // expected output of `commit-msg-linter sha1..sha2`? Just exit with code 1 and print the invalid msg list?
8 | const { execSync } = require('child_process');
9 | const { readFileSync } = require('fs');
10 | const path = require('path');
11 | const { validateMessage, YELLOW, RED } = require('../core/validate');
12 |
13 | const args = process.argv.slice(2);
14 | const config = readConfig();
15 |
16 | main(args);
17 |
18 | /**
19 | *
20 | * @param {string[]} ids
21 | */
22 | function main(ids) {
23 | // sha1..sha2 [sha1, sha2]
24 | // sha1: sha1
25 | // sha1 sha2: sha1 and sha2
26 |
27 | console.log('ids:', ids);
28 |
29 | /** @type {Array} */
30 | const invalidMsgList = [];
31 |
32 | ids.forEach((sha) => {
33 | // contains double `.`
34 | const parts = prune(sha.split('..'));
35 |
36 | // lint range
37 | if (parts.length > 1) {
38 | const [start, end] = parts;
39 |
40 | invalidMsgList.push(...lintBetween(start, end));
41 | }
42 |
43 | // lint single sha
44 | const invalidMsg = lint(sha);
45 | invalidMsg !== true && invalidMsgList.push(invalidMsg);
46 | });
47 |
48 | /** @type {Set} */
49 | const invalidMsgs = new Set(invalidMsgList);
50 |
51 | if (invalidMsgs.size) {
52 | // eslint-disable-next-line no-console
53 | console.error(invalidMsgs.size, 'invalid commit msg(s) found:', invalidMsgs);
54 |
55 | process.exitCode = 1;
56 | } else {
57 | process.exitCode = 0;
58 | }
59 | }
60 |
61 | /**
62 | *
63 | * @param {string[]} arr
64 | */
65 | function prune(arr) {
66 | return arr.map((x) => x.trim()).filter(Boolean);
67 | }
68 |
69 | /**
70 | *
71 | * @param {ISha} sha
72 | * @returns {true | string} return `true` when msg is valid
73 | */
74 | function lint(sha) {
75 | // git log $sha --pretty=format:%s -n1
76 | const msg = execSync(`git log ${sha} --pretty=format:%s -n1`).toString();
77 |
78 | return validate(msg) ? true : msg;
79 | }
80 |
81 | /**
82 | * @param {ISha} start
83 | * @param {ISha} end
84 | * @returns {string[]}
85 | */
86 | function lintBetween(start, end) {
87 | // git log sha1..sha2 --pretty=format:%s
88 | // temp: temp
89 | // fix(i18n): issues#27 lang config on file commitlinterrc.json not working
90 |
91 | // git log sha1 --pretty=format:%s -n1
92 | // chore: upgrade to 4
93 | const rangeCmd = `git log ${start}..${end} --pretty=format:%s`;
94 |
95 | const msgs = execSync(rangeCmd).toString().split('\n');
96 | const invalidMsgs = msgs.filter((msg) => !validate(msg));
97 |
98 | const startMsg = lint(start);
99 |
100 | if (startMsg === true) {
101 | return invalidMsgs;
102 | }
103 |
104 | return [startMsg, ...invalidMsgs];
105 | }
106 |
107 | /**
108 | *
109 | * @param {string} msg
110 | * @returns {boolean}
111 | */
112 | function validate(msg) {
113 | // const { descriptions: DESCRIPTIONS, stereotypes: STEREOTYPES } = getLangData(lang);
114 |
115 | // const {
116 | // example,
117 | // scope,
118 | // invalidScope,
119 | // subject,
120 | // invalidSubject,
121 | // } = DESCRIPTIONS;
122 |
123 | const {
124 | types,
125 | 'max-len': maxLength,
126 | 'min-len': minLength,
127 | debug: verbose = process.env.COMMIT_MSG_LINTER_ENV === 'debug',
128 | showInvalidHeader = true,
129 |
130 | scopeDescriptions = scope,
131 | invalidScopeDescriptions = invalidScope,
132 |
133 | subjectDescriptions = subject,
134 | invalidSubjectDescriptions = invalidSubject,
135 |
136 | postSubjectDescriptions = [],
137 | englishOnly = false,
138 | scopeRequired = false,
139 | ...rest
140 | } = config;
141 |
142 | verbose && debug('config:', config);
143 |
144 | // const msg = getFirstLine(commitMsgContent).replace(/\s{2,}/g, ' ');
145 | const mergedTypes = merge(STEREOTYPES, types);
146 | const maxLen = typeof maxLength === 'number' ? maxLength : MAX_LENGTH;
147 | const minLen = typeof minLength === 'number' ? minLength : MIN_LENGTH;
148 |
149 | if (!validateMessage(msg, {
150 | ...rest,
151 |
152 | mergedTypes,
153 | maxLen,
154 | minLen,
155 | verbose,
156 | example,
157 | showInvalidHeader,
158 | scopeDescriptions,
159 | invalidScopeDescriptions,
160 | subjectDescriptions,
161 | invalidSubjectDescriptions,
162 | postSubjectDescriptions,
163 |
164 | lang,
165 | englishOnly,
166 | scopeRequired,
167 | })) {
168 | process.exit(1);
169 | } else {
170 | process.exit(0);
171 | }
172 | }
173 |
174 | function readConfig() {
175 | const filename = path.resolve(process.cwd(), 'commitlinterrc.json');
176 |
177 | const packageName = `${YELLOW}git-commit-msg-linter`;
178 | let content = '{}';
179 |
180 | try {
181 | content = readFileSync(filename);
182 | } catch (error) {
183 | if (error.code === 'ENOENT') {
184 | /** pass, as commitlinterrc are optional */
185 | } else {
186 | /** pass, commitlinterrc ignored when invalid */
187 | /** It must be a bug so output the error to the user */
188 | console.error(`${packageName}: ${RED}read commitlinterrc.json failed`, error);
189 | }
190 | }
191 |
192 | // console.log('filename:', filename);
193 | // console.log('content:', content);
194 |
195 | let configObject = {};
196 |
197 | try {
198 | configObject = JSON.parse(content);
199 | } catch (error) {
200 | /** pass, commitlinterrc ignored when invalid json */
201 | /** output the error to the user for self-checking */
202 | console.error(`${packageName}: ${RED}commitlinterrc.json ignored because of invalid json`);
203 | }
204 |
205 | return configObject;
206 | }
207 |
208 | function merge(stereotypes, configTypes) {
209 | return compact({ ...stereotypes, ...configTypes }, { strictFalse: true });
210 | }
211 |
212 | /**
213 | * @typedef {string} ISha
214 | */
215 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/core/validate.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | /* eslint-disable no-console */
3 | const colorSupported = false;
4 |
5 | exports.YELLOW = colorSupported ? '\x1b[1;33m' : '';
6 | exports.GRAY = colorSupported ? '\x1b[0;37m' : '';
7 | exports.RED = colorSupported ? '\x1b[0;31m' : '';
8 | exports.GREEN = colorSupported ? '\x1b[0;32m' : '';
9 |
10 | /** End Of Style, removes all attributes (formatting and colors) */
11 | exports.EOS = colorSupported ? '\x1b[0m' : '';
12 | exports.BOLD = colorSupported ? '\x1b[1m' : '';
13 |
14 | /**
15 | * validate git message
16 | *
17 | * @param {string} message
18 | * @param {Object} options.mergedTypes 和 commitlinterrc merge 过的 types
19 | * @param {number} options.maxLen 提交信息最大长度
20 | * @param {boolean} options.verbose 是否打印 debug 信息
21 | * @returns {boolean}
22 | */
23 | exports.validateMessage = function validateMessage(
24 | message,
25 | {
26 | shouldDisplayError = true,
27 | mergedTypes,
28 | maxLen,
29 | minLen,
30 | verbose,
31 | example,
32 | showInvalidHeader,
33 | scopeDescriptions,
34 | subjectDescriptions,
35 | postSubjectDescriptions,
36 | invalidSubjectDescriptions,
37 | lang,
38 | englishOnly,
39 |
40 | scopeRequired,
41 | validScopes,
42 | invalidScopeDescriptions,
43 | scopeNotInRangeDescription = Array.isArray(validScopes) && validScopes.length
44 | ? `scope must be one of [${validScopes.map((s) => `"${s}"`).join(', ')}].`
45 | : 'scope not in range. SHOULD NOT SEE THIS MESSAGE. PLEASE REPORT AN ISSUE.',
46 | },
47 | ) {
48 | let isValid = true;
49 | let invalidLength = false;
50 |
51 | /* eslint-enable no-useless-escape */
52 | const IGNORED_PATTERNS = [
53 | /(^WIP:)|(^\d+\.\d+\.\d+)/,
54 |
55 | /^Publish$/,
56 |
57 | // ignore auto-generated commit msg
58 | /^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)/m,
59 | ];
60 |
61 | if (IGNORED_PATTERNS.some((pattern) => pattern.test(message))) {
62 | shouldDisplayError && console.log('Commit message validation ignored.');
63 | return true;
64 | }
65 |
66 | // verbose && debug(`commit message: |${message}|`);
67 |
68 | if (message.length > maxLen || message.length < minLen) {
69 | invalidLength = true;
70 | isValid = false;
71 | }
72 |
73 | // eslint-disable-next-line no-useless-escape
74 | if (englishOnly && !/^[a-zA-Z\s\.!@#$%^&*\(\)-_+=\\\|\[\]\{\};:'"?/.>,<]+$/.test(message)) {
75 | shouldDisplayError && console.log('');
76 | // eslint-disable-next-line no-undef
77 | shouldDisplayError && console.warn(`${YELLOW}[git-commit-msg-linter] Commit message can not contain ${RED}non-English${EOS}${YELLOW} characters due to ${red('`englishOnly`')} ${yellow('in "commitlinterrc.json" is true.')}`);
78 |
79 | return false;
80 | }
81 |
82 | const matches = resolvePatterns(message);
83 |
84 | if (!matches) {
85 | // eslint-disable-next-line no-undef
86 | shouldDisplayError && displayError(
87 | { invalidLength, invalidFormat: true },
88 | {
89 | mergedTypes,
90 | maxLen,
91 | minLen,
92 | message,
93 | example,
94 | showInvalidHeader,
95 | scopeDescriptions,
96 |
97 | subjectDescriptions,
98 | postSubjectDescriptions,
99 | invalidSubjectDescriptions,
100 | lang,
101 |
102 | scopeRequired,
103 | validScopes,
104 | invalidScopeDescriptions,
105 | },
106 | );
107 |
108 | return false;
109 | }
110 |
111 | const { type, scope, subject } = matches;
112 |
113 | verbose && debug(`type: ${type}, scope: ${scope}, subject: ${subject}`);
114 |
115 | const types = Object.keys(mergedTypes);
116 | const typeInvalid = !types.includes(type);
117 |
118 | const [invalidScope, reason] = isScopeInvalid(scope, { scopeRequired, validScopes });
119 |
120 | // Don't capitalize first letter; No dot (.) at the end
121 | const invalidSubject = isUpperCase(subject[0]) || subject.endsWith('.');
122 |
123 | if (invalidLength || typeInvalid || invalidScope || invalidSubject) {
124 | shouldDisplayError && displayError(
125 | {
126 | invalidLength, type, typeInvalid, invalidScope, invalidSubject,
127 | },
128 | {
129 | mergedTypes,
130 | maxLen,
131 | minLen,
132 | message,
133 | example,
134 | showInvalidHeader,
135 | scopeDescriptions,
136 | invalidScopeDescriptions: reason === 'NOT_IN_RANGE' ? castArry(scopeNotInRangeDescription) : invalidScopeDescriptions,
137 | subjectDescriptions,
138 | postSubjectDescriptions,
139 | invalidSubjectDescriptions,
140 | lang,
141 | scopeRequired,
142 | },
143 | );
144 |
145 | return false;
146 | }
147 |
148 | return isValid;
149 | };
150 |
151 | function resolvePatterns(message) {
152 | // eslint-disable-next-line no-useless-escape
153 | const PATTERN = /^(?:fixup!\s*)?(\w*)(\(([\w\$\.\*/-]*)\))?\: (.*)$/;
154 | const matches = PATTERN.exec(message);
155 |
156 | if (matches) {
157 | const type = matches[1];
158 | const scope = matches[3];
159 | const subject = matches[4];
160 |
161 | return {
162 | type,
163 | scope,
164 | subject,
165 | };
166 | }
167 |
168 | return null;
169 | }
170 |
171 | function isScopeInvalid(scope, { validScopes, scopeRequired }) {
172 | const trimedScope = scope && scope.trim();
173 |
174 | const notInRange = () => Array.isArray(validScopes)
175 | && validScopes.length > 0
176 | && !validScopes.includes(scope);
177 |
178 | if (scopeRequired) {
179 | if (!trimedScope) return [true, 'SCOPE_REQUIRED'];
180 |
181 | if (notInRange()) {
182 | return [true, 'NOT_IN_RANGE'];
183 | }
184 | }
185 |
186 | if (typeof scope === 'string') {
187 | // scope can be optional, but not empty string
188 | // @example
189 | // "test: hello" OK
190 | // "test(): hello" FAILED
191 | if (trimedScope === '') { return [true, 'SCOPE_EMPTY_STRING']; }
192 |
193 | if (notInRange()) {
194 | return [true, 'NOT_IN_RANGE'];
195 | }
196 | }
197 |
198 | return [false];
199 | }
200 |
201 | function isUpperCase(letter) {
202 | return /^[A-Z]$/.test(letter);
203 | }
204 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | git-commit-msg-linter 👋
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | > A lightweight, independent, 0 configurations and joyful git commit message linter.
15 | >
16 | > 👀 Watching your every git commit message INSTANTLY 🚀.
17 |
18 | 
19 | `gcam` is just an alias to `git commit -a -m`
20 |
21 | A git "commit-msg" hook for linting your git commit message against the popular [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format). As a hook it will run at every commiting to make sure that the message to commit is valid against the conventions. If not the commit will be aborted.
22 |
23 | *Heavily inspired by [pre-commit](https://github.com/observing/pre-commit). Thanks.*
24 |
25 | [简体中文](https://github.com/legend80s/commit-msg-linter/blob/master/README-zh-CN.md)
26 |
27 |
28 | Table of Contents
29 |
30 | - [Install](#install)
31 | - [Recommended Commit Message Format](#recommended-commit-message-format)
32 | - [Zero Configurations](#zero-configurations)
33 | - [commitlinterrc.json](#commitlinterrcjson)
34 | - [Set Linting Prompter's Language](#set-linting-prompters-language)
35 | - [Set in commitlinterrc.json](#set-in-commitlinterrcjson)
36 | - [Set in bash profiles](#set-in-bash-profiles)
37 | - [Features](#features)
38 | - [Why yet a new linter](#why-yet-a-new-linter)
39 | - [How it works](#how-it-works)
40 | - [FAQs](#faqs)
41 | - [1. Why not conventional-changelog/commitlint?](#1-why-not-conventional-changelogcommitlint)
42 | - [2. Work With Husky 5](#2-work-with-husky-5)
43 | - [3. git-commit-msg-linter badge](#3-git-commit-msg-linter-badge)
44 | - [TODO](#todo)
45 | - [Development](#development)
46 | - [Publish](#publish)
47 | - [References](#references)
48 | - [🤝 Contributing](#-contributing)
49 | - [Show your support](#show-your-support)
50 | - [📝 License](#-license)
51 |
52 |
53 | ## Install
54 |
55 | ```shell
56 | npm install git-commit-msg-linter --save-dev
57 | ```
58 |
59 | **Just install NO CONFIGURATIONS REQUIRED!** and your commit message is under linting from now on 🎉. Now go to your codebase to commit a message.
60 |
61 | > 💡 Tips: for husky 5 see [Work With Husky 5](#2-work-with-husky-5).
62 |
63 | ## Recommended Commit Message Format
64 |
65 | ```
66 | ():
67 | │ │ │
68 | │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
69 | │ │
70 | │ └─⫸ Commit Scope: Optional, can be anything specifying the scope of the commit change.
71 | | For example $location|$browser|$compile|$rootScope|ngHref|ngClick|ngView, etc.
72 | | In App Development, scope can be a page, a module or a component.
73 | │
74 | └─⫸ Commit Type: feat|fix|docs|style|refactor|test|chore|perf|ci|build|temp
75 | ```
76 |
77 | The `` and `` fields are mandatory, the `()` field is optional.
78 |
79 | Bad:
80 |
81 | > Correct spelling of CHANGELOG.
82 |
83 | Good:
84 |
85 | > docs: correct spelling of CHANGELOG
86 |
87 | Good (commit message with scope):
88 |
89 | > docs(CHANGELOG): correct spelling
90 |
91 | The default commit `type`s can be extended or modified by [commitlinterrc.json](https://github.com/legend80s/commit-msg-linter/blob/master/assets/docs.md#commitlinterrcjson).
92 |
93 | ## Zero Configurations
94 |
95 | **Configurations Not Required!** If it has to be customized we have the guide below.
96 |
97 | The default `type`s includes **feat**, **fix**, **docs**, **style**, **refactor**, **test**, **chore**, **perf**, **ci**, **build** and **temp**.
98 |
99 | The default `max-len` is 100 which means the commit message cannot be longer than 100 characters. All the settings can be modified in commitlinterrc.json.
100 |
101 | ### commitlinterrc.json
102 |
103 |
104 | More advanced settings
105 |
106 | Except for default types, you can add, overwrite or forbid certain types and so does the `max-len`.
107 |
108 | For example if you have this `commitlinterrc.json` file below in the root directory of your project:
109 |
110 | ```json
111 | {
112 | "types": {
113 | "feat": "ユーザーが知覚できる新機能",
114 | "build": "ビルドシステムまたは外部の依存関係に影響する変更(スコープの例:gulp、broccoli、npm)",
115 | "deps": "依存関係を追加、アップグレード、削除",
116 | "temp": false,
117 | "chore": false
118 | },
119 | "max-len": 80,
120 | "debug": true
121 | }
122 | ```
123 |
124 | Which means:
125 |
126 | - Modify existing type `feat`'s description to "ユーザーが知覚できる新機能".
127 | - Add two new types: `build` and `deps`.
128 | - `temp` is not allowed.
129 | - `chore` is forbidden as `build` covers the same scope.
130 | - Maximum length of a commit message is adjusted to 80.
131 | - Display verbose information about the commit message.
132 |
133 | A more detailed `commitlinterrc.json`:
134 |
135 | ```jsonc
136 | {
137 | "lang": "en-US", // or "zh-CN". Set linter prompt's language
138 | "types": {
139 | "feat": "ユーザーが知覚できる新機能",
140 | "build": "ビルドシステムまたは外部の依存関係に影響する変更(スコープの例:gulp、broccoli、npm)",
141 | "deps": "依存関係を追加、アップグレード、削除",
142 | "docs": "ドキュメントのみ変更",
143 | "fix": false,
144 | "style": false,
145 | "refactor": false,
146 | "test": false,
147 | "perf": false,
148 | "ci": false,
149 | "temp": false,
150 | "chore": false
151 | },
152 | "min-len": 10,
153 | "max-len": 80,
154 | "example": "feat: 新機能",
155 | "scopeDescriptions": [
156 | "オプションで、コミット変更の場所を指定するものであれば何でもかまいません。",
157 | "たとえば、$ location、$ browser、$ compile、$ rootScope、ngHref、ngClick、ngViewなど。",
158 | "アプリ開発では、スコープはページ、モジュール、またはコンポーネントです。"
159 | ],
160 | "validScopes": ["workspace", "package1", "package2", "package3", ...],
161 | "invalidScopeDescriptions": [
162 | "`scope`はオプションですが、括弧が存在する場合は空にすることはできません。"
163 | ],
164 | "subjectDescriptions": [
165 | "1行での変更の非常に短い説明。"
166 | ],
167 | "invalidSubjectDescriptions": [
168 | "最初の文字を大文字にしないでください",
169 | "最後にドット「。」なし"
170 | ],
171 | "showInvalidHeader": false,
172 | "debug": false
173 | }
174 | ```
175 |
176 | In this config, the one-line `example` and `scope`, `subject`'s description section are modified as what your write in the `commitlinterrc.json`. And the the invalid header is hidden by set `"showInvalidHeader": false`。
177 |
178 | 
179 |
180 | ### Set Linting Prompter's Language
181 |
182 | It will use your system's language as the default language. But two ways are provided also. Priority from high to low.
183 |
184 | #### Set in commitlinterrc.json
185 |
186 | ```json
187 | {
188 | "lang": "zh-CN"
189 | }
190 | ```
191 |
192 | `lang` in ["**en-US**", "**zh-CN**", "**pt-BR**", "**es-ES**"].
193 |
194 | #### Set in bash profiles
195 |
196 | ```sh
197 | echo 'export COMMIT_MSG_LINTER_LANG=zh-CN' >> ~/.zshrc
198 | ```
199 |
200 | profiles such as `.bash_profile`, `.zshrc` etc.
201 |
202 |
203 | ## Features
204 |
205 | 1. Visualization, low cost to Learn.
206 | 2. Independent, zero configurations.
207 | 3. Prompt error msg precisely, friendly to commit message format unfamiliar developers.
208 | 4. i18n: **en-US**, **pt-BR** (Brazilian Portuguese), **zh-CN** an **es-ES** supported and you can add more in [How to contribute new language support](#how-to-add-new-language-support).
209 | 5. The linter is customizable for your team.
210 | 6. It works with the husky flow.
211 | 7. pnpm supported.
212 | 8. ES6 modules or project with "type: module" in package.json supported.
213 |
214 | ## Why yet a new linter
215 |
216 |
217 | The answer
218 |
219 | Firstly it's very important to follow certain git commit message conventions and we recommend Angular's.
220 |
221 | Secondly no simple git commit message hook ever exists right now. To Add, to overwrite or to remove `type`s is not so friendly supported. *Why not conventional-changelog/commitlint or husky, read the [FAQs](https://github.com/legend80s/commit-msg-linter/blob/master/assets/docs.md#faqs)*.
222 |
223 |
224 | ## How it works
225 |
226 | The answer
227 |
228 | > The `commit-msg` hook takes one parameter, which again is the path to a temporary file that contains the commit message written by the developer. If this script exits non-zero, Git aborts the commit process, so you can use it to validate your project state or commit message before allowing a commit to go through.
229 | >
230 | > https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
231 |
232 | After installed, it will copy the hook `{PROJECT_ROOT}/.git/hooks/commit-msg` if it exists to `{PROJECT_ROOT}/.git/hooks/commit-msg.old` then the `commit-msg` will be overwritten by our linting rules.
233 |
234 | To uninstall run the `uninstall` script instead of removing it manually because only in this way, the old `commit-msg` hook can be restored, so that your next commit messages will be ignored by the linter.
235 |
236 | ```shell
237 | npm uninstall git-commit-msg-linter --save-dev
238 | ```
239 |
240 | Before uninstalling, the `commit-msg` file will be restored and the `commit-msg.old` will be removed.
241 |
242 |
243 | ## FAQs
244 |
245 |
246 | 1. Why not commitlint
247 |
248 | Why not [conventional-changelog/commitlint](https://github.com/conventional-changelog/commitlint)?
249 |
250 | - Configurations are relatively complex.
251 | - No description for type, unfriendly to commit newbies. Because every time your are wondering which type should I use, you must jump out of you commit context to seek documentation in the wild web.
252 | - To modify type description is also not supported. Unfriendly to non-english speakers. For example, all my team members are Japanese, isn't it more productive to change all the descriptions to Japanese?
253 | - To add more types is also impossible. This is unacceptable for project with different types already existed.
254 |
255 |
256 |
257 | 2. Work With Husky 5
258 |
259 | This linter can work by standalone. But if you have husky 5 installed, because husky 5 will ignore the `.git/hooks/commit-msg` so a `.husky/commit-msg` need to be added manually:
260 |
261 | ```sh
262 | npx husky add .husky/commit-msg ".git/hooks/commit-msg \$1"
263 | ```
264 |
265 | Show the file content of `.husky/commit-msg` to make sure it has been added successfully otherwise do it manually.
266 |
267 | ```sh
268 | #!/bin/sh
269 | . "$(dirname "$0")/_/husky.sh"
270 |
271 | .git/hooks/commit-msg $1
272 | ```
273 |
274 | More details at [issues 8](https://github.com/legend80s/commit-msg-linter/issues/8).
275 |
276 |
277 |
278 | 3. git-commit-msg-linter badge
279 |
280 | ```html
281 |
282 |
283 |
284 | ```
285 |
286 |
287 | ## TODO
288 |
289 | - [x] Existing rule can be overwritten and new ones can be added through `commitlinterrc.json`.
290 | - [x] `englishOnly` should be configurable through `commitlinterrc.json`, default `false`.
291 | - [x] `max-len` should be configurable through `commitlinterrc.json`, default `100`.
292 | - [x] First letter of `subject` must be a lowercase one.
293 | - [x] `subject` must not end with dot.
294 | - [x] Empty `scope` parenthesis not allowed.
295 | - [x] `scope` parenthesis must be of English which means full-width ones are not allowed.
296 | - [ ] Keep a space between Chinese and English character.
297 | - [x] Fix git merge commit not valid.
298 | - [x] Enable showing verbose information for debugging.
299 | - [x] Suggest similar but valid `type` on invalid input using [did-you-mean](https://www.npmjs.com/package/did-you-mean).
300 | - [x] No backup when `commit-msg.old` existed.
301 | - [x] Display commit message on invalid error.
302 | - [x] i18n.
303 | - [x] Set lang in zshrc, or commitlinrrc.
304 |
305 | ## Development
306 |
307 | Use pnpm.
308 |
309 | ### Publish
310 |
311 | ```sh
312 | npm version patch / minor / major
313 | ```
314 |
315 | ## References
316 |
317 | 1. [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format)
318 | 2. [Angular.js Git Commit Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines)
319 | 3. [Google AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit)
320 |
321 | ## 🤝 Contributing
322 |
323 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/legend80s/commit-msg-linter/issues).
324 |
325 | ### How to add new language support
326 |
327 | 1. Add the translation into commit-msg-linter.js.
328 | 2. Modify README.md to add the new language.
329 |
330 | You can read this PR [feat: add support to spanish (es-ES) #18](https://github.com/legend80s/commit-msg-linter/pull/18/files) as an example.
331 |
332 | ## Show your support
333 |
334 | Give a ⭐️ if this project helped you!
335 |
336 | ## 📝 License
337 |
338 | Copyright © 2019 [legend80s](https://github.com/legend80s).
339 |
340 | This project is [MIT](https://github.com/legend80s/commit-msg-linter/blob/master/LICENSE) licensed.
341 |
342 | ------
343 |
344 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
345 |
--------------------------------------------------------------------------------
/packages/git-commit-msg-linter/README.md:
--------------------------------------------------------------------------------
1 | git-commit-msg-linter 👋
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | > A lightweight, independent, 0 configurations and joyful git commit message linter.
15 | >
16 | > 👀 Watching your every git commit message INSTANTLY 🚀.
17 |
18 | 
19 | `gcam` is just an alias to `git commit -a -m`
20 |
21 | A git "commit-msg" hook for linting your git commit message against the popular [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format). As a hook it will run at every commiting to make sure that the message to commit is valid against the conventions. If not the commit will be aborted.
22 |
23 | *Heavily inspired by [pre-commit](https://github.com/observing/pre-commit). Thanks.*
24 |
25 | [简体中文](https://github.com/legend80s/commit-msg-linter/blob/master/README-zh-CN.md)
26 |
27 |
28 | Table of Contents
29 |
30 | - [Install](#install)
31 | - [Recommended Commit Message Format](#recommended-commit-message-format)
32 | - [Zero Configurations](#zero-configurations)
33 | - [commitlinterrc.json](#commitlinterrcjson)
34 | - [Set Linting Prompter's Language](#set-linting-prompters-language)
35 | - [Set in commitlinterrc.json](#set-in-commitlinterrcjson)
36 | - [Set in bash profiles](#set-in-bash-profiles)
37 | - [Features](#features)
38 | - [Why yet a new linter](#why-yet-a-new-linter)
39 | - [How it works](#how-it-works)
40 | - [FAQs](#faqs)
41 | - [1. Why not conventional-changelog/commitlint?](#1-why-not-conventional-changelogcommitlint)
42 | - [2. Work With Husky 5](#2-work-with-husky-5)
43 | - [3. git-commit-msg-linter badge](#3-git-commit-msg-linter-badge)
44 | - [TODO](#todo)
45 | - [Development](#development)
46 | - [Publish](#publish)
47 | - [References](#references)
48 | - [🤝 Contributing](#-contributing)
49 | - [Show your support](#show-your-support)
50 | - [📝 License](#-license)
51 |
52 |
53 | ## Install
54 |
55 | ```shell
56 | npm install git-commit-msg-linter --save-dev
57 | ```
58 |
59 | **Just install NO CONFIGURATIONS REQUIRED!** and your commit message is under linting from now on 🎉. Now go to your codebase to commit a message.
60 |
61 | > 💡 Tips: for husky 5 see [Work With Husky 5](#2-work-with-husky-5).
62 |
63 | ## Recommended Commit Message Format
64 |
65 | ```
66 | ():
67 | │ │ │
68 | │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
69 | │ │
70 | │ └─⫸ Commit Scope: Optional, can be anything specifying the scope of the commit change.
71 | | For example $location|$browser|$compile|$rootScope|ngHref|ngClick|ngView, etc.
72 | | In App Development, scope can be a page, a module or a component.
73 | │
74 | └─⫸ Commit Type: feat|fix|docs|style|refactor|test|chore|perf|ci|build|temp
75 | ```
76 |
77 | The `` and `` fields are mandatory, the `()` field is optional.
78 |
79 | Bad:
80 |
81 | > Correct spelling of CHANGELOG.
82 |
83 | Good:
84 |
85 | > docs: correct spelling of CHANGELOG
86 |
87 | Good (commit message with scope):
88 |
89 | > docs(CHANGELOG): correct spelling
90 |
91 | The default commit `type`s can be extended or modified by [commitlinterrc.json](https://github.com/legend80s/commit-msg-linter/blob/master/assets/docs.md#commitlinterrcjson).
92 |
93 | ## Zero Configurations
94 |
95 | **Configurations Not Required!** If it has to be customized we have the guide below.
96 |
97 | The default `type`s includes **feat**, **fix**, **docs**, **style**, **refactor**, **test**, **chore**, **perf**, **ci**, **build** and **temp**.
98 |
99 | The default `max-len` is 100 which means the commit message cannot be longer than 100 characters. All the settings can be modified in commitlinterrc.json.
100 |
101 | ### commitlinterrc.json
102 |
103 |
104 | More advanced settings
105 |
106 | Except for default types, you can add, overwrite or forbid certain types and so does the `max-len`.
107 |
108 | For example if you have this `commitlinterrc.json` file below in the root directory of your project:
109 |
110 | ```json
111 | {
112 | "types": {
113 | "feat": "ユーザーが知覚できる新機能",
114 | "build": "ビルドシステムまたは外部の依存関係に影響する変更(スコープの例:gulp、broccoli、npm)",
115 | "deps": "依存関係を追加、アップグレード、削除",
116 | "temp": false,
117 | "chore": false
118 | },
119 | "max-len": 80,
120 | "debug": true
121 | }
122 | ```
123 |
124 | Which means:
125 |
126 | - Modify existing type `feat`'s description to "ユーザーが知覚できる新機能".
127 | - Add two new types: `build` and `deps`.
128 | - `temp` is not allowed.
129 | - `chore` is forbidden as `build` covers the same scope.
130 | - Maximum length of a commit message is adjusted to 80.
131 | - Display verbose information about the commit message.
132 |
133 | A more detailed `commitlinterrc.json`:
134 |
135 | ```jsonc
136 | {
137 | "lang": "en-US", // or "zh-CN". Set linter prompt's language
138 | "types": {
139 | "feat": "ユーザーが知覚できる新機能",
140 | "build": "ビルドシステムまたは外部の依存関係に影響する変更(スコープの例:gulp、broccoli、npm)",
141 | "deps": "依存関係を追加、アップグレード、削除",
142 | "docs": "ドキュメントのみ変更",
143 | "fix": false,
144 | "style": false,
145 | "refactor": false,
146 | "test": false,
147 | "perf": false,
148 | "ci": false,
149 | "temp": false,
150 | "chore": false
151 | },
152 | "min-len": 10,
153 | "max-len": 80,
154 | "example": "feat: 新機能",
155 | "scopeDescriptions": [
156 | "オプションで、コミット変更の場所を指定するものであれば何でもかまいません。",
157 | "たとえば、$ location、$ browser、$ compile、$ rootScope、ngHref、ngClick、ngViewなど。",
158 | "アプリ開発では、スコープはページ、モジュール、またはコンポーネントです。"
159 | ],
160 | "validScopes": ["workspace", "package1", "package2", "package3", ...],
161 | "invalidScopeDescriptions": [
162 | "`scope`はオプションですが、括弧が存在する場合は空にすることはできません。"
163 | ],
164 | "subjectDescriptions": [
165 | "1行での変更の非常に短い説明。"
166 | ],
167 | "invalidSubjectDescriptions": [
168 | "最初の文字を大文字にしないでください",
169 | "最後にドット「。」なし"
170 | ],
171 | "showInvalidHeader": false,
172 | "debug": false
173 | }
174 | ```
175 |
176 | In this config, the one-line `example` and `scope`, `subject`'s description section are modified as what your write in the `commitlinterrc.json`. And the the invalid header is hidden by set `"showInvalidHeader": false`。
177 |
178 | 
179 |
180 | ### Set Linting Prompter's Language
181 |
182 | It will use your system's language as the default language. But two ways are provided also. Priority from high to low.
183 |
184 | #### Set in commitlinterrc.json
185 |
186 | ```json
187 | {
188 | "lang": "zh-CN"
189 | }
190 | ```
191 |
192 | `lang` in ["**en-US**", "**zh-CN**", "**pt-BR**", "**es-ES**"].
193 |
194 | #### Set in bash profiles
195 |
196 | ```sh
197 | echo 'export COMMIT_MSG_LINTER_LANG=zh-CN' >> ~/.zshrc
198 | ```
199 |
200 | profiles such as `.bash_profile`, `.zshrc` etc.
201 |
202 |
203 | ## Features
204 |
205 | 1. Visualization, low cost to learn for git newbies.
206 | 2. Independent, zero configurations.
207 | 3. Prompt error msg precisely, friendly to commit message format unfamiliar developers.
208 | 4. i18n: **en-US**, **pt-BR** (Brazilian Portuguese), **zh-CN** an **es-ES** supported and you can add more in [How to contribute new language support](#how-to-add-new-language-support).
209 | 5. The linter is customizable for your team.
210 | 6. It works with the husky flow.
211 | 7. pnpm supported.
212 | 8. ES6 modules or project with "type: module" in package.json supported.
213 |
214 | ## Why yet a new linter
215 |
216 |
217 | The answer
218 |
219 | Firstly it's very important to follow certain git commit message conventions and we recommend Angular's.
220 |
221 | Secondly no simple git commit message hook ever exists right now. To Add, to overwrite or to remove `type`s is not so friendly supported. *Why not conventional-changelog/commitlint or husky, read the [FAQs](https://github.com/legend80s/commit-msg-linter/blob/master/assets/docs.md#faqs)*.
222 |
223 |
224 | ## How it works
225 |
226 | The answer
227 |
228 | > The `commit-msg` hook takes one parameter, which again is the path to a temporary file that contains the commit message written by the developer. If this script exits non-zero, Git aborts the commit process, so you can use it to validate your project state or commit message before allowing a commit to go through.
229 | >
230 | > https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
231 |
232 | After installed, it will copy the hook `{PROJECT_ROOT}/.git/hooks/commit-msg` if it exists to `{PROJECT_ROOT}/.git/hooks/commit-msg.old` then the `commit-msg` will be overwritten by our linting rules.
233 |
234 | To uninstall run the `uninstall` script instead of removing it manually because only in this way, the old `commit-msg` hook can be restored, so that your next commit messages will be ignored by the linter.
235 |
236 | ```shell
237 | npm uninstall git-commit-msg-linter --save-dev
238 | ```
239 |
240 | Before uninstalling, the `commit-msg` file will be restored and the `commit-msg.old` will be removed.
241 |
242 |
243 | ## FAQs
244 |
245 |
246 | 1. Why not commitlint
247 |
248 | Why not [conventional-changelog/commitlint](https://github.com/conventional-changelog/commitlint)?
249 |
250 | - Configurations are relatively complex.
251 | - No description for type, unfriendly to commit newbies. Because every time your are wondering which type should I use, you must jump out of you commit context to seek documentation in the wild web.
252 | - To modify type description is also not supported. Unfriendly to non-english speakers. For example, all my team members are Japanese, isn't it more productive to change all the descriptions to Japanese?
253 | - To add more types is also impossible. This is unacceptable for project with different types already existed.
254 |
255 |
256 |
257 | 2. Work With Husky 5
258 |
259 | This linter can work by standalone. But if you have husky 5 installed, because husky 5 will ignore the `.git/hooks/commit-msg` so a `.husky/commit-msg` need to be added manually:
260 |
261 | ```sh
262 | npx husky add .husky/commit-msg ".git/hooks/commit-msg \$1"
263 | ```
264 |
265 | Show the file content of `.husky/commit-msg` to make sure it has been added successfully otherwise do it manually.
266 |
267 | ```sh
268 | #!/bin/sh
269 | . "$(dirname "$0")/_/husky.sh"
270 |
271 | .git/hooks/commit-msg $1
272 | ```
273 |
274 | More details at [issues 8](https://github.com/legend80s/commit-msg-linter/issues/8).
275 |
276 |
277 |
278 | 3. git-commit-msg-linter badge
279 |
280 | ```html
281 |
282 |
283 |
284 | ```
285 |
286 |
287 | ## TODO
288 |
289 | - [x] Existing rule can be overwritten and new ones can be added through `commitlinterrc.json`.
290 | - [x] `englishOnly` should be configurable through `commitlinterrc.json`, default `false`.
291 | - [x] `max-len` should be configurable through `commitlinterrc.json`, default `100`.
292 | - [x] First letter of `subject` must be a lowercase one.
293 | - [x] `subject` must not end with dot.
294 | - [x] Empty `scope` parenthesis not allowed.
295 | - [x] `scope` parenthesis must be of English which means full-width ones are not allowed.
296 | - [ ] Keep a space between Chinese and English character.
297 | - [x] Fix git merge commit not valid.
298 | - [x] Enable showing verbose information for debugging.
299 | - [x] Suggest similar but valid `type` on invalid input using [did-you-mean](https://www.npmjs.com/package/did-you-mean).
300 | - [x] No backup when `commit-msg.old` existed.
301 | - [x] Display commit message on invalid error.
302 | - [x] i18n.
303 | - [x] Set lang in zshrc, or commitlinrrc.
304 |
305 | ## Development
306 |
307 | Use pnpm.
308 |
309 | ### Publish
310 |
311 | ```sh
312 | npm version patch / minor / major
313 | ```
314 |
315 | ## References
316 |
317 | 1. [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format)
318 | 2. [Angular.js Git Commit Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines)
319 | 3. [Google AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit)
320 |
321 | ## 🤝 Contributing
322 |
323 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/legend80s/commit-msg-linter/issues).
324 |
325 | ### How to add new language support
326 |
327 | 1. Add the translation into commit-msg-linter.js.
328 | 2. Modify README.md to add the new language.
329 |
330 | You can read this PR [feat: add support to spanish (es-ES) #18](https://github.com/legend80s/commit-msg-linter/pull/18/files) as an example.
331 |
332 | ## Show your support
333 |
334 | Give a ⭐️ if this project helped you!
335 |
336 | ## 📝 License
337 |
338 | Copyright © 2019 [legend80s](https://github.com/legend80s).
339 |
340 | This project is [MIT](https://github.com/legend80s/commit-msg-linter/blob/master/LICENSE) licensed.
341 |
342 | ------
343 |
344 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
345 | 2023-05-05 21:26:07
346 | 2023-05-05 21:26:56
347 |
--------------------------------------------------------------------------------
/packages/commit-msg-linter/commit-msg-linter.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 |
6 | // pnpm wont resolve this package's dependencies as npm does
7 | // unless `pnpm install --shamefully-hoist`. What a shame!
8 | // issue#13 https://github.com/legend80s/commit-msg-linter/issues/13
9 | // So it had to be degraded to not use this two packages.
10 | let Matcher;
11 | try {
12 | // eslint-disable-next-line global-require
13 | Matcher = require('did-you-mean');
14 | } catch (error) {
15 | // DO NOTHING
16 | // on MODULE_NOT_FOUND when installed by pnpm
17 | }
18 |
19 | let supportsColor = { stdout: true };
20 | try {
21 | // eslint-disable-next-line global-require
22 | supportsColor = require('supports-color');
23 | } catch (error) {
24 | // DO NOTHING
25 | // on MODULE_NOT_FOUND when installed by pnpm
26 | }
27 |
28 | const colorSupported = supportsColor.stdout;
29 |
30 | const YELLOW = colorSupported ? '\x1b[1;33m' : '';
31 | const GRAY = colorSupported ? '\x1b[0;37m' : '';
32 | const RED = colorSupported ? '\x1b[0;31m' : '';
33 | const GREEN = colorSupported ? '\x1b[0;32m' : '';
34 |
35 | /** End Of Style, removes all attributes (formatting and colors) */
36 | const EOS = colorSupported ? '\x1b[0m' : '';
37 | const BOLD = colorSupported ? '\x1b[1m' : '';
38 |
39 | const config = readConfig();
40 | const i18n = getLangs();
41 |
42 | // Any line of the commit message cannot be longer than 100 characters!
43 | // This allows the message to be easier to read on github as well as in various git tools.
44 | // https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit
45 | const MAX_LENGTH = 100;
46 | const MIN_LENGTH = 10;
47 |
48 | function main() {
49 | const commitMsgFilePath = '.git/COMMIT_EDITMSG';
50 |
51 | // console.log(commitlinterrcFilePath);
52 |
53 | try {
54 | const commitMsgContent = fs.readFileSync(commitMsgFilePath, 'utf-8');
55 |
56 | const lang = getLanguage(config.lang);
57 |
58 | lint(commitMsgContent, lang);
59 | } catch (err) {
60 | console.error('[git-commit-msg-linter] failed:', err.message);
61 | console.error(err);
62 |
63 | process.exitCode = 1;
64 | }
65 | }
66 |
67 | main();
68 |
69 | /**
70 | * Returns the given language's data if the language exists.
71 | * Otherwise, returns the data for the English language.
72 | *
73 | * @param {string} lang Language
74 | * @returns {typeof i18n['en-US']}
75 | */
76 | function getLangData(lang) {
77 | return i18n[lang] ? i18n[lang] : i18n['en-US'];
78 | }
79 |
80 | /**
81 | * @param {string} commitMsgContent
82 | *
83 | * @returns {void}
84 | */
85 | async function lint(commitMsgContent, lang) {
86 | const { descriptions: DESCRIPTIONS, stereotypes: STEREOTYPES } = getLangData(lang);
87 |
88 | const {
89 | example,
90 | scope,
91 | invalidScope,
92 | subject,
93 | invalidSubject,
94 | } = DESCRIPTIONS;
95 |
96 | const {
97 | types,
98 | 'max-len': maxLength,
99 | 'min-len': minLength,
100 | debug: verbose = process.env.COMMIT_MSG_LINTER_ENV === 'debug',
101 | showInvalidHeader = true,
102 |
103 | scopeDescriptions = scope,
104 | invalidScopeDescriptions = invalidScope,
105 |
106 | subjectDescriptions = subject,
107 | invalidSubjectDescriptions = invalidSubject,
108 |
109 | postSubjectDescriptions = [],
110 | englishOnly = false,
111 | scopeRequired = false,
112 | ...rest
113 | } = config;
114 |
115 | verbose && debug('config:', config);
116 |
117 | const msg = getFirstLine(commitMsgContent).replace(/\s{2,}/g, ' ');
118 | const mergedTypes = merge(STEREOTYPES, types);
119 | const maxLen = typeof maxLength === 'number' ? maxLength : MAX_LENGTH;
120 | const minLen = typeof minLength === 'number' ? minLength : MIN_LENGTH;
121 |
122 | if (!validateMessage(msg, {
123 | ...rest,
124 |
125 | mergedTypes,
126 | maxLen,
127 | minLen,
128 | verbose,
129 | example: config.example || example,
130 | showInvalidHeader,
131 | scopeDescriptions,
132 | invalidScopeDescriptions,
133 | subjectDescriptions,
134 | invalidSubjectDescriptions,
135 | postSubjectDescriptions,
136 |
137 | lang,
138 | englishOnly,
139 | scopeRequired,
140 | })) {
141 | process.exit(1);
142 | } else {
143 | process.exit(0);
144 | }
145 | }
146 |
147 | /**
148 | * @returns {Partial}
149 | */
150 | function readConfig() {
151 | const filename = path.resolve(process.cwd(), 'commitlinterrc.json');
152 |
153 | const packageName = `${YELLOW}git-commit-msg-linter`;
154 | let content = '{}';
155 |
156 | try {
157 | content = fs.readFileSync(filename);
158 | } catch (error) {
159 | if (error.code === 'ENOENT') {
160 | /** pass, as commitlinterrc are optional */
161 | } else {
162 | /** pass, commitlinterrc ignored when invalid */
163 | /** It must be a bug so output the error to the user */
164 | console.error(`${packageName}: ${RED}read commitlinterrc.json failed`, error);
165 | }
166 | }
167 |
168 | // console.log('filename:', filename);
169 | // console.log('content:', content);
170 |
171 | let configObject = {};
172 |
173 | try {
174 | configObject = JSON.parse(content);
175 | } catch (error) {
176 | /** pass, commitlinterrc ignored when invalid json */
177 | /** output the error to the user for self-checking */
178 | console.error(`${packageName}: ${RED}commitlinterrc.json ignored because of invalid json`);
179 | }
180 |
181 | return configObject;
182 | }
183 |
184 | function merge(stereotypes, configTypes) {
185 | return compact({ ...stereotypes, ...configTypes }, { strictFalse: true });
186 | }
187 |
188 | /**
189 | * Create a new Object with all falsy values removed.
190 | * The values false, null, 0, "", undefined, and NaN are falsy when `strictFalse` is false,
191 | * Otherwise when `strictFalse` is true the only falsy value is false.
192 | * @param {Object} obj
193 | * @param {boolean} options.strictFalse
194 | * @returns {Object}
195 | */
196 | function compact(target, { strictFalse = false } = {}) {
197 | return Object.keys(target).reduce((acc, key) => {
198 | const shouldBeRemoved = strictFalse ? target[key] === false : !target[key];
199 |
200 | return shouldBeRemoved ? acc : { ...acc, [key]: target[key] };
201 | }, {});
202 | }
203 |
204 | function getFirstLine(buffer) {
205 | return buffer.toString().split('\n').shift();
206 | }
207 |
208 | /**
209 | * validate git message
210 | *
211 | * @param {string} message
212 | * @param {Object} options.mergedTypes 和 commitlinterrc merge 过的 types
213 | * @param {number} options.maxLen 提交信息最大长度
214 | * @param {boolean} options.verbose 是否打印 debug 信息
215 | * @returns {boolean}
216 | */
217 | function validateMessage(
218 | message,
219 | {
220 | shouldDisplayError = true,
221 | mergedTypes,
222 | maxLen,
223 | minLen,
224 | verbose,
225 | example,
226 | showInvalidHeader,
227 | scopeDescriptions,
228 | subjectDescriptions,
229 | postSubjectDescriptions,
230 | invalidSubjectDescriptions,
231 | lang,
232 | englishOnly,
233 |
234 | scopeRequired,
235 | validScopes,
236 | invalidScopeDescriptions,
237 | scopeNotInRangeDescription = Array.isArray(validScopes) && validScopes.length
238 | ? `scope must be one of [${validScopes.map((s) => `"${s}"`).join(', ')}].`
239 | : 'scope not in range. SHOULD NOT SEE THIS MESSAGE. PLEASE REPORT AN ISSUE.',
240 | },
241 | ) {
242 | let isValid = true;
243 | let invalidLength = false;
244 |
245 | /* eslint-enable no-useless-escape */
246 | const IGNORED_PATTERNS = [
247 | /(^WIP:)|(^\d+\.\d+\.\d+)/,
248 |
249 | /^Publish$/,
250 |
251 | // ignore auto-generated commit msg
252 | /^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)/m,
253 | ];
254 |
255 | if (IGNORED_PATTERNS.some((pattern) => pattern.test(message))) {
256 | shouldDisplayError && console.log('Commit message validation ignored.');
257 | return true;
258 | }
259 |
260 | verbose && debug(`commit message: |${message}|`);
261 |
262 | if (message.length > maxLen || message.length < minLen) {
263 | invalidLength = true;
264 | isValid = false;
265 | }
266 |
267 | // eslint-disable-next-line no-useless-escape
268 | if (englishOnly && !/^[a-zA-Z\s\.!@#$%^&*\(\)-_+=\\\|\[\]\{\};:'"?/.>,<]+$/.test(message)) {
269 | shouldDisplayError && console.log('');
270 | shouldDisplayError && console.warn(`${YELLOW}[git-commit-msg-linter] Commit message can not contain ${RED}non-English${EOS}${YELLOW} characters due to ${red('`englishOnly`')} ${yellow('in "commitlinterrc.json" is true.')}`);
271 |
272 | return false;
273 | }
274 |
275 | const matches = resolvePatterns(message);
276 |
277 | if (!matches) {
278 | shouldDisplayError && displayError(
279 | { invalidLength, invalidFormat: true },
280 | {
281 | mergedTypes,
282 | maxLen,
283 | minLen,
284 | message,
285 | example,
286 | showInvalidHeader,
287 | scopeDescriptions,
288 |
289 | subjectDescriptions,
290 | postSubjectDescriptions,
291 | invalidSubjectDescriptions,
292 | lang,
293 |
294 | scopeRequired,
295 | validScopes,
296 | invalidScopeDescriptions,
297 | },
298 | );
299 |
300 | return false;
301 | }
302 |
303 | const { type, scope, subject } = matches;
304 |
305 | verbose && debug(`type: ${type}, scope: ${scope}, subject: ${subject}`);
306 |
307 | const types = Object.keys(mergedTypes);
308 | const typeInvalid = !types.includes(type);
309 |
310 | const [invalidScope, reason] = isScopeInvalid(scope, { scopeRequired, validScopes });
311 |
312 | // Don't capitalize first letter; No dot (.) at the end
313 | const invalidSubject = isUpperCase(subject[0]) || subject.endsWith('.');
314 |
315 | if (invalidLength || typeInvalid || invalidScope || invalidSubject) {
316 | shouldDisplayError && displayError(
317 | {
318 | invalidLength, type, typeInvalid, invalidScope, invalidSubject,
319 | },
320 | {
321 | mergedTypes,
322 | maxLen,
323 | minLen,
324 | message,
325 | example,
326 | showInvalidHeader,
327 | scopeDescriptions,
328 | invalidScopeDescriptions: reason === 'NOT_IN_RANGE' ? castArry(scopeNotInRangeDescription) : invalidScopeDescriptions,
329 | subjectDescriptions,
330 | postSubjectDescriptions,
331 | invalidSubjectDescriptions,
332 | lang,
333 | scopeRequired,
334 | },
335 | );
336 |
337 | return false;
338 | }
339 |
340 | return isValid;
341 | }
342 |
343 | function displayError(
344 | {
345 | invalidLength = false,
346 | invalidFormat = false,
347 | type,
348 | typeInvalid = false,
349 | invalidScope = false,
350 | invalidSubject = false,
351 | } = {},
352 | {
353 | mergedTypes,
354 | maxLen,
355 | minLen,
356 | message,
357 | example,
358 | showInvalidHeader,
359 |
360 | scopeDescriptions,
361 | invalidScopeDescriptions,
362 | subjectDescriptions,
363 | postSubjectDescriptions,
364 | invalidSubjectDescriptions,
365 |
366 | lang,
367 | scopeRequired,
368 | },
369 | ) {
370 | const decoratedType = decorate('type', typeInvalid, true);
371 | const scope = decorate('scope', invalidScope, scopeRequired);
372 | const subject = decorate('subject', invalidSubject, true);
373 |
374 | const types = Object.keys(mergedTypes);
375 | const suggestedType = suggestType(type, types);
376 | const typeDescriptions = describeTypes(mergedTypes, suggestedType);
377 |
378 | const invalid = invalidLength || invalidFormat || typeInvalid || invalidScope || invalidSubject;
379 | const translated = getLangData(lang).i18n;
380 | const { invalidHeader } = translated;
381 | const header = !showInvalidHeader
382 | ? ''
383 | : `\n ${invalidFormat ? RED : YELLOW}************* ${invalidHeader} **************${EOS}`;
384 |
385 | const scopeDescription = scopeDescriptions.join('\n ');
386 | const invalidScopeDescription = invalidScopeDescriptions.join('\n ');
387 | const defaultInvalidScopeDescription = `scope can be ${emphasis('optional')}${RED}, but its parenthesis if exists cannot be empty.`;
388 |
389 | const subjectDescription = subjectDescriptions.join('\n ');
390 | let postSubjectDescription = postSubjectDescriptions.join('\n ');
391 | postSubjectDescription = postSubjectDescription ? `\n\n ${italic(postSubjectDescription)}` : '';
392 |
393 | const invalidSubjectDescription = invalidSubjectDescriptions.join('\n ');
394 |
395 | const { example: labelExample, correctFormat, commitMessage } = translated;
396 |
397 | const correctedExample = typeInvalid
398 | ? didYouMean(message, { example, types })
399 | : example;
400 |
401 | console.info(
402 | `${header}${invalid ? `
403 | ${label(`${commitMessage}:`)} ${RED}${message}${EOS}` : ''}${generateInvalidLengthTips(message, invalidLength, maxLen, minLen, lang)}
404 | ${label(`${correctFormat}:`)} ${GREEN}${decoratedType}${scope}: ${subject}${EOS}
405 | ${label(`${labelExample}:`)} ${GREEN}${correctedExample}${EOS}
406 |
407 | ${typeInvalid ? RED : YELLOW}type:
408 | ${typeDescriptions}
409 |
410 | ${invalidScope ? RED : YELLOW}scope:
411 | ${GRAY}${scopeDescription}${invalidScope ? `${RED}
412 | ${invalidScopeDescription || defaultInvalidScopeDescription}` : ''}
413 |
414 | ${invalidSubject ? RED : YELLOW}subject:
415 | ${GRAY}${subjectDescription}${postSubjectDescription}${invalidSubject ? `${RED}
416 | ${invalidSubjectDescription}` : ''}
417 | `,
418 | );
419 | }
420 |
421 | /**
422 | *
423 | * @param {string} example
424 | * @param {boolean} typeInvalid
425 | * @param mergedTypes
426 | *
427 | * @example
428 | * didYouMean('refact: abc', { types: ['refactor'], example: 'docs: xyz' })
429 | * => 'refactor: abc'
430 | *
431 | * didYouMean('abc', { types: ['refactor'], example: 'docs: xyz' })
432 | * => 'docs: xyz'
433 | */
434 | function didYouMean(message, { types, example }) {
435 | const patterns = resolvePatterns(message);
436 |
437 | if (!patterns && !patterns.type) {
438 | return example;
439 | }
440 |
441 | const { type } = patterns;
442 |
443 | // Get the closest match
444 | const suggestedType = suggestType(type, types);
445 |
446 | if (!suggestedType) {
447 | return example;
448 | }
449 |
450 | const TYPE_REGEXP = /^\w+(\(\w*\))?:/;
451 |
452 | return message.replace(TYPE_REGEXP, (_, p1) => (p1 && p1 !== '()' ? `${suggestedType}${p1}:` : `${suggestedType}:`));
453 | }
454 |
455 | function suggestType(type = '', types) {
456 | if (!Matcher) {
457 | return '';
458 | }
459 |
460 | const matcher = new Matcher(types);
461 | const match = matcher.get(type);
462 |
463 | if (match) { return match; }
464 |
465 | const suggestedType = types.find((t) => type.includes(t) || t.includes(type));
466 |
467 | return suggestedType || '';
468 | }
469 |
470 | /**
471 | * Decorate the part of pattern.
472 | *
473 | * @param {string} text Text to decorate
474 | * @param {boolean} invalid Whether the part is invalid
475 | * @param {boolean} required For example `scope` is optional
476 | *
477 | * @returns {string}
478 | */
479 | function decorate(text, invalid, required = true) {
480 | if (invalid) {
481 | return `${RED}${addPeripherals(underline(text) + RED, required)}`;
482 | }
483 |
484 | return `${GREEN}${addPeripherals(text, required)}`;
485 | }
486 |
487 | /**
488 | * Add peripherals.
489 | *
490 | * @example
491 | * addPeripherals('type')
492 | * // => ""
493 | * addPeripherals('scope', false)
494 | * // => "(scope)"
495 | *
496 | * @param {string} text
497 | * @param {boolean} required
498 | *
499 | * @returns {string}
500 | */
501 | function addPeripherals(text, required = true) {
502 | if (required) {
503 | return `<${text}>`;
504 | }
505 |
506 | return `(${text})`;
507 | }
508 |
509 | /**
510 | * Put emphasis on text.
511 | * @param {string} text
512 | * @returns {string}
513 | */
514 | function emphasis(text) {
515 | const ITALIC = '\x1b[3m';
516 | const UNDERLINED = '\x1b[4m';
517 |
518 | return `${ITALIC}${UNDERLINED}${text}${EOS}`;
519 | }
520 |
521 | /**
522 | * Make text italic.
523 | * @param {string} text
524 | * @returns {string}
525 | */
526 | function italic(text) {
527 | const ITALIC = '\x1b[3m';
528 |
529 | return `${ITALIC}${text}${EOS}`;
530 | }
531 |
532 | /**
533 | * Make text underlined.
534 | * @param {string} text
535 | * @returns {string}
536 | */
537 | function underline(text) {
538 | const UNDERLINED = '\x1b[4m';
539 |
540 | return `${UNDERLINED}${text}${EOS}`;
541 | }
542 |
543 | /**
544 | * Make text displayed with error style.
545 | * @param {string} text
546 | * @returns {string}
547 | */
548 | function red(text) {
549 | return `${RED}${text}${EOS}`;
550 | }
551 |
552 | function yellow(text) {
553 | return `${YELLOW}${text}${EOS}`;
554 | }
555 |
556 | /**
557 | * isUpperCase
558 | * @param {string} letter
559 | * @returns {boolean}
560 | */
561 | function isUpperCase(letter) {
562 | return /^[A-Z]$/.test(letter);
563 | }
564 |
565 | /**
566 | * return a string of n spaces
567 | *
568 | * @param {number}
569 | * @return {string}
570 | */
571 | function nSpaces(n) {
572 | const space = ' ';
573 |
574 | return space.repeat(n);
575 | }
576 |
577 | /**
578 | * generate a type description
579 | *
580 | * @param {number} options.index type index
581 | * @param {string} options.type type name
582 | * @param {string} options.description type description
583 | * @param {number} options.maxTypeLength max type length
584 | *
585 | * @returns {string}
586 | */
587 | function describe({
588 | index, type, suggestedType, description, maxTypeLength,
589 | }) {
590 | const paddingBefore = index === 0 ? '' : nSpaces(4);
591 | const marginRight = nSpaces(maxTypeLength - type.length + 1);
592 | const typeColor = suggestedType === type ? GREEN + BOLD : YELLOW;
593 |
594 | return `${paddingBefore}${typeColor}${type}${marginRight}${GRAY}${description}`;
595 | }
596 |
597 | /**
598 | * generate type descriptions
599 | *
600 | * @param {Object} mergedTypes
601 | * @returns {string} type descriptions
602 | */
603 | function describeTypes(mergedTypes, suggestedType = '') {
604 | const types = Object.keys(mergedTypes);
605 | const maxTypeLength = [...types].sort((t1, t2) => t2.length - t1.length)[0].length;
606 |
607 | return types
608 | .map((type, index) => {
609 | const description = mergedTypes[type];
610 |
611 | return describe({
612 | index, type, suggestedType, description, maxTypeLength,
613 | });
614 | })
615 | .join('\n');
616 | }
617 |
618 | /**
619 | * Style text like a label.
620 | * @param {string} text
621 | * @returns {string}
622 | */
623 | function label(text) {
624 | return `${BOLD}${text}${EOS}`;
625 | }
626 |
627 | /**
628 | * Generate invalid length tips.
629 | *
630 | * @param {string} message commit message
631 | * @param {boolean} invalid
632 | * @param {number} maxLen
633 | * @param {number} minLen
634 | * @param {string} lang
635 | * @returns {string}
636 | */
637 | function generateInvalidLengthTips(message, invalid, maxLen, minLen, lang) {
638 | if (invalid) {
639 | const max = `${BOLD}${maxLen}${EOS}${RED}`;
640 | const min = `${BOLD}${minLen}${EOS}${RED}`;
641 | // eslint-disable-next-line no-shadow
642 | const { i18n } = getLangData(lang);
643 | const tips = `${RED}${i18n.length} ${BOLD}${message.length}${EOS}${RED}. ${format(i18n.invalidLengthTip, max, min)}${EOS}`;
644 | return `\n ${BOLD}${i18n.invalidLength}${EOS}: ${tips}`;
645 | }
646 |
647 | return '';
648 | }
649 |
650 | /**
651 | * Output debugging information.
652 | * @param {any[]} args
653 | * @returns {void}
654 | */
655 | function debug(...args) {
656 | console.info(`${GREEN}[DEBUG]`, ...args, EOS);
657 | }
658 |
659 | /**
660 | * @returns {string}
661 | */
662 | function getLanguage(configLang) {
663 | const lang = configLang
664 | || process.env.COMMIT_MSG_LINTER_LANG
665 | || Intl.DateTimeFormat().resolvedOptions().locale;
666 |
667 | return lang;
668 | }
669 |
670 | /**
671 | * Replaces numeric arguments inside curly brackets with their corresponding values.
672 | *
673 | * @example
674 | * format( 'Good {1}, Mr {2}', 'morning', 'Bob' ) returns 'Good morning, Mr Bob'
675 | *
676 | * @param {string} text A text with arguments between curly brackets
677 | * @param {any[]} args Values to replace the arguments
678 | * @returns
679 | */
680 | function format(text, ...args) {
681 | return text.replace(/\{(\d+)\}/g, (_, i) => args[i - 1]);
682 | }
683 |
684 | /**
685 | *
686 | * @param {string} message
687 | * @returns {null | {type: string; scope: string | undefined; subject: string}}
688 | */
689 | function resolvePatterns(message) {
690 | // eslint-disable-next-line no-useless-escape
691 | const PATTERN = /^(?:fixup!\s*)?(\w+)(\(([\w\$\.\*/-]*(?:,[\w\$\.\*/-]+)*)\))?!?\: (.+)$/;
692 |
693 | const matches = PATTERN.exec(message);
694 |
695 | if (matches) {
696 | const type = matches[1];
697 | const scope = matches[3];
698 | const subject = matches[4];
699 |
700 | return {
701 | type,
702 | scope,
703 | subject,
704 | };
705 | }
706 |
707 | return null;
708 | }
709 |
710 | /**
711 | * - required: scope not exists FAIL, scope exists but not in range FAIL
712 | * - not required: scope not exits OK, scope exists but not in range FAIL
713 | * @param {string} scope
714 | * @param {{validScopes: string[]; scopeRequired: boolean;}} param1
715 | * @return {[invalid: false] | [invalid: true, reason: 'SCOPE_REQUIRED' | 'SCOPE_DUPLICATED' | 'NOT_IN_RANGE' | 'SCOPE_EMPTY_STRING']}
716 | */
717 | function isScopeInvalid(scope, { validScopes, scopeRequired }) {
718 | const scopeArr = scope ? scope.split(',') : [];
719 | const hasScope = (scopeArr.length > 0);
720 |
721 | if ((new Set(scopeArr)).size !== scopeArr.length) {
722 | return [true, 'SCOPE_DUPLICATED'];
723 | }
724 |
725 | const notInRange = () => Array.isArray(validScopes)
726 | && validScopes.length > 0
727 | && !scopeArr.every((item) => validScopes.includes(item));
728 |
729 | if (scopeRequired) {
730 | if (!hasScope) return [true, 'SCOPE_REQUIRED'];
731 |
732 | if (notInRange()) {
733 | return [true, 'NOT_IN_RANGE'];
734 | }
735 | }
736 |
737 | if (typeof scope === 'string') {
738 | // scope can be optional, but not empty string
739 | // @example
740 | // "test: hello" OK
741 | // "test(): hello" FAILED
742 | if (scope === '') { return [true, 'SCOPE_EMPTY_STRING']; }
743 |
744 | if (notInRange()) {
745 | return [true, 'NOT_IN_RANGE'];
746 | }
747 | }
748 |
749 | return [false];
750 | }
751 |
752 | function getLangs() {
753 | return {
754 | 'en-US': {
755 | stereotypes: {
756 | feat: 'A new feature.',
757 | fix: 'A bug fix.',
758 | docs: 'Documentation only changes.',
759 | style: 'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc).',
760 | refactor: 'A code change that neither fixes a bug nor adds a feature.',
761 | test: 'Adding missing tests or correcting existing ones.',
762 | chore: 'Changes to the build process or auxiliary tools and libraries such as documentation generation.',
763 | // added
764 | perf: 'A code change that improves performance.',
765 | ci: 'Changes to your CI configuration files and scripts.',
766 | build: 'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm).',
767 | temp: 'Temporary commit that won\'t be included in your CHANGELOG.',
768 | },
769 | descriptions: {
770 | example: config.scopeRequired
771 | ? 'docs(README): add developer tips'
772 | : 'docs: update README to add developer tips',
773 |
774 | scope: [
775 | `${config.scopeRequired ? 'Required'
776 | : 'Optional'}, can be anything specifying the scope of the commit change.`,
777 | 'For example $location, $browser, $compile, $rootScope, ngHref, ngClick, ngView, etc.',
778 | 'In App Development, scope can be a page, a module or a component.',
779 | ],
780 | invalidScope: [
781 | config.scopeRequired ? '`scope` required.'
782 | : '`scope` is optional, but if it exists, the parentheses cannot be empty and scopes cannot be duplicated.',
783 | ],
784 | subject: [
785 | 'Brief summary of the change in present tense. Not capitalized. No period at the end.',
786 | ],
787 | invalidSubject: [
788 | '- don\'t capitalize first letter',
789 | '- no dot "." at the end`',
790 | ],
791 | },
792 | i18n: {
793 | invalidHeader: 'Invalid Git Commit Message',
794 | example: 'example',
795 | commitMessage: 'commit message',
796 | correctFormat: 'correct format',
797 | invalidLength: 'Invalid length',
798 | length: 'Length',
799 | invalidLengthTip: 'Commit message cannot be longer than {1} characters or shorter than {2} characters',
800 | },
801 | },
802 |
803 | 'zh-CN': {
804 | stereotypes: {
805 | feat: '产品新功能:通常是能够让用户觉察到的变化,小到文案或样式修改',
806 | fix: '修复 bug',
807 | docs: '更新文档或注释',
808 | style: '代码格式调整,对逻辑无影响:比如为按照 eslint 或团队风格修改代码格式。注意不是 UI 变更',
809 | refactor: '重构:不影响现有功能或添加功能。比如文件、变量重命名,代码抽象为函数,消除魔法数字等',
810 | test: '单测相关变更',
811 | chore: '杂项:其他无法归类的变更,比如代码合并',
812 | // added
813 | perf: '性能提升变更',
814 | ci: '持续集成相关变更',
815 | build: '代码构建相关变更:比如修复部署时的构建问题、构建脚本 webpack 或 gulp 相关变更',
816 | temp: '临时代码:不计入 CHANGELOG,比如必须部署到某种环境才能测试的变更',
817 | },
818 | descriptions: {
819 | example: config.scopeRequired
820 | ? 'docs(README): 添加开发者部分'
821 | : 'docs: 更新 README 添加开发者部分',
822 |
823 | scope: [
824 | '可选。变更范围(细粒度要合适,并在一个项目中保持一致):比如页面名、模块名、或组件名',
825 | ],
826 | invalidScope: [
827 | config.scopeRequired ? '`scope` 必选'
828 | : '`scope` 可选,若有则必须加小括号',
829 | ],
830 | subject: [
831 | '此次变更的简短描述,必须采用现在时态,如果是英语则首字母不能大写,句尾不加句号',
832 | ],
833 | invalidSubject: [
834 | '首字母不能大写',
835 | '句尾不加句号',
836 | ],
837 | },
838 | i18n: {
839 | invalidHeader: 'Git 提交信息不规范',
840 | example: '示例',
841 | commitMessage: '提交信息',
842 | correctFormat: '正确格式',
843 | invalidLength: '提交信息长度不合法',
844 | length: '长度',
845 | invalidLengthTip: '提交信息长度不能大于 {1} 或小于 {2}',
846 | },
847 | },
848 |
849 | 'pt-BR': {
850 | stereotypes: {
851 | feat: 'Adição de funcionalidade.',
852 | fix: 'Correção de defeito.',
853 | docs: 'Mudança em documentação.',
854 | style: 'Mudança de formatação ou estilo, que não afeta a execução do código (espaço, tabulação, etc).',
855 | refactor: 'Mudança na organização do código, que não afeta o comportamento existente.',
856 | test: 'Adição ou mudança de um teste.',
857 | chore: 'Adição ou mudança em script de build, que não afeta o código de produção.',
858 | // added
859 | perf: 'Mudança de código para melhoria de desempenho.',
860 | ci: 'Mudança de configuração de integração contínua.',
861 | build: 'Mudança em arquivos de build ou em dependências externas.',
862 | temp: 'Commit temporário, que não deve ser incluído no CHANGELOG.',
863 | },
864 | descriptions: {
865 | example: config.scopeRequired
866 | ? 'docs(README): link para a nova documentação'
867 | : 'docs: atualiza o README com link para a nova documentação',
868 |
869 | scope: [
870 | 'Opcional, pode ser qualquer coisa que especifique o escopo da mudança.',
871 | 'Exemplos: subpacote, workspace, módulo, componente, página.',
872 | ],
873 | invalidScope: [
874 | '`scope` é opcional, mas o conteúdo entre parênteses não pode ficar vazio.',
875 | ],
876 | subject: [
877 | 'Breve resumo da mudança, escrito no tempo verbal presente. Começa com letra minúscula e não há ponto final.',
878 | ],
879 | invalidSubject: [
880 | '- não coloque a primeira letra em maiúsculo',
881 | '- não use ponto final',
882 | ],
883 | },
884 | i18n: {
885 | invalidHeader: 'Mensagem de commit inválida',
886 | example: 'exemplo',
887 | commitMessage: 'mensagem de commit',
888 | correctFormat: 'formato correto',
889 | invalidLength: 'Comprimento inválido',
890 | length: 'Comprimento',
891 | invalidLengthTip: 'Mensagem de commit não pode ser maior que {1} caracteres ou menor que {2}',
892 | },
893 | },
894 |
895 | 'es-ES': {
896 | stereotypes: {
897 | feat: 'Una nueva funcionalidad.',
898 | fix: 'Corregir un error.',
899 | docs: 'Cambios únicamente en la documentación.',
900 | style: 'Cambios que no afectan la ejecución del código (espacios en blanco, formato, falta de punto y coma, etc.).',
901 | refactor: 'Un cambio de código que no afecta el funcionamiento existente (no corrige un error ni añade una función.)',
902 | test: 'Añadir pruebas que faltan o corregir las existentes.',
903 | chore: 'Cambios en el proceso de construcción o en las herramientas y bibliotecas auxiliares, como la generación de documentación, sin afectar el código de producción.',
904 | // added
905 | perf: 'Un cambio de código que mejora el rendimiento.',
906 | ci: 'Cambios en sus archivos de configuración y scripts de CI.',
907 | build: 'Cambios que afectan al sistema de construcción o a las dependencias externas (ejemplos de ámbitos: gulp, broccoli, npm).',
908 | temp: 'Cambio temporal que no se incluirá en su CHANGELOG.',
909 | },
910 | descriptions: {
911 | example: 'docs: actualiza el README para añadir consejos para desarrolladores',
912 | scope: [
913 | 'Opcional, puede ser cualquier cosa que especifique el alcance del cambio en el commit.',
914 | 'Por ejemplo $location, $browser, $compile, $rootScope, ngHref, ngClick, ngView, etc.',
915 | 'En el desarrollo, el ámbito puede ser una página, un módulo o un componente.',
916 | ],
917 | invalidScope: [
918 | 'El `alcance` puede ser opcional, pero su paréntesis, si existe, no puede estar vacío.',
919 | ],
920 | subject: [
921 | 'Breve resumen del cambio en tiempo presente. Sin mayúsculas. Sin punto al final.',
922 | ],
923 | invalidSubject: [
924 | '- no escriba la primera letra en mayúscula',
925 | '- no use "." al final`',
926 | ],
927 | },
928 | i18n: {
929 | invalidHeader: 'Mensaje del commit inválido',
930 | example: 'ejemplo',
931 | commitMessage: 'mensaje del commit',
932 | correctFormat: 'formato correcto',
933 | invalidLength: 'longitud inválida',
934 | length: 'Longitud',
935 | invalidLengthTip: 'El mensaje del commit no puede tener más de {1} caracteres, o menos de {2}',
936 | },
937 | },
938 | };
939 | }
940 |
941 | function castArry(val) {
942 | return Array.isArray(val) ? val : [val];
943 | }
944 |
--------------------------------------------------------------------------------