├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── hook ├── index.js ├── install.js ├── package.json ├── test.js └── uninstall.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | coverage 4 | .tern-port 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | - "0.10" 6 | - "4" 7 | - "iojs" 8 | script: 9 | - "npm run test-travis" 10 | after_script: 11 | - "npm install coveralls@2.11.x && cat coverage/lcov.info | coveralls" 12 | matrix: 13 | fast_finish: true 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 2 | - Check `/usr/local/bin/node` if we cannot find the binaries in the PATH. 3 | 4 | ## 1.0.1 5 | - Corrected the `hook` file so it doesn't attempt to run **your** index.js but 6 | **ours** instead. 7 | 8 | ## 1.0 9 | - Create symlinks instead of a copying the hook file so we can depend on 10 | modules. 11 | - More readable output messages. 12 | - Lookup git and npm using `which`. 13 | - Allow nodejs, node and iojs to call the the hook. 14 | - Refactored the way options can be passed in to pre-commit, we're now allowing 15 | objects. 16 | - The refactor made it possible to test most of the internals so we now have 17 | 90%+ coverage. 18 | - And the list goes on. 19 | 20 | ## 0.0.9 21 | - Added missing uninstall hook to remove and restore old scripts. 22 | 23 | ## 0.0.8 24 | - Added support for installing custom commit templates using `pre-commit.commit-template` 25 | 26 | ## 0.0.7 27 | - Fixes regression introduced in 0.0.6 28 | 29 | ## 0.0.6 30 | - Also silence `npm` output when the silent flag has been given. 31 | 32 | ## 0.0.5 33 | - Allow silencing of the pre-commit output by setting a `precommit.silent: true` 34 | in your `package.json` 35 | 36 | ## 0.0.4 37 | - Added a better error message when you fucked up your `package.json`. 38 | - Only run tests if there are changes. 39 | - Improved output formatting. 40 | 41 | ## 0.0.3 42 | - Added compatibility for Node.js 0.6 by falling back to path.existsSync. 43 | 44 | ## 0.0.2 45 | - Fixed a typo in the output, see #1. 46 | 47 | ## 0.0.1 48 | - Use `spawn` instead of `exec` and give custom file descriptors. This way we 49 | can output color and have more control over the process. 50 | 51 | ## 0.0.0 52 | - Initial release. 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman, the Contributors. 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pre-commit 2 | 3 | [![Version npm][version]](http://browsenpm.org/package/pre-commit)[![Build Status][build]](https://travis-ci.org/observing/pre-commit)[![Dependencies][david]](https://david-dm.org/observing/pre-commit)[![Coverage Status][cover]](https://coveralls.io/r/observing/pre-commit?branch=master) 4 | 5 | [version]: http://img.shields.io/npm/v/pre-commit.svg?style=flat-square 6 | [build]: http://img.shields.io/travis/observing/pre-commit/master.svg?style=flat-square 7 | [david]: https://img.shields.io/david/observing/pre-commit.svg?style=flat-square 8 | [cover]: http://img.shields.io/coveralls/observing/pre-commit/master.svg?style=flat-square 9 | 10 | **pre-commit** is a pre-commit hook installer for `git`. It will ensure that 11 | your `npm test` (or other specified scripts) passes before you can commit your 12 | changes. This all conveniently configured in your `package.json`. 13 | 14 | But don't worry, you can still force a commit by telling `git` to skip the 15 | `pre-commit` hooks by simply committing using `--no-verify`. 16 | 17 | ### Installation 18 | 19 | It's advised to install the **pre-commit** module as a `devDependencies` in your 20 | `package.json` as you only need this for development purposes. To install the 21 | module simply run: 22 | 23 | ``` 24 | npm install --save-dev pre-commit 25 | ``` 26 | 27 | To install it as `devDependency`. When this module is installed it will override 28 | the existing `pre-commit` file in your `.git/hooks` folder. Existing 29 | `pre-commit` hooks will be backed up as `pre-commit.old` in the same repository. 30 | 31 | ### Configuration 32 | 33 | `pre-commit` will try to run your `npm test` command in the root of the git 34 | repository by default unless it's the default value that is set by the `npm 35 | init` script. 36 | 37 | But `pre-commit` is not limited to just running your `npm test`'s during the 38 | commit hook. It's also capable of running every other script that you've 39 | specified in your `package.json` "scripts" field. So before people commit you 40 | could ensure that: 41 | 42 | - You have 100% coverage 43 | - All styling passes. 44 | - JSHint passes. 45 | - Contribution licenses signed etc. 46 | 47 | The only thing you need to do is add a `pre-commit` array to your `package.json` 48 | that specifies which scripts you want to have ran and in which order: 49 | 50 | ```js 51 | { 52 | "name": "437464d0899504fb6b7b", 53 | "version": "0.0.0", 54 | "description": "ERROR: No README.md file found!", 55 | "main": "index.js", 56 | "scripts": { 57 | "test": "echo \"Error: I SHOULD FAIL LOLOLOLOLOL \" && exit 1", 58 | "foo": "echo \"fooo\" && exit 0", 59 | "bar": "echo \"bar\" && exit 0" 60 | }, 61 | "pre-commit": [ 62 | "foo", 63 | "bar", 64 | "test" 65 | ] 66 | } 67 | ``` 68 | 69 | In the example above, it will first run: `npm run foo` then `npm run bar` and 70 | finally `npm run test` which will make the commit fail as it returns the error 71 | code `1`. If you prefer strings over arrays or `precommit` without a middle 72 | dash, that also works: 73 | 74 | ```js 75 | { 76 | "precommit": "foo, bar, test" 77 | "pre-commit": "foo, bar, test" 78 | "pre-commit": ["foo", "bar", "test"] 79 | "precommit": ["foo", "bar", "test"], 80 | "precommit": { 81 | "run": "foo, bar, test", 82 | }, 83 | "pre-commit": { 84 | "run": ["foo", "bar", "test"], 85 | }, 86 | "precommit": { 87 | "run": ["foo", "bar", "test"], 88 | }, 89 | "pre-commit": { 90 | "run": "foo, bar, test", 91 | } 92 | } 93 | ``` 94 | 95 | The examples above are all the same. In addition to configuring which scripts 96 | should be ran you can also configure the following options: 97 | 98 | - **silent** Don't output the prefixed `pre-commit:` messages when things fail 99 | or when we have nothing to run. Should be a boolean. 100 | - **colors** Don't output colors when we write messages. Should be a boolean. 101 | - **template** Path to a file who's content should be used as template for the 102 | git commit body. 103 | 104 | These options can either be added in the `pre-commit`/`precommit` object as keys 105 | or as `"pre-commit.{key}` key properties in the `package.json`: 106 | 107 | ```js 108 | { 109 | "precommit.silent": true, 110 | "pre-commit": { 111 | "silent": true 112 | } 113 | } 114 | ``` 115 | 116 | It's all the same. Different styles so use what matches your project. To learn 117 | more about the scripts, please read the official `npm` documentation: 118 | 119 | https://docs.npmjs.com/misc/scripts 120 | 121 | And to learn more about git hooks read: 122 | 123 | http://githooks.com 124 | 125 | ### License 126 | 127 | MIT 128 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "0.10" 4 | - nodejs_version: "0.12" 5 | - nodejs_version: "1.0" 6 | 7 | matrix: 8 | fast_finish: true 9 | allow_failures: 10 | - platform: x86 11 | - platform: x64 12 | 13 | platform: 14 | - x86 15 | - x64 16 | 17 | install: 18 | - ps: Install-Product node $env:nodejs_version $env:platform 19 | - npm install -g npm@1.4.x 20 | - npm install 21 | 22 | test_script: 23 | - node --version 24 | - npm --version 25 | - npm test 26 | 27 | build: off 28 | -------------------------------------------------------------------------------- /hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | HAS_NODE=`which node 2> /dev/null || which nodejs 2> /dev/null || which iojs 2> /dev/null` 4 | 5 | # 6 | # There are some issues with Source Tree because paths are not set correctly for 7 | # the given environment. Sourcing the bash_profile seems to resolve this for bash users, 8 | # sourcing the zshrc for zshell users. 9 | # 10 | # https://answers.atlassian.com/questions/140339/sourcetree-hook-failing-because-paths-don-t-seem-to-be-set-correctly 11 | # 12 | function source_home_file { 13 | file="$HOME/$1" 14 | [[ -f "${file}" ]] && source "${file}" 15 | } 16 | 17 | if [[ -z "$HAS_NODE" ]]; then 18 | source_home_file ".bash_profile" || source_home_file ".zshrc" || source_home_file ".bashrc" || true 19 | fi 20 | 21 | NODE=`which node 2> /dev/null` 22 | NODEJS=`which nodejs 2> /dev/null` 23 | IOJS=`which iojs 2> /dev/null` 24 | LOCAL="/usr/local/bin/node" 25 | BINARY= 26 | 27 | # 28 | # Figure out which binary we need to use for our script execution. 29 | # 30 | if [[ -n "$NODE" ]]; then 31 | BINARY="$NODE" 32 | elif [[ -n "$NODEJS" ]]; then 33 | BINARY="$NODEJS" 34 | elif [[ -n "$IOJS" ]]; then 35 | BINARY="$IOJS" 36 | elif [[ -x "$LOCAL" ]]; then 37 | BINARY="$LOCAL" 38 | fi 39 | 40 | # 41 | # Add --dry-run cli flag support so we can execute this hook without side effects 42 | # and see if it works in the current environment 43 | # 44 | if [[ $* == *--dry-run* ]]; then 45 | if [[ -z "$BINARY" ]]; then 46 | exit 1 47 | fi 48 | else 49 | "$BINARY" "$("$BINARY" -e "console.log(require.resolve('pre-commit'))")" 50 | fi 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var spawn = require('cross-spawn') 4 | , which = require('which') 5 | , path = require('path') 6 | , util = require('util') 7 | , tty = require('tty'); 8 | 9 | /** 10 | * Representation of a hook runner. 11 | * 12 | * @constructor 13 | * @param {Function} fn Function to be called when we want to exit 14 | * @param {Object} options Optional configuration, primarily used for testing. 15 | * @api public 16 | */ 17 | function Hook(fn, options) { 18 | if (!this) return new Hook(fn, options); 19 | options = options || {}; 20 | 21 | this.options = options; // Used for testing only. Ignore this. Don't touch. 22 | this.config = {}; // pre-commit configuration from the `package.json`. 23 | this.json = {}; // Actual content of the `package.json`. 24 | this.npm = ''; // The location of the `npm` binary. 25 | this.git = ''; // The location of the `git` binary. 26 | this.root = ''; // The root location of the .git folder. 27 | this.status = ''; // Contents of the `git status`. 28 | this.exit = fn; // Exit function. 29 | 30 | this.initialize(); 31 | } 32 | 33 | /** 34 | * Boolean indicating if we're allowed to output progress information into the 35 | * terminal. 36 | * 37 | * @type {Boolean} 38 | * @public 39 | */ 40 | Object.defineProperty(Hook.prototype, 'silent', { 41 | get: function silent() { 42 | return !!this.config.silent; 43 | } 44 | }); 45 | 46 | /** 47 | * Boolean indicating if we're allowed and capable of outputting colors into the 48 | * terminal. 49 | * 50 | * @type {Boolean} 51 | * @public 52 | */ 53 | Object.defineProperty(Hook.prototype, 'colors', { 54 | get: function colors() { 55 | return this.config.colors !== false && tty.isatty(process.stdout.fd); 56 | } 57 | }); 58 | 59 | /** 60 | * Execute a binary. 61 | * 62 | * @param {String} bin Binary that needs to be executed 63 | * @param {Array} args Arguments for the binary 64 | * @returns {Object} 65 | * @api private 66 | */ 67 | Hook.prototype.exec = function exec(bin, args) { 68 | return spawn.sync(bin, args, { 69 | stdio: 'pipe' 70 | }); 71 | }; 72 | 73 | /** 74 | * Parse the package.json so we can create an normalize it's contents to 75 | * a usable configuration structure. 76 | * 77 | * @api private 78 | */ 79 | Hook.prototype.parse = function parse() { 80 | var pre = this.json['pre-commit'] || this.json.precommit 81 | , config = !Array.isArray(pre) && 'object' === typeof pre ? pre : {}; 82 | 83 | ['silent', 'colors', 'template'].forEach(function each(flag) { 84 | var value; 85 | 86 | if (flag in config) value = config[flag]; 87 | else if ('precommit.'+ flag in this.json) value = this.json['precommit.'+ flag]; 88 | else if ('pre-commit.'+ flag in this.json) value = this.json['pre-commit.'+ flag]; 89 | else return; 90 | 91 | config[flag] = value; 92 | }, this); 93 | 94 | // 95 | // The scripts we need to run can be set under the `run` property. 96 | // 97 | config.run = config.run || pre; 98 | 99 | if ('string' === typeof config.run) config.run = config.run.split(/[, ]+/); 100 | if ( 101 | !Array.isArray(config.run) 102 | && this.json.scripts 103 | && this.json.scripts.test 104 | && this.json.scripts.test !== 'echo "Error: no test specified" && exit 1' 105 | ) { 106 | config.run = ['test']; 107 | } 108 | 109 | this.config = config; 110 | }; 111 | 112 | /** 113 | * Write messages to the terminal, for feedback purposes. 114 | * 115 | * @param {Array} lines The messages that need to be written. 116 | * @param {Number} exit Exit code for the process.exit. 117 | * @api public 118 | */ 119 | Hook.prototype.log = function log(lines, exit) { 120 | if (!Array.isArray(lines)) lines = lines.split('\n'); 121 | if ('number' !== typeof exit) exit = 1; 122 | 123 | var prefix = this.colors 124 | ? '\u001b[38;5;166mpre-commit:\u001b[39;49m ' 125 | : 'pre-commit: '; 126 | 127 | lines.push(''); // Whitespace at the end of the log. 128 | lines.unshift(''); // Whitespace at the beginning. 129 | 130 | lines = lines.map(function map(line) { 131 | return prefix + line; 132 | }); 133 | 134 | if (!this.silent) lines.forEach(function output(line) { 135 | if (exit) console.error(line); 136 | else console.log(line); 137 | }); 138 | 139 | this.exit(exit, lines); 140 | return exit === 0; 141 | }; 142 | 143 | /** 144 | * Initialize all the values of the constructor to see if we can run as an 145 | * pre-commit hook. 146 | * 147 | * @api private 148 | */ 149 | Hook.prototype.initialize = function initialize() { 150 | ['git', 'npm'].forEach(function each(binary) { 151 | try { this[binary] = which.sync(binary); } 152 | catch (e) {} 153 | }, this); 154 | 155 | // 156 | // in GUI clients node and npm are not in the PATH so get node binary PATH, 157 | // add it to the PATH list and try again. 158 | // 159 | if (!this.npm) { 160 | try { 161 | process.env.PATH += path.delimiter + path.dirname(process.env._); 162 | this.npm = which.sync('npm'); 163 | } catch (e) { 164 | return this.log(this.format(Hook.log.binary, 'npm'), 0); 165 | } 166 | } 167 | 168 | // 169 | // Also bail out if we cannot find the git binary. 170 | // 171 | if (!this.git) return this.log(this.format(Hook.log.binary, 'git'), 0); 172 | 173 | this.root = this.exec(this.git, ['rev-parse', '--show-toplevel']); 174 | this.status = this.exec(this.git, ['status', '--porcelain']); 175 | 176 | if (this.status.code) return this.log(Hook.log.status, 0); 177 | if (this.root.code) return this.log(Hook.log.root, 0); 178 | 179 | this.status = this.status.stdout.toString().trim(); 180 | this.root = this.root.stdout.toString().trim(); 181 | 182 | try { 183 | this.json = require(path.join(this.root, 'package.json')); 184 | this.parse(); 185 | } catch (e) { return this.log(this.format(Hook.log.json, e.message), 0); } 186 | 187 | // 188 | // We can only check for changes after we've parsed the package.json as it 189 | // contains information if we need to suppress the empty message or not. 190 | // 191 | if (!this.status.length && !this.options.ignorestatus) { 192 | return this.log(Hook.log.empty, 0); 193 | } 194 | 195 | // 196 | // If we have a git template we should configure it before checking for 197 | // scripts so it will still be applied even if we don't have anything to 198 | // execute. 199 | // 200 | if (this.config.template) { 201 | this.exec(this.git, ['config', 'commit.template', this.config.template]); 202 | } 203 | 204 | if (!this.config.run) return this.log(Hook.log.run, 0); 205 | }; 206 | 207 | /** 208 | * Run the specified hooks. 209 | * 210 | * @api public 211 | */ 212 | Hook.prototype.run = function runner() { 213 | var hooked = this; 214 | 215 | (function again(scripts) { 216 | if (!scripts.length) return hooked.exit(0); 217 | 218 | var script = scripts.shift(); 219 | 220 | // 221 | // There's a reason on why we're using an async `spawn` here instead of the 222 | // `shelljs.exec`. The sync `exec` is a hack that writes writes a file to 223 | // disk and they poll with sync fs calls to see for results. The problem is 224 | // that the way they capture the output which us using input redirection and 225 | // this doesn't have the required `isAtty` information that libraries use to 226 | // output colors resulting in script output that doesn't have any color. 227 | // 228 | spawn(hooked.npm, ['run', script, '--silent'], { 229 | env: process.env, 230 | cwd: hooked.root, 231 | stdio: [0, 1, 2] 232 | }).once('close', function closed(code) { 233 | if (code) return hooked.log(hooked.format(Hook.log.failure, script, code)); 234 | 235 | again(scripts); 236 | }); 237 | })(hooked.config.run.slice(0)); 238 | }; 239 | 240 | /** 241 | * Expose some of our internal tools so plugins can also re-use them for their 242 | * own processing. 243 | * 244 | * @type {Function} 245 | * @public 246 | */ 247 | Hook.prototype.format = util.format; 248 | 249 | /** 250 | * The various of error and status messages that we can output. 251 | * 252 | * @type {Object} 253 | * @private 254 | */ 255 | Hook.log = { 256 | binary: [ 257 | 'Failed to locate the `%s` binary, make sure it\'s installed in your $PATH.', 258 | 'Skipping the pre-commit hook.' 259 | ].join('\n'), 260 | 261 | status: [ 262 | 'Failed to retrieve the `git status` from the project.', 263 | 'Skipping the pre-commit hook.' 264 | ].join('\n'), 265 | 266 | root: [ 267 | 'Failed to find the root of this git repository, cannot locate the `package.json`.', 268 | 'Skipping the pre-commit hook.' 269 | ].join('\n'), 270 | 271 | empty: [ 272 | 'No changes detected.', 273 | 'Skipping the pre-commit hook.' 274 | ].join('\n'), 275 | 276 | json: [ 277 | 'Received an error while parsing or locating the `package.json` file:', 278 | '', 279 | ' %s', 280 | '', 281 | 'Skipping the pre-commit hook.' 282 | ].join('\n'), 283 | 284 | run: [ 285 | 'We have nothing pre-commit hooks to run. Either you\'re missing the `scripts`', 286 | 'in your `package.json` or have configured pre-commit to run nothing.', 287 | 'Skipping the pre-commit hook.' 288 | ].join('\n'), 289 | 290 | failure: [ 291 | 'We\'ve failed to pass the specified git pre-commit hooks as the `%s`', 292 | 'hook returned an exit code (%d). If you\'re feeling adventurous you can', 293 | 'skip the git pre-commit hooks by adding the following flags to your commit:', 294 | '', 295 | ' git commit -n (or --no-verify)', 296 | '', 297 | 'This is ill-advised since the commit is broken.' 298 | ].join('\n') 299 | }; 300 | 301 | // 302 | // Expose the Hook instance so we can use it for testing purposes. 303 | // 304 | module.exports = Hook; 305 | 306 | // 307 | // Run directly if we're required executed directly through the CLI 308 | // 309 | if (module !== require.main) return; 310 | 311 | var hook = new Hook(function cli(code) { 312 | process.exit(code); 313 | }); 314 | 315 | hook.run(); 316 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 4 | // Compatibility with older node.js as path.exists got moved to `fs`. 5 | // 6 | var fs = require('fs') 7 | , path = require('path') 8 | , os = require('os') 9 | , hook = path.join(__dirname, 'hook') 10 | , root = path.resolve(__dirname, '..', '..') 11 | , exists = fs.existsSync || path.existsSync; 12 | 13 | // 14 | // Gather the location of the possible hidden .git directory, the hooks 15 | // directory which contains all git hooks and the absolute location of the 16 | // `pre-commit` file. The path needs to be absolute in order for the symlinking 17 | // to work correctly. 18 | // 19 | 20 | var git = getGitFolderPath(root); 21 | 22 | // Function to recursively finding .git folder 23 | function getGitFolderPath(currentPath) { 24 | var git = path.resolve(currentPath, '.git') 25 | 26 | if (!exists(git) || !fs.lstatSync(git).isDirectory()) { 27 | console.log('pre-commit:'); 28 | console.log('pre-commit: Not found .git folder in', git); 29 | 30 | var newPath = path.resolve(currentPath, '..'); 31 | 32 | // Stop if we on top folder 33 | if (currentPath === newPath) { 34 | return null; 35 | } 36 | 37 | return getGitFolderPath(newPath); 38 | } 39 | 40 | console.log('pre-commit:'); 41 | console.log('pre-commit: Found .git folder in', git); 42 | return git; 43 | } 44 | 45 | // 46 | // Resolve git directory for submodules 47 | // 48 | if (exists(git) && fs.lstatSync(git).isFile()) { 49 | var gitinfo = fs.readFileSync(git).toString() 50 | , gitdirmatch = /gitdir: (.+)/.exec(gitinfo) 51 | , gitdir = gitdirmatch.length == 2 ? gitdirmatch[1] : null; 52 | 53 | if (gitdir !== null) { 54 | git = path.resolve(root, gitdir); 55 | hooks = path.resolve(git, 'hooks'); 56 | precommit = path.resolve(hooks, 'pre-commit'); 57 | } 58 | } 59 | 60 | // 61 | // Bail out if we don't have an `.git` directory as the hooks will not get 62 | // triggered. If we do have directory create a hooks folder if it doesn't exist. 63 | // 64 | if (!git) { 65 | console.log('pre-commit:'); 66 | console.log('pre-commit: Not found any .git folder for installing pre-commit hook'); 67 | return; 68 | } 69 | 70 | var hooks = path.resolve(git, 'hooks') 71 | , precommit = path.resolve(hooks, 'pre-commit'); 72 | 73 | if (!exists(hooks)) fs.mkdirSync(hooks); 74 | 75 | // 76 | // If there's an existing `pre-commit` hook we want to back it up instead of 77 | // overriding it and losing it completely as it might contain something 78 | // important. 79 | // 80 | if (exists(precommit) && !fs.lstatSync(precommit).isSymbolicLink()) { 81 | console.log('pre-commit:'); 82 | console.log('pre-commit: Detected an existing git pre-commit hook'); 83 | fs.writeFileSync(precommit +'.old', fs.readFileSync(precommit)); 84 | console.log('pre-commit: Old pre-commit hook backuped to pre-commit.old'); 85 | console.log('pre-commit:'); 86 | } 87 | 88 | // 89 | // We cannot create a symlink over an existing file so make sure it's gone and 90 | // finish the installation process. 91 | // 92 | try { fs.unlinkSync(precommit); } 93 | catch (e) {} 94 | 95 | // Create generic precommit hook that launches this modules hook (as well 96 | // as stashing - unstashing the unstaged changes) 97 | // TODO: we could keep launching the old pre-commit scripts 98 | var hookRelativeUnixPath = hook.replace(root, '.'); 99 | 100 | if(os.platform() === 'win32') { 101 | hookRelativeUnixPath = hookRelativeUnixPath.replace(/[\\\/]+/g, '/'); 102 | } 103 | 104 | var precommitContent = '#!/usr/bin/env bash' + os.EOL 105 | + hookRelativeUnixPath + os.EOL 106 | + 'RESULT=$?' + os.EOL 107 | + '[ $RESULT -ne 0 ] && exit 1' + os.EOL 108 | + 'exit 0' + os.EOL; 109 | 110 | // 111 | // It could be that we do not have rights to this folder which could cause the 112 | // installation of this module to completely fail. We should just output the 113 | // error instead destroying the whole npm install process. 114 | // 115 | try { fs.writeFileSync(precommit, precommitContent); } 116 | catch (e) { 117 | console.error('pre-commit:'); 118 | console.error('pre-commit: Failed to create the hook file in your .git/hooks folder because:'); 119 | console.error('pre-commit: '+ e.message); 120 | console.error('pre-commit: The hook was not installed.'); 121 | console.error('pre-commit:'); 122 | } 123 | 124 | try { fs.chmodSync(precommit, '777'); } 125 | catch (e) { 126 | console.error('pre-commit:'); 127 | console.error('pre-commit: chmod 0777 the pre-commit file in your .git/hooks folder because:'); 128 | console.error('pre-commit: '+ e.message); 129 | console.error('pre-commit:'); 130 | } 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pre-commit", 3 | "version": "1.2.2", 4 | "description": "Automatically install pre-commit hooks for your npm modules.", 5 | "main": "index.js", 6 | "scripts": { 7 | "coverage": "istanbul cover ./node_modules/.bin/_mocha -- test.js", 8 | "example-fail": "echo \"This is the example hook, I exit with 1\" && exit 1", 9 | "example-pass": "echo \"This is the example hook, I exit with 0\" && exit 0", 10 | "install": "node install.js", 11 | "test": "mocha test.js", 12 | "test-travis": "istanbul cover node_modules/.bin/_mocha --report lcovonly -- test.js", 13 | "uninstall": "node uninstall.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/observing/pre-commit.git" 18 | }, 19 | "keywords": [ 20 | "git", 21 | "hooks", 22 | "npm", 23 | "pre-commit", 24 | "precommit", 25 | "run", 26 | "test", 27 | "development" 28 | ], 29 | "author": "Arnout Kazemier ", 30 | "homepage": "https://github.com/observing/pre-commit", 31 | "license": "MIT", 32 | "dependencies": { 33 | "cross-spawn": "^5.0.1", 34 | "spawn-sync": "^1.0.15", 35 | "which": "1.2.x" 36 | }, 37 | "devDependencies": { 38 | "assume": "~1.5.0", 39 | "istanbul": "0.4.x", 40 | "mocha": "~3.3.0", 41 | "pre-commit": "git://github.com/observing/pre-commit.git" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | describe('pre-commit', function () { 3 | 'use strict'; 4 | 5 | var assume = require('assume') 6 | , Hook = require('./'); 7 | 8 | it('is exported as a function', function () { 9 | assume(Hook).is.a('function'); 10 | }); 11 | 12 | it('can be initialized without a `new` keyword', function () { 13 | var hook = Hook(function () {}, { 14 | ignorestatus: true 15 | }); 16 | 17 | assume(hook).is.instanceOf(Hook); 18 | assume(hook.parse).is.a('function'); 19 | }); 20 | 21 | describe('#parse', function () { 22 | var hook; 23 | 24 | beforeEach(function () { 25 | hook = new Hook(function () {}, { 26 | ignorestatus: true 27 | }); 28 | }); 29 | 30 | it('extracts configuration values from precommit.', function () { 31 | hook.json = { 32 | 'precommit.silent': true 33 | }; 34 | 35 | assume(hook.silent).is.false(); 36 | 37 | hook.parse(); 38 | 39 | assume(hook.config.silent).is.true(); 40 | assume(hook.silent).is.true(); 41 | }); 42 | 43 | it('extracts configuration values from pre-commit.', function () { 44 | hook.json = { 45 | 'pre-commit.silent': true, 46 | 'pre-commit.colors': false 47 | }; 48 | 49 | assume(hook.silent).is.false(); 50 | assume(hook.colors).is.true(); 51 | 52 | hook.parse(); 53 | 54 | assume(hook.config.silent).is.true(); 55 | assume(hook.silent).is.true(); 56 | assume(hook.colors).is.false(); 57 | }); 58 | 59 | it('normalizes the `pre-commit` to an array', function () { 60 | hook.json = { 61 | 'pre-commit': 'test, cows, moo' 62 | }; 63 | 64 | hook.parse(); 65 | 66 | assume(hook.config.run).is.length(3); 67 | assume(hook.config.run).contains('test'); 68 | assume(hook.config.run).contains('cows'); 69 | assume(hook.config.run).contains('moo'); 70 | }); 71 | 72 | it('normalizes the `precommit` to an array', function () { 73 | hook.json = { 74 | 'precommit': 'test, cows, moo' 75 | }; 76 | 77 | hook.parse(); 78 | 79 | assume(hook.config.run).is.length(3); 80 | assume(hook.config.run).contains('test'); 81 | assume(hook.config.run).contains('cows'); 82 | assume(hook.config.run).contains('moo'); 83 | }); 84 | 85 | it('allows `pre-commit` object based syntax', function () { 86 | hook.json = { 87 | 'pre-commit': { 88 | run: 'test scripts go here', 89 | silent: true, 90 | colors: false 91 | } 92 | }; 93 | 94 | hook.parse(); 95 | 96 | assume(hook.config.run).is.length(4); 97 | assume(hook.config.run).contains('test'); 98 | assume(hook.config.run).contains('scripts'); 99 | assume(hook.config.run).contains('go'); 100 | assume(hook.config.run).contains('here'); 101 | assume(hook.silent).is.true(); 102 | assume(hook.colors).is.false(); 103 | }); 104 | 105 | it('defaults to `test` if nothing is specified', function () { 106 | hook.json = { 107 | scripts: { 108 | test: 'mocha test.js' 109 | } 110 | }; 111 | 112 | hook.parse(); 113 | assume(hook.config.run).deep.equals(['test']); 114 | }); 115 | 116 | it('ignores the default npm.script.test placeholder', function () { 117 | hook.json = { 118 | scripts: { 119 | test: 'echo "Error: no test specified" && exit 1' 120 | } 121 | }; 122 | 123 | hook.parse(); 124 | assume(hook.config.run).has.length(0); 125 | }); 126 | }); 127 | 128 | describe('#log', function () { 129 | it('prefixes the logs with `pre-commit`', function (next) { 130 | var hook = new Hook(function (code, lines) { 131 | assume(code).equals(1); 132 | assume(lines).is.a('array'); 133 | 134 | assume(lines[0]).includes('pre-commit'); 135 | assume(lines[1]).includes('pre-commit'); 136 | assume(lines[1]).includes('foo'); 137 | assume(lines).has.length(3); 138 | 139 | // color prefix check 140 | lines.forEach(function (line) { 141 | assume(line).contains('\u001b'); 142 | }); 143 | 144 | next(); 145 | }, { ignorestatus: true }); 146 | 147 | hook.config.silent = true; 148 | hook.log(['foo']); 149 | }); 150 | 151 | it('allows for a custom error code', function (next) { 152 | var hook = new Hook(function (code, lines) { 153 | assume(code).equals(0); 154 | 155 | next(); 156 | }, { ignorestatus: true }); 157 | 158 | hook.config.silent = true; 159 | hook.log(['foo'], 0); 160 | }); 161 | 162 | it('allows strings to be split \\n', function (next) { 163 | var hook = new Hook(function (code, lines) { 164 | assume(code).equals(0); 165 | 166 | assume(lines).has.length(4); 167 | assume(lines[1]).contains('foo'); 168 | assume(lines[2]).contains('bar'); 169 | 170 | next(); 171 | }, { ignorestatus: true }); 172 | 173 | hook.config.silent = true; 174 | hook.log('foo\nbar', 0); 175 | }); 176 | 177 | it('does not output colors when configured to do so', function (next) { 178 | var hook = new Hook(function (code, lines) { 179 | assume(code).equals(0); 180 | 181 | lines.forEach(function (line) { 182 | assume(line).does.not.contain('\u001b'); 183 | }); 184 | 185 | next(); 186 | }, { ignorestatus: true }); 187 | 188 | hook.config.silent = true; 189 | hook.config.colors = false; 190 | 191 | hook.log('foo\nbar', 0); 192 | }); 193 | 194 | it('output lines to stderr if error code 1', function (next) { 195 | var err = console.error; 196 | next = assume.plan(4, next); 197 | 198 | var hook = new Hook(function (code, lines) { 199 | console.error = err; 200 | next(); 201 | }, { ignorestatus: true }); 202 | 203 | console.error = function (line) { 204 | assume(line).contains('pre-commit: '); 205 | }; 206 | 207 | hook.config.colors = false; 208 | hook.log('foo\nbar', 1); 209 | }); 210 | 211 | it('output lines to stdout if error code 0', function (next) { 212 | var log = console.log; 213 | next = assume.plan(4, next); 214 | 215 | var hook = new Hook(function (code, lines) { 216 | console.log = log; 217 | next(); 218 | }, { ignorestatus: true }); 219 | 220 | console.log = function (line) { 221 | assume(line).contains('pre-commit: '); 222 | }; 223 | 224 | hook.config.colors = false; 225 | hook.log('foo\nbar', 0); 226 | }); 227 | }); 228 | 229 | describe('#run', function () { 230 | it('runs the specified scripts and exit with 0 on no error', function (next) { 231 | var hook = new Hook(function (code, lines) { 232 | assume(code).equals(0); 233 | assume(lines).is.undefined(); 234 | 235 | next(); 236 | }, { ignorestatus: true }); 237 | 238 | hook.config.run = ['example-pass']; 239 | hook.run(); 240 | }); 241 | 242 | it('runs the specified test and exits with 1 on error', function (next) { 243 | var hook = new Hook(function (code, lines) { 244 | assume(code).equals(1); 245 | 246 | assume(lines).is.a('array'); 247 | assume(lines[1]).contains('`example-fail`'); 248 | assume(lines[2]).contains('code (1)'); 249 | 250 | next(); 251 | }, { ignorestatus: true }); 252 | 253 | hook.config.run = ['example-fail']; 254 | hook.run(); 255 | }); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /uninstall.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs') 4 | , path = require('path') 5 | , exists = fs.existsSync || path.existsSync 6 | , root = path.resolve(__dirname, '..', '..') 7 | , git = path.resolve(root, '.git'); 8 | 9 | // 10 | // Resolve git directory for submodules 11 | // 12 | if (exists(git) && fs.lstatSync(git).isFile()) { 13 | var gitinfo = fs.readFileSync(git).toString() 14 | , gitdirmatch = /gitdir: (.+)/.exec(gitinfo) 15 | , gitdir = gitdirmatch.length == 2 ? gitdirmatch[1] : null; 16 | 17 | if (gitdir !== null) { 18 | git = path.resolve(root, gitdir); 19 | } 20 | } 21 | 22 | // 23 | // Location of pre-commit hook, if it exists 24 | // 25 | var precommit = path.resolve(git, 'hooks', 'pre-commit'); 26 | 27 | // 28 | // Bail out if we don't have pre-commit file, it might be removed manually. 29 | // 30 | if (!exists(precommit)) return; 31 | 32 | // 33 | // If we don't have an old file, we should just remove the pre-commit hook. But 34 | // if we do have an old precommit file we want to restore that. 35 | // 36 | if (!exists(precommit +'.old')) { 37 | fs.unlinkSync(precommit); 38 | } else { 39 | fs.writeFileSync(precommit, fs.readFileSync(precommit +'.old')); 40 | fs.chmodSync(precommit, '755'); 41 | fs.unlinkSync(precommit +'.old'); 42 | } 43 | --------------------------------------------------------------------------------