├── 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 | git-commit-msg-linter zh-CN demo 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 | git-commit-msg-linter zh-CN demo 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 | npm version 6 | 7 | 8 | npm downloads 9 | 10 | prerequisite node version 11 | visitor count 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 | ![git-commit-msg-linter-demo](https://raw.githubusercontent.com/legend80s/commit-msg-linter/master/assets/demo-7-compressed.png) 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 | ![detailed-config-demo](https://raw.githubusercontent.com/legend80s/commit-msg-linter/master/assets/detailed-config-wx-compressed.png) 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 | commit msg linted by git-commit-msg-linter 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 | npm version 6 | 7 | 8 | npm downloads 9 | 10 | prerequisite node version 11 | visitor count 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 | ![git-commit-msg-linter-demo](https://raw.githubusercontent.com/legend80s/commit-msg-linter/master/assets/demo-7-compressed.png) 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 | ![detailed-config-demo](https://raw.githubusercontent.com/legend80s/commit-msg-linter/master/assets/detailed-config-wx-compressed.png) 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 | commit msg linted by git-commit-msg-linter 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 | --------------------------------------------------------------------------------