Skip to content

Commit 26533fc

Browse files
olgnjimthedev
authored andcommittedApr 18, 2019
feat(cli): Implement --hook option for git hooks integration (#615)
fixes #448 (re #462) This pr allows project maintainers to enforce Commitizen generated commit messages as part of the workflow triggered by the `git commit` command. * implements the `--hook` flag, which directs Commitizen to edit the `.git/COMMIT_EDITMSG` file directly. * documents the use of the `--hook` flag in the `README`, both through traditional `git hooks` and `husky`.
1 parent 515a57e commit 26533fc

File tree

5 files changed

+162
-38
lines changed

5 files changed

+162
-38
lines changed
 

‎README.md

+32
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,38 @@ This will be more convenient for your users because then if they want to do a co
141141

142142
> **NOTE:** if you are using `precommit` hooks thanks to something like `husky`, you will need to name your script some thing other than "commit" (e.g. "cm": "git-cz"). The reason is because npm-scripts has a "feature" where it automatically runs scripts with the name *prexxx* where *xxx* is the name of another script. In essence, npm and husky will run "precommit" scripts twice if you name the script "commit," and the work around is to prevent the npm-triggered *precommit* script.
143143
144+
#### Optional: Running Commitizen on `git commit`
145+
146+
This example shows how to incorporate Commitizen into the existing `git commit` workflow by using git hooks and the `--hook` command-line option. This is useful for project maintainers
147+
who wish to ensure the proper commit format is enforced on contributions from those unfamiliar with Commitizen.
148+
149+
Once either of these methods is implemented, users running `git commit` will be presented with an interactive Commitizen session that helps them write useful commit messages.
150+
151+
> **NOTE:** This example assumes that the project has been set up to [use Commitizen locally](https://github.com/commitizen/cz-cli#optional-install-and-run-commitizen-locally).
152+
153+
##### Traditional git hooks
154+
155+
Update `.git/hooks/prepare-commit-msg` with the following code:
156+
157+
```
158+
#!/bin/bash
159+
exec < /dev/tty
160+
node_modules/.bin/git-cz --hook
161+
```
162+
163+
##### Husky
164+
For `husky` users, add the following configuration to the project's `package.json`:
165+
166+
```
167+
"husky": {
168+
"hooks": {
169+
"prepare-commit-msg": "exec < /dev/tty && git cz --hook",
170+
}
171+
}
172+
```
173+
174+
> **Why `exec < /dev/tty`?** By default, git hooks are not interactive. This command allows the user to use their terminal to interact with Commitizen during the hook.
175+
144176
#### Congratulations your repo is Commitizen-friendly. Time to flaunt it!
145177

146178
Add the Commitizen-friendly badge to your README using the following markdown:

‎src/cli/strategies/git-cz.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ function gitCz (rawGitArgs, environment, adapterConfig) {
3535
// normal commit.
3636
let retryLastCommit = rawGitArgs && rawGitArgs[0] === '--retry';
3737

38+
// Determine if we need to process this commit using interactive hook mode
39+
// for husky prepare-commit-message
40+
let hookMode = !(typeof parsedCommitizenArgs.hook === 'undefined');
41+
3842
let resolvedAdapterConfigPath = resolveAdapterPath(adapterConfig.path);
3943
let resolvedAdapterRootPath = findRoot(resolvedAdapterConfigPath);
4044
let prompter = getPrompter(adapterConfig.path);
@@ -57,7 +61,8 @@ function gitCz (rawGitArgs, environment, adapterConfig) {
5761
disableAppendPaths: true,
5862
emitData: true,
5963
quiet: false,
60-
retryLastCommit
64+
retryLastCommit,
65+
hookMode
6166
}, function (error) {
6267
if (error) {
6368
throw error;

‎src/git/commit.js

+68-29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { spawn } from 'child_process';
22

3+
import path from 'path';
4+
5+
import { writeFileSync, openSync, closeSync } from 'fs';
6+
37
import dedent from 'dedent';
48

59
export { commit };
@@ -9,35 +13,70 @@ export { commit };
913
*/
1014
function commit (sh, repoPath, message, options, done) {
1115
let called = false;
12-
let args = ['commit', '-m', dedent(message), ...(options.args || [])];
13-
let child = spawn('git', args, {
14-
cwd: repoPath,
15-
stdio: options.quiet ? 'ignore' : 'inherit'
16-
});
17-
18-
child.on('error', function (err) {
19-
if (called) return;
20-
called = true;
21-
22-
done(err);
23-
});
24-
25-
child.on('exit', function (code, signal) {
26-
if (called) return;
27-
called = true;
28-
29-
if (code) {
30-
if (code === 128) {
31-
console.warn(`
32-
Git exited with code 128. Did you forget to run:
33-
34-
git config --global user.email "you@example.com"
35-
git config --global user.name "Your Name"
36-
`)
16+
17+
// commit the file by spawning a git process, unless the --hook
18+
// option was provided. in that case, write the commit message into
19+
// the .git/COMMIT_EDITMSG file
20+
if (!options.hookMode) {
21+
let args = ['commit', '-m', dedent(message), ...(options.args || [])];
22+
let child = spawn('git', args, {
23+
cwd: repoPath,
24+
stdio: options.quiet ? 'ignore' : 'inherit'
25+
});
26+
27+
child.on('error', function (err) {
28+
if (called) return;
29+
called = true;
30+
31+
done(err);
32+
});
33+
34+
child.on('exit', function (code, signal) {
35+
if (called) return;
36+
called = true;
37+
38+
if (code) {
39+
if (code === 128) {
40+
console.warn(`
41+
Git exited with code 128. Did you forget to run:
42+
43+
git config --global user.email "you@example.com"
44+
git config --global user.name "Your Name"
45+
`)
46+
}
47+
done(Object.assign(new Error(`git exited with error code ${code}`), { code, signal }));
48+
} else {
49+
done(null);
50+
}
51+
});
52+
} else {
53+
const commitFilePath = path.join(repoPath, '/.git/COMMIT_EDITMSG');
54+
try {
55+
const fd = openSync(commitFilePath, 'w');
56+
try {
57+
writeFileSync(fd, dedent(message));
58+
done(null);
59+
} catch (e) {
60+
done(e);
61+
} finally {
62+
closeSync(fd);
63+
}
64+
} catch (e) {
65+
// windows doesn't allow opening existing hidden files
66+
// in 'w' mode... but it does let you do 'r+'!
67+
try {
68+
const fd = openSync(commitFilePath, 'r+');
69+
try {
70+
writeFileSync(fd, dedent(message));
71+
done(null);
72+
} catch (e) {
73+
done(e);
74+
} finally {
75+
closeSync(fd);
76+
}
77+
} catch (e) {
78+
done(e);
3779
}
38-
done(Object.assign(new Error(`git exited with error code ${code}`), { code, signal }));
39-
} else {
40-
done(null);
4180
}
42-
});
81+
}
4382
}

‎test/tests/commit.js

+39
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { expect } from 'chai';
44
import os from 'os';
5+
import fs from 'fs';
56
import path from 'path';
67

78
import inquirer from 'inquirer';
@@ -274,6 +275,44 @@ ${(os.platform === 'win32') ? '' : ' '}
274275

275276
});
276277

278+
it('should save directly to .git/COMMIT_EDITMSG with --hook option', function (done) {
279+
280+
this.timeout(config.maxTimeout);
281+
282+
// SETUP
283+
let dummyCommitMessage = `doggies!`;
284+
285+
let repoConfig = {
286+
path: config.paths.endUserRepo,
287+
files: {
288+
dummyfile: {
289+
contents: 'arf arf!',
290+
filename: 'woof.txt'
291+
}
292+
}
293+
};
294+
295+
// Describe an adapter
296+
let adapterConfig = {
297+
path: path.join(repoConfig.path, '/node_modules/cz-jira-smart-commit'),
298+
npmName: 'cz-jira-smart-commit'
299+
}
300+
301+
// Quick setup the repos, adapter, and grab a simple prompter
302+
let prompter = quickPrompterSetup(sh, repoConfig, adapterConfig, dummyCommitMessage);
303+
// TEST
304+
305+
// This is a successful commit directly to .git/COMMIT_EDITMSG
306+
commitizenCommit(sh, inquirer, repoConfig.path, prompter, { disableAppendPaths: true, quiet: true, emitData: true, hookMode: true }, function (err) {
307+
const commitFilePath = path.join(repoConfig.path, '.git/COMMIT_EDITMSG')
308+
const commitFile = fs.openSync(commitFilePath, 'r+')
309+
let commitContents = fs.readFileSync(commitFile, { flags: 'r+' }).toString();
310+
fs.closeSync(commitFile);
311+
expect(commitContents).to.have.string(dummyCommitMessage);
312+
expect(err).to.be.a('null');
313+
done();
314+
});
315+
});
277316
});
278317

279318
afterEach(function () {

‎test/tests/parsers.js

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,45 @@
11
/* eslint-env mocha */
22

33
import { expect } from 'chai';
4-
import { parse } from '../../src/cli/parsers/git-cz';
4+
import { gitCz as gitCzParser, commitizen as commitizenParser } from '../../src/cli/parsers';
55

66
describe('parsers', () => {
77
describe('git-cz', () => {
88
it('should parse --message "Hello, World!"', () => {
9-
expect(parse(['--amend', '--message', 'Hello, World!'])).to.deep.equal(['--amend']);
9+
expect(gitCzParser.parse(['--amend', '--message', 'Hello, World!'])).to.deep.equal(['--amend']);
1010
});
1111

1212
it('should parse --message="Hello, World!"', () => {
13-
expect(parse(['--amend', '--message=Hello, World!'])).to.deep.equal(['--amend']);
13+
expect(gitCzParser.parse(['--amend', '--message=Hello, World!'])).to.deep.equal(['--amend']);
1414
});
1515

1616
it('should parse -amwip', () => {
17-
expect(parse(['-amwip'])).to.deep.equal(['-a']);
17+
expect(gitCzParser.parse(['-amwip'])).to.deep.equal(['-a']);
1818
});
1919

2020
it('should parse -am=wip', () => {
21-
expect(parse(['-am=wip'])).to.deep.equal(['-a']);
21+
expect(gitCzParser.parse(['-am=wip'])).to.deep.equal(['-a']);
2222
});
2323

2424
it('should parse -am wip', () => {
25-
expect(parse(['-am', 'wip'])).to.deep.equal(['-a']);
25+
expect(gitCzParser.parse(['-am', 'wip'])).to.deep.equal(['-a']);
2626
});
2727

2828
it('should parse -a -m wip -n', () => {
29-
expect(parse(['-a', '-m', 'wip', '-n'])).to.deep.equal(['-a', '-n']);
29+
expect(gitCzParser.parse(['-a', '-m', 'wip', '-n'])).to.deep.equal(['-a', '-n']);
3030
});
3131

3232
it('should parse -a -m=wip -n', () => {
33-
expect(parse(['-a', '-m=wip', '-n'])).to.deep.equal(['-a', '-n']);
33+
expect(gitCzParser.parse(['-a', '-m=wip', '-n'])).to.deep.equal(['-a', '-n']);
34+
});
35+
});
36+
37+
describe('commitizen', () => {
38+
it('should parse out the --amend option', () => {
39+
expect(commitizenParser.parse(['--amend'])).to.deep.equal({ _: [], amend: true })
40+
});
41+
it('should parse out the --hook option', () => {
42+
expect(commitizenParser.parse(['--hook'])).to.deep.equal({ _: [], hook: true })
3443
});
3544
});
3645
});

0 commit comments

Comments
 (0)
Please sign in to comment.