Skip to content

Commit 1f70f20

Browse files
authored
Merge pull request #617 from HelixDesignSystem/add-pre-commit-hook
build: add project git-hooks
2 parents fa28267 + b803a2e commit 1f70f20

File tree

12 files changed

+337
-4
lines changed

12 files changed

+337
-4
lines changed

.node-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8.15.0
1+
12.10.0

.npmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
registry=https://registry.npmjs.org/
22

3-
message = "chore(release): v%s [skip ci]"
3+
message = "publish(npm): v%s [skip ci]"
44
preid = "rc"

git-hooks/commit-msg

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
const { TYPES, Err } = require('./commit');
7+
8+
/**
9+
* @param string header
10+
* @returns object - object with the following keys:
11+
* - isBreaking: true if header implies a breaking change with an exclamation point
12+
* - scope: matched value for the optional <scope> portion of the header
13+
* - summary: matched value for the <summary> portion of the header
14+
* - type: matched value for the <type> portion of the header
15+
*/
16+
function parseHeader (header) {
17+
// See https://regex101.com/r/FRRac2/10
18+
let pattern = /^(?<type>\w+)(\((?<scope>.*)\))?(?<isBreaking>!)?: (?<summary>.+)$/;
19+
20+
// check header for matches against the regex pattern
21+
let matches = pattern.exec(header);
22+
23+
// default capture group values if there are no matches
24+
let groups = {
25+
isBreaking: null,
26+
scope: null,
27+
summary: null,
28+
type: null,
29+
};
30+
31+
// replace default capture group values with the real values
32+
if (matches && matches.groups) {
33+
groups = matches.groups;
34+
}
35+
36+
return {
37+
...groups,
38+
isBreaking: !!groups.isBreaking,
39+
matches,
40+
};
41+
}
42+
43+
/**
44+
* Validate format of the message header
45+
*
46+
* @param string header
47+
* @returns boolean
48+
*/
49+
function isValidHeader (header) {
50+
let { matches, type, scope } = parsed = parseHeader(header);
51+
52+
if (!matches) {
53+
Err.incorrectFormat(header);
54+
return false;
55+
}
56+
57+
if (!TYPES.hasOwnProperty(type)) {
58+
Err.unknownType(header, type);
59+
return false;
60+
}
61+
62+
if (typeof scope === 'string' && scope.trim() === '') {
63+
Err.emptyScope(header);
64+
return false;
65+
}
66+
67+
return true;
68+
}
69+
70+
/**
71+
* Split raw commit message into an array of message lines (excluding comments)
72+
*
73+
* @param string message - commit message contents
74+
* @returns array<string>
75+
*/
76+
function getLinesWithoutComments (message) {
77+
let allLines = message.split('\n');
78+
79+
// filter out comments
80+
let nonCommentLines = allLines.filter( line => !line.match(/^#/) );
81+
82+
return nonCommentLines;
83+
}
84+
85+
/**
86+
* git uses the first non-empty, non-comment line as the header
87+
* for the commit message, so we need to do the same
88+
*
89+
* @param string message - full git commit message (may be multi-line)
90+
* @returns string - first non-empty, non-comment line of text
91+
*/
92+
function getFirstMessageLine (message) {
93+
let nonCommentLines = getLinesWithoutComments(message);
94+
95+
// exclude empty/blank lines
96+
let linesWithContent = nonCommentLines.filter( line => line.trim().length );
97+
98+
// if no usable content, print error and exit 1 to fail hook
99+
if (linesWithContent.length === 0) {
100+
Err.unusableContent(message);
101+
process.exit(1);
102+
}
103+
104+
// return first non-empty, non-comment line of text
105+
return linesWithContent[0];
106+
}
107+
108+
(function () {
109+
console.log('[commit-msg] checking git commit message format');
110+
111+
let msgBuffer = fs.readFileSync(path.resolve(__dirname, '..', process.argv[2]));
112+
let msgString = msgBuffer.toString();
113+
let strHeader = getFirstMessageLine(msgString);
114+
let isValid = isValidHeader(strHeader);
115+
116+
if (!isValid) {
117+
// exit with non-zero status code to fail hook
118+
process.exit(1);
119+
} else {
120+
// exit with zero status code to pass hook
121+
process.exit(0);
122+
}
123+
})()

git-hooks/commit/errors.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const TYPES = require('./types');
2+
const { format } = require('../util/formatting');
3+
4+
/* STYLES */
5+
const h1 = format({ bold: true, underline: true });
6+
const h2 = format({ bold: true });
7+
8+
9+
/* SNIPPETS */
10+
const SYNTAX = '<type>[(<scope>)][!]: <summary>';
11+
const SYNTAX_NO_SCOPE = '<type>[!]: <summary>';
12+
const TYPES_INLINE = Object.keys(TYPES).join(', ');
13+
14+
15+
/* ERROR FUNCTIONS */
16+
17+
const incorrectFormat = (txtMsg) => console.error(`
18+
${h1('ERROR: Submitted commit message header does not match expected format!')}
19+
20+
${h2('Submitted')}
21+
${txtMsg}
22+
23+
${h2('Syntax')}
24+
${SYNTAX}
25+
`);
26+
27+
const unknownType = (header, type) => console.error(`
28+
${h1('ERROR: Submitted commit message header has unknown commit type!')}
29+
30+
${h2('Submitted Header')}
31+
${header}
32+
33+
${h2('Parsed Type')}
34+
${type}
35+
36+
${h2('Valid Types')}
37+
${TYPES_INLINE}
38+
39+
${h2('Syntax')}
40+
${SYNTAX}
41+
`);
42+
43+
const unusableContent = (msgContent) => console.error(`
44+
${h1('ERROR: Submitted commit message contains no useful content!')}
45+
46+
Please make sure that your commit message has at least one
47+
line of non-comment text.
48+
49+
${h2('Syntax')}
50+
${SYNTAX}
51+
52+
[<body>]
53+
54+
[<footer>]
55+
56+
${h2('Submitted Message Content')}
57+
// ---------- BEGIN ---------- //
58+
${msgContent}
59+
// ----------- END ----------- //
60+
`);
61+
62+
const emptyScope = (header) => console.error(`
63+
${h1('ERROR: Submitted commit message header has empty scope!')}
64+
65+
${h2('Submitted Header')}
66+
${header}
67+
68+
${h2('Syntax')}
69+
If you do not need a scope, consider using the scope-less syntax:
70+
71+
${SYNTAX_NO_SCOPE}
72+
73+
Otherwise, please make sure that the scope is not blank.
74+
75+
${SYNTAX}
76+
`);
77+
78+
79+
/* EXPORTS */
80+
module.exports = {
81+
emptyScope,
82+
incorrectFormat,
83+
unknownType,
84+
unusableContent,
85+
};

git-hooks/commit/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const TYPES = require('./types');
2+
const Err = require('./errors');
3+
4+
module.exports = {
5+
Err,
6+
TYPES,
7+
};

git-hooks/commit/types.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
'build': 'update/modify build system and/or dependencies',
3+
'chore': 'any change that does not fit into any other type',
4+
'docs': 'changes to documentation content',
5+
'feat': 'add new functionality (semver MINOR)',
6+
'fix': 'patch bug/error (semver PATCH)',
7+
'perf': 'code changes that improve performance',
8+
'publish': 'reserved for pipeline processes',
9+
'refactor': 'code changes that neither fix a bug nor add a feature',
10+
'saas': 'changes to SAAS configurations',
11+
'style': 'code changes that do not modify algorithms (white space, linting, etc.)',
12+
'test': 'add missing tests or update existing tests',
13+
};

git-hooks/pre-commit

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/sh
2+
3+
# Check for linting errors
4+
echo "[pre-commit] check linting"
5+
npm run lint

git-hooks/util/formatting.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// See http://bit.ly/2ORJ0x2 (\x1b sequence)
2+
// See http://bit.ly/2psMdZ4 (additional sequences)
3+
const FORMAT = {
4+
bg: {
5+
black: '\x1b[40m',
6+
blue: '\x1b[44m',
7+
cyan: '\x1b[46m',
8+
green: '\x1b[42m',
9+
magenta: '\x1b[45m',
10+
red: '\x1b[41m',
11+
white: '\x1b[47m',
12+
yellow: '\x1b[43m',
13+
},
14+
fg: {
15+
black: '\x1b[30m',
16+
blue: '\x1b[34m',
17+
cyan: '\x1b[36m',
18+
gray: '\x1b[90m',
19+
green: '\x1b[32m',
20+
magenta: '\x1b[35m',
21+
red: '\x1b[31m',
22+
white: '\x1b[37m',
23+
yellow: '\x1b[33m',
24+
},
25+
blink: '\x1b[5m',
26+
bold: '\x1b[1m',
27+
dim: '\x1b[2m',
28+
hidden: '\x1b[8m',
29+
invert: '\x1b[7m',
30+
italic: '\x1b[3m',
31+
reset: '\x1b[0m',
32+
strike: '\x1b[9m',
33+
underline: '\x1b[4m',
34+
};
35+
36+
/**
37+
* Define a custom formatting function to apply to a string.
38+
*
39+
* @param object cfg - see FORMAT for valid props
40+
* @returns function - returns a function that accepts a string to apply formatting
41+
* obj -> string -> string
42+
* obj => { string => string }
43+
*/
44+
const format = (cfg={}) => (txt='') => {
45+
// break down configuration into array of [key, value] items
46+
let cfgEntries = Object.entries(cfg);
47+
let { reset, fg, bg } = FORMAT;
48+
49+
// reduce configuration into a single string of formating escape sequences
50+
let fmtSeq = cfgEntries.reduce( (seq, entry) => {
51+
let [ key, val ] = entry;
52+
let _fmt = '';
53+
54+
// get format escape sequence for entry
55+
switch (key) {
56+
case 'fg':
57+
_fmt = fg[val] || fg['white'];
58+
break;
59+
case 'bg':
60+
_fmt = bg[val] || bg['black'];
61+
break;
62+
default:
63+
// only add format to sequence if opt is true
64+
if (!!val) {
65+
_fmt = FORMAT[key] || '';
66+
}
67+
break;
68+
}
69+
70+
// append escape sequence to list of all sequences
71+
return `${seq}${_fmt}`;
72+
}, '');
73+
74+
// wrap text in formatting and reset escape sequences
75+
return `${fmtSeq}${txt}${reset}`;
76+
};
77+
78+
module.exports = {
79+
format
80+
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
"rollup": "rollup -c rollup.config.js",
8282
"preserve": "yarn clean:public && yarn generate",
8383
"serve": "scripts/serve.js",
84-
"prestart": "yarn clean:public && yarn generate",
84+
"prestart": "scripts/setup-git-hooks.sh && yarn clean:public && yarn generate",
8585
"start": "scripts/start.js",
8686
"test:unit": "mocha test/run_unit.js",
8787
"test": "cd test; yarn test"

scripts/_publish/publishDocs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async function publishDocs () {
1616
let cfg = {
1717
add: false, // replace all gh-pages content, do not append
1818
dotfiles: false,
19-
message: `(${PKG.version}) Updated: ${moment().format('YYYY-MM-DD HH:mm:ss')}`,
19+
message: `publish(docs): v${PKG.version} (${moment().format('YYYY-MM-DD HH:mm:ss')})`,
2020
remote: 'upstream',
2121
silent: false,
2222
};

0 commit comments

Comments
 (0)