├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── hook.sh.tpl ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | hook.sh 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !hook.sh.tpl 3 | !index.js 4 | !package.json 5 | !README.md 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 4 5 | - 5 6 | 7 | script: 8 | - npm run test:coverage 9 | - $(npm bin)/codeclimate-test-reporter < coverage/lcov.info 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `shared-git-hooks` 2 | 3 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-green.svg?style=flat-square)](https://github.com/feross/standard) 4 | [![build status](https://img.shields.io/travis/kilianc/shared-git-hooks.svg?style=flat-square)](https://travis-ci.org/kilianc/shared-git-hooks) 5 | [![coverage](https://img.shields.io/codeclimate/coverage/github/kilianc/shared-git-hooks.svg?style=flat-square)](https://codeclimate.com/github/kilianc/shared-git-hooks/coverage) 6 | [![npm version](https://img.shields.io/npm/v/shared-git-hooks.svg?style=flat-square)](https://www.npmjs.com/package/shared-git-hooks) 7 | [![npm downloads](https://img.shields.io/npm/dm/shared-git-hooks.svg?style=flat-square)](https://www.npmjs.com/package/shared-git-hooks) 8 | [![License](https://img.shields.io/npm/l/shared-git-hooks.svg?style=flat-square)](https://www.npmjs.com/package/shared-git-hooks) 9 | 10 | ## Install (Node.js >= 0.12.0) 11 | 12 | $ npm i shared-git-hooks --save-dev 13 | 14 | ## Usage 15 | 16 | Add your scripts to `./hooks` and name them 1:1 as git hooks (optional extension allowed): 17 | 18 | applypatch-msg.EXT 19 | pre-applypatch.EXT 20 | post-applypatch.EXT 21 | pre-commit.EXT 22 | prepare-commit-msg.EXT 23 | commit-msg.EXT 24 | post-commit.EXT 25 | pre-rebase.EXT 26 | post-checkout.EXT 27 | post-merge.EXT 28 | pre-push.EXT 29 | pre-receive.EXT 30 | update.EXT 31 | post-receive.EXT 32 | post-update.EXT 33 | pre-auto-gc.EXT 34 | post-rewrite.EXT 35 | 36 | A generic script is symlinked in `.git/hooks` when you `npm install` your project. The script will look for an executable file with a ~matching name in your `./hooks` and run it if found. 37 | 38 | You can also set a `$GIT_HOOKS_PATH` env var and customize your scripts location. 39 | 40 | ## Assumption 41 | 42 | This project assumes that: 43 | 44 | * you have a `package.json`, `.git/` and `hooks/` in the root of your project. 45 | * you want to run your hooks with the same env `$PATH` you have when you run `npm install` *(this will allow git GUI like Tower to access node through `nvm` for example)* 46 | 47 | ## Example 48 | 49 | Save the following as `./hooks/pre-commit` 50 | 51 | ```bash 52 | #!/usr/bin/env node 53 | console.log('refusing all commits!') 54 | process.exit(1) 55 | ``` 56 | 57 | Remember to `chmod +x ./hooks/pre-commit`. 58 | 59 | ## Related projects 60 | 61 | * Original [git-hooks](https://github.com/icefox/git-hooks) project 62 | * [pre-commit](https://github.com/observing/pre-commit) 63 | * [ghooks](https://github.com/gtramontina/ghooks) 64 | * [git-hooks-js](https://github.com/tarmolov/git-hooks-js) 65 | * [husky](https://github.com/typicode/husky) 66 | 67 | ## How to contribute 68 | 69 | This project follows the awesome [Vincent Driessen](http://nvie.com/about/) [branching model](http://nvie.com/posts/a-successful-git-branching-model/). 70 | 71 | * You must add a new feature on its own branch 72 | * You must contribute to hot-fixing, directly into the master branch (and pull-request to it) 73 | 74 | This project uses [standardjs](https://github.com/feross/standard) to enforce a consistent code style. Your contribution must be pass standard validation. 75 | 76 | The test suite is written on top of [mochajs/mocha](http://mochajs.org/). Use the tests to check if your contribution breaks some part of the library and be sure to add new tests for each new feature. 77 | 78 | $ npm test 79 | 80 | ## License 81 | 82 | _This software is released under the MIT license cited below_. 83 | 84 | Copyright (c) 2015 Kilian Ciuffolo, me@nailik.org. All Rights Reserved. 85 | 86 | Permission is hereby granted, free of charge, to any person 87 | obtaining a copy of this software and associated documentation 88 | files (the 'Software'), to deal in the Software without 89 | restriction, including without limitation the rights to use, 90 | copy, modify, merge, publish, distribute, sublicense, and/or sell 91 | copies of the Software, and to permit persons to whom the 92 | Software is furnished to do so, subject to the following 93 | conditions: 94 | 95 | The above copyright notice and this permission notice shall be 96 | included in all copies or substantial portions of the Software. 97 | 98 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 99 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 100 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 101 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 102 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 103 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 104 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 105 | OTHER DEALINGS IN THE SOFTWARE. 106 | -------------------------------------------------------------------------------- /hook.sh.tpl: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin bash 2 | 3 | FILENAME="$(basename "$0")" 4 | GIT_HOOKS_DIR="${GIT_HOOKS_DIR:=$(pwd)/hooks}" 5 | GIT_HOOK_PATH="$GIT_HOOKS_DIR"/$(ls "$GIT_HOOKS_DIR" | grep "$FILENAME") 6 | PATH="{{PATH}}" 7 | 8 | if [ -f "$GIT_HOOK_PATH" ]; then 9 | echo "> $GIT_HOOK_PATH exists, running" 10 | echo "" 11 | "$GIT_HOOK_PATH" $@ 12 | fi 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * index.js 3 | * Created by Kilian Ciuffolo on Dec 28, 2015 4 | * (c) 2015 Kilian Ciuffolo 5 | */ 6 | 7 | 'use strict' 8 | 9 | const fs = require('fs') 10 | const execSync = require('child_process').execSync 11 | const resolve = require('path').resolve 12 | const HOOK_SCRIPT_PATH = resolve(__dirname, 'hook.sh') 13 | 14 | function findProjectRoot () { 15 | let stdout = execSync('git rev-parse --git-dir') 16 | return resolve(stdout.toString().replace('\n', ''), '..') 17 | } 18 | 19 | function ensureHooksDirExists (projectPath) { 20 | let hooksDir = resolve(projectPath, '.git/hooks') 21 | !doesPathExist(hooksDir) && fs.mkdirSync(hooksDir) 22 | } 23 | 24 | function saveHookRunner () { 25 | let tpl = fs.readFileSync(resolve(__dirname, 'hook.sh.tpl'), 'utf8') 26 | tpl = tpl.replace('{{PATH}}', execSync('sh -c \'echo ${PATH}\'', {encoding: 'utf-8'}).trim()) 27 | fs.writeFileSync(resolve(__dirname, 'hook.sh'), tpl) 28 | fs.chmodSync(resolve(__dirname, 'hook.sh'), '744') 29 | } 30 | 31 | function installHook (hookName, projectPath) { 32 | var path = resolve(projectPath, '.git/hooks', hookName) 33 | 34 | if (isAlreadyInstalled(path)) { 35 | exports.log(` [ = ] ${hookName}`) 36 | return false 37 | } 38 | if (doesPathExist(path)) { 39 | exports.log(` [ ➜ ] ${hookName}.bak`) 40 | backup(path) 41 | } 42 | 43 | fs.symlinkSync(HOOK_SCRIPT_PATH, path) 44 | exports.log(` [ ✓ ] ${hookName}`) 45 | 46 | return true 47 | } 48 | 49 | function doesPathExist (path) { 50 | try { 51 | fs.statSync(path) 52 | } catch (err) { 53 | return false 54 | } 55 | 56 | return true 57 | } 58 | 59 | function isAlreadyInstalled (path) { 60 | return doesPathExist(path) && 61 | fs.lstatSync(path).isSymbolicLink() && 62 | fs.readlinkSync(path) === HOOK_SCRIPT_PATH 63 | } 64 | 65 | function backup (path) { 66 | fs.renameSync(path, path + '.bak') 67 | } 68 | 69 | function installHooks (hooks) { 70 | let projectPath = exports.findProjectRoot() 71 | exports.ensureHooksDirExists(projectPath) 72 | exports.saveHookRunner() 73 | hooks.forEach((hookName) => exports.installHook(hookName, projectPath)) 74 | } 75 | 76 | exports.backup = backup 77 | exports.ensureHooksDirExists = ensureHooksDirExists 78 | exports.findProjectRoot = findProjectRoot 79 | exports.installHook = installHook 80 | exports.installHooks = installHooks 81 | exports.isAlreadyInstalled = isAlreadyInstalled 82 | exports.saveHookRunner = saveHookRunner 83 | exports.HOOK_SCRIPT_PATH = HOOK_SCRIPT_PATH 84 | exports.log = console.log.bind(console) 85 | 86 | if (!module.parent) { 87 | exports.log('Symlinking shared-git-hooks runner in .git/hooks\n') 88 | 89 | installHooks([ 90 | 'applypatch-msg', 91 | 'commit-msg', 92 | 'post-applypatch', 93 | 'post-checkout', 94 | 'post-commit', 95 | 'post-merge', 96 | 'post-receive', 97 | 'post-rewrite', 98 | 'post-update', 99 | 'pre-applypatch', 100 | 'pre-auto-gc', 101 | 'pre-commit', 102 | 'pre-push', 103 | 'pre-rebase', 104 | 'pre-receive', 105 | 'prepare-commit-msg', 106 | 'update' 107 | ]) 108 | 109 | exports.log() 110 | exports.log(`> Please add your scripts in ${exports.findProjectRoot()}/hooks or $GIT_HOOKS_PATH\n`) 111 | } 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared-git-hooks", 3 | "version": "1.2.1", 4 | "description": "Share git hooks across your team through npm install", 5 | "main": "index.js", 6 | "engines": { 7 | "node" : ">=0.12.0" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "chai": "^3.4.1", 12 | "codeclimate-test-reporter": "^0.1.1", 13 | "istanbul": "^0.4.1", 14 | "mocha": "^2.3.4", 15 | "mocha-standard": "^1.0.0", 16 | "mock-fs": "^3.6.0", 17 | "sinon": "^1.17.2", 18 | "standard": "^5.4.1" 19 | }, 20 | "scripts": { 21 | "test": "$(npm bin)/mocha", 22 | "test:watch": "nodemon --exec $(npm bin)/_mocha --bail", 23 | "test:coverage": "$(npm bin)/istanbul cover $(npm bin)/_mocha", 24 | "install": "node index.js" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/kilianc/shared-git-hooks.git" 29 | }, 30 | "keywords": [ 31 | "git", 32 | "hooks" 33 | ], 34 | "author": "Kilian Ciuffolo ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/kilianc/shared-git-hooks/issues" 38 | }, 39 | "homepage": "https://github.com/kilianc/shared-git-hooks#readme", 40 | "standard": { 41 | "globals": [ 42 | "after", 43 | "afterEach", 44 | "before", 45 | "describe", 46 | "it" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * test.js 3 | * Created by Kilian Ciuffolo on Dec 28, 2015 4 | * (c) 2015 Kilian Ciuffolo 5 | */ 6 | 7 | 'use strict' 8 | 9 | const fs = require('fs') 10 | const execSync = require('child_process').execSync 11 | const assert = require('chai').assert 12 | const sinon = require('sinon') 13 | const mock = require('mock-fs') 14 | const ghooks = require('./') 15 | 16 | ghooks.log = () => {} 17 | 18 | describe('shared-git-hooks', () => { 19 | let sandbox = sinon.sandbox.create() 20 | 21 | afterEach(() => { 22 | mock.restore() 23 | sandbox.restore() 24 | }) 25 | 26 | describe('findProjectRoot()', () => { 27 | let cwd = process.cwd() 28 | execSync('rm -rf test-git-root') 29 | 30 | after(function () { 31 | process.chdir(cwd) 32 | execSync('rm -rf test-git-root') 33 | }) 34 | 35 | it('should return the root path of the project', () => { 36 | fs.mkdirSync('test-git-root') 37 | fs.mkdirSync('test-git-root/foo') 38 | fs.mkdirSync('test-git-root/foo/bar') 39 | execSync('cd test-git-root && git init') 40 | process.chdir('test-git-root/foo/bar') 41 | assert.equal(execSync('pwd'), __dirname + '/test-git-root/foo/bar\n') 42 | assert.equal(ghooks.findProjectRoot(), __dirname + '/test-git-root') 43 | }) 44 | }) 45 | 46 | describe('ensureHooksDirExists()', () => { 47 | it('should create .git/hooks if not present', () => { 48 | mock({ '/project/.git': {} }) 49 | assert.throws(() => fs.statSync('/project/.git/hooks')) 50 | ghooks.ensureHooksDirExists('/project/') 51 | assert.doesNotThrow(() => fs.statSync('/project/.git/hooks')) 52 | }) 53 | }) 54 | 55 | describe('saveHookRunner()', () => { 56 | it('should save hook.sh and freeze $PATH', () => { 57 | mock({ 58 | [`${__dirname}/hook.sh.tpl`]: fs.readFileSync(__dirname + '/hook.sh.tpl') 59 | }) 60 | ghooks.saveHookRunner() 61 | let runner = fs.readFileSync(__dirname + '/hook.sh').toString() 62 | assert.ok(runner.includes(`PATH="${process.env.PATH}"`)) 63 | }) 64 | }) 65 | 66 | describe('isAlreadyInstalled()', () => { 67 | it('should return true if already installed', () => { 68 | mock({ 69 | [__dirname + '/hook.sh']: 'foo', 70 | '/foo/hook': mock.symlink({ 71 | path: __dirname + '/hook.sh' 72 | }) 73 | }) 74 | assert.ok(ghooks.isAlreadyInstalled('/foo/hook')) 75 | }) 76 | 77 | it('should return false if another script is present', () => { 78 | mock({ '/foo/hook': 'foo' }) 79 | assert.notOk(ghooks.isAlreadyInstalled('/foo/hook')) 80 | }) 81 | 82 | it('should return false hook is missing', () => { 83 | mock({}) 84 | assert.notOk(ghooks.isAlreadyInstalled('/foo/not-exist')) 85 | }) 86 | }) 87 | 88 | describe('backup()', () => { 89 | it('should work', () => { 90 | mock({ 'path/to/something': 'foo' }) 91 | ghooks.backup('path/to/something') 92 | assert.equal(fs.readFileSync('path/to/something.bak'), 'foo') 93 | }) 94 | }) 95 | 96 | describe('installHook()', () => { 97 | it('should install the hook', () => { 98 | mock({ 99 | [__dirname + '/hook.sh']: 'foo', 100 | '/project/.git/hooks': {} 101 | }) 102 | assert.ok(ghooks.installHook('test', '/project')) 103 | assert.doesNotThrow(() => fs.lstatSync('/project/.git/hooks/test')) 104 | assert.equal(fs.readlinkSync('/project/.git/hooks/test'), ghooks.HOOK_SCRIPT_PATH) 105 | }) 106 | 107 | it('should skip if already installed', () => { 108 | mock({ 109 | [__dirname + '/hook.sh']: 'foo', 110 | '/project/.git/hooks/test': mock.symlink({ 111 | path: __dirname + '/hook.sh' 112 | }) 113 | }) 114 | assert.notOk(ghooks.installHook('test', '/project')) 115 | assert.doesNotThrow(() => fs.lstatSync('/project/.git/hooks/test')) 116 | assert.equal(fs.readlinkSync('/project/.git/hooks/test'), ghooks.HOOK_SCRIPT_PATH) 117 | }) 118 | 119 | it('should backup old hooks', () => { 120 | mock({ 121 | [__dirname + '/hook.sh']: 'foo', 122 | '/project/.git/hooks/test': 'foo' 123 | }) 124 | assert.ok(ghooks.installHook('test', '/project')) 125 | assert.doesNotThrow(() => fs.lstatSync('/project/.git/hooks/test')) 126 | assert.equal(fs.readlinkSync('/project/.git/hooks/test'), ghooks.HOOK_SCRIPT_PATH) 127 | assert.equal(fs.readFileSync('/project/.git/hooks/test.bak'), 'foo') 128 | }) 129 | }) 130 | 131 | describe('installHooks()', () => { 132 | it('should work', () => { 133 | sandbox.stub(ghooks, 'ensureHooksDirExists') 134 | sandbox.stub(ghooks, 'installHook') 135 | 136 | ghooks.installHooks(['foo']) 137 | assert.ok(ghooks.ensureHooksDirExists.calledOnce, 'ensureHooksDirExists') 138 | assert.ok(ghooks.installHook.calledWith('foo', __dirname)) 139 | }) 140 | }) 141 | }) 142 | 143 | describe('JavaScript codebase', function () { 144 | it('conforms to javascript standard style (http://standardjs.com)', require('mocha-standard')) 145 | }) 146 | --------------------------------------------------------------------------------