validate-commit-msg.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #!/usr/bin/env node
  2. /**
  3. * Git COMMIT-MSG hook for validating commit message
  4. * See https://docs.google.com/document/d/1rk04jEuGfk9kYzfqCuOlPTSJw3hEDZJTBN5E5f1SALo/edit
  5. *
  6. * Installation:
  7. * >> use ghooks, config in package.json
  8. */
  9. const fs = require("fs");
  10. const util = require("util");
  11. const resolve = require("path").resolve;
  12. const findup = require("findup");
  13. function semverRegex() {
  14. return /(?<=^v?|\sv?)(?:(?:0|[1-9]\d{0,9}?)\.){2}(?:0|[1-9]\d{0,9})(?:-(?:--+)?(?:0|[1-9]\d*|\d*[a-z]+\d*)){0,100}(?=$| |\+|\.)(?:(?<=-\S+)(?:\.(?:--?|[\da-z-]*[a-z-]\d*|0|[1-9]\d*)){1,100}?)?(?!\.)(?:\+(?:[\da-z]\.?-?){1,100}?(?!\w))?(?!\+)/gi;
  15. }
  16. const config = getConfig();
  17. const MAX_LENGTH = config.maxSubjectLength || 100;
  18. // 忽略commit msg 中 以 ‘WIP、v’开头的字符串或语义化字符串
  19. const IGNORED = new RegExp(
  20. util.format("(^WIP)|(^v)|(^%s$)", semverRegex().source)
  21. );
  22. function log(...content) {
  23. return console.log("validate commit msg:", ...content);
  24. }
  25. log("校验 commit message");
  26. // fixup! and squash! are part of Git, commits tagged with them are not intended
  27. // to be merged, cf. https://git-scm.com/docs/git-commit
  28. const PATTERN = /^((fixup! |squash! )?(\w+)(?:\(([^)\s]+)\))?: (.+))(?:\n|$)/;
  29. const MERGE_COMMIT_PATTERN = /^Merge /;
  30. const error = function (title, ...argv) {
  31. // gitx does not display it
  32. // http://gitx.lighthouseapp.com/projects/17830/tickets/294-feature-display-hook
  33. // - error-message-when-hook-fails
  34. // https://groups.google.com/group/gitx/browse_thread/thread/a03bcab60844b812
  35. const type = config.warnOnFail ? "warn" : "error";
  36. console[type](`validate commit msg: commit message 不符合规范`);
  37. console[type](`validate commit msg: ${title}`, ...argv);
  38. };
  39. const validateMessage = function (raw) {
  40. console.log(raw);
  41. // config.types = config.types || resolve(process.cwd(), '.vsc-commitizen.json');
  42. const types = config.types ? config.types.map((item) => item.label) : null;
  43. const scopes = config.scopes ? config.scopes.map((item) => item.label) : null;
  44. const messageWithBody = (raw || "")
  45. .split("\n")
  46. .filter((str) => str.indexOf("#") !== 0)
  47. .join("\n");
  48. const message = messageWithBody.split("\n").shift();
  49. if (message === "") {
  50. log("由于 commit message 为空,所以中止提交。");
  51. return false;
  52. }
  53. let isValid = true;
  54. if (MERGE_COMMIT_PATTERN.test(message)) {
  55. log("检测到有 Merge commit。");
  56. return true;
  57. }
  58. if (IGNORED.test(message)) {
  59. log("忽略 Commit message。");
  60. return true;
  61. }
  62. const match = PATTERN.exec(message);
  63. if (!match) {
  64. error(
  65. `与规范不相符:
  66. <类型 *>[(<作用域>)]: [<emoji>] <主题 *>
  67. [空行]
  68. [详细描述]
  69. [空行]
  70. [底部信息]
  71. 规范地址:http://confluence.pri.ibanyu.com/pages/viewpage.action?pageId=1737437#Git&Gitlab工作流-4、Git提交信息格式规范
  72. 类型、作用域 查看:./.vsc-commitizen.json`
  73. );
  74. isValid = false;
  75. } else {
  76. const firstLine = match[1];
  77. const squashing = !!match[2];
  78. const type = match[3];
  79. const scope = match[4];
  80. const subject = match[5];
  81. const SUBJECT_PATTERN = new RegExp(config.subjectPattern || ".+");
  82. const SUBJECT_PATTERN_ERROR_MSG =
  83. config.subjectPatternErrorMsg || "主题不符合规范!";
  84. if (firstLine.length > MAX_LENGTH && !squashing) {
  85. error("主题超过 %d 字符限制!", MAX_LENGTH);
  86. isValid = false;
  87. }
  88. if (scopes !== null && scope !== void 0 && scopes.indexOf(scope) === -1) {
  89. error('"%s" 是无效的scope!请选择以下范围: %s', scope, scopes.join(", "));
  90. isValid = false;
  91. }
  92. if (types !== null && types.indexOf(type) === -1) {
  93. error('"%s" 是无效的type!请选择以下类型: %s', type, types.join(", "));
  94. isValid = false;
  95. }
  96. if (!SUBJECT_PATTERN.exec(subject)) {
  97. error(SUBJECT_PATTERN_ERROR_MSG);
  98. isValid = false;
  99. }
  100. }
  101. // Some more ideas, do want anything like this ?
  102. // - Validate the rest of the message (body, footer, BREAKING CHANGE
  103. // annotations)
  104. // - allow only specific scopes (eg. fix(docs) should not be allowed ?
  105. // - auto correct the type to lower case ?
  106. // - auto correct first letter of the subject to lower case ?
  107. // - auto add empty line after subject ?
  108. // - auto remove empty () ?
  109. // - auto correct typos in type ?
  110. // - store incorrect messages, so that we can learn
  111. isValid = isValid || config.warnOnFail;
  112. if (isValid) {
  113. // exit early and skip messaging logics
  114. log("校验完成!👏 👏 👏");
  115. return true;
  116. }
  117. const argInHelp =
  118. config.helpMessage && config.helpMessage.indexOf("%s") !== -1;
  119. if (argInHelp) {
  120. log(config.helpMessage, messageWithBody);
  121. } else if (message) {
  122. log(message);
  123. }
  124. if (!argInHelp && config.helpMessage) {
  125. log(config.helpMessage);
  126. }
  127. return false;
  128. };
  129. // publish for testing
  130. exports.validateMessage = validateMessage;
  131. exports.getGitFolder = getGitFolder;
  132. exports.config = config;
  133. // hacky start if not run by mocha :-D istanbul ignore next
  134. if (process.argv.join("").indexOf("mocha") === -1) {
  135. const commitMsgFile = `${getGitFolder()}/COMMIT_EDITMSG`;
  136. // const incorrectLogFile = commitMsgFile.replace('COMMIT_EDITMSG', 'logs/incorrect-commit-msgs');
  137. const hasToString = function hasToString(x) {
  138. return x && typeof x.toString === "function";
  139. };
  140. fs.readFile(commitMsgFile, (err, buffer) => {
  141. const msg = getCommitMessage(buffer);
  142. if (!validateMessage(msg)) {
  143. console.log(`提交信息:\n${msg}\n`);
  144. process.exit(1);
  145. } else {
  146. process.exit(0);
  147. }
  148. function getCommitMessage(buf) {
  149. return hasToString(buf) && buf.toString();
  150. }
  151. });
  152. }
  153. function getConfig() {
  154. const pkgFile = findup.sync(process.cwd(), "package.json");
  155. const pkg = JSON.parse(fs.readFileSync(resolve(pkgFile, "package.json")));
  156. if (pkg && pkg.config) {
  157. return pkg.config["validate-commit-msg"];
  158. }
  159. const vsc = resolve(process.cwd(), "scripts/.vsc-commitizen.json");
  160. const config = fs.existsSync(vsc) ? require(vsc) : {};
  161. return config;
  162. }
  163. function getGitFolder() {
  164. let gitDirLocation = "./.git";
  165. if (!fs.existsSync(gitDirLocation)) {
  166. throw new Error(`Cannot find file ${gitDirLocation}`);
  167. }
  168. if (!fs.lstatSync(gitDirLocation).isDirectory()) {
  169. const unparsedText = `${fs.readFileSync(gitDirLocation)}`;
  170. gitDirLocation = unparsedText.substring("gitdir: ".length).trim();
  171. }
  172. if (!fs.existsSync(gitDirLocation)) {
  173. throw new Error(`Cannot find file ${gitDirLocation}`);
  174. }
  175. return gitDirLocation;
  176. }