v1.0.0-rc.14 (#301)

Merging dev branch back to master 🎉
This commit is contained in:
typicode
2018-09-22 01:20:22 +02:00
committed by GitHub
parent 7a77d01562
commit 29feb2d188
51 changed files with 6882 additions and 2067 deletions
+1
View File
@@ -0,0 +1 @@
src/* linguist-documentation
+4 -2
View File
@@ -1,3 +1,5 @@
*.log
.DS_Store
node_modules
tmp
ci-post-checkout
lib
node_modules/
+7
View File
@@ -0,0 +1,7 @@
{
"skipCI": false,
"hooks": {
"pre-commit": "npm test",
"post-checkout": "rm -f ci-post-checkout && echo \"running CI post-checkout test\" && node scripts/ci-post-checkout-script"
}
}
+11 -2
View File
@@ -1,3 +1,12 @@
test/
.gitattributes
.huskyrc
.travis.yml
appveyor.yml
ci-post-checkout
preprocessor.js
scripts
src
tsconfig.json
tslint.json
typings
docs-util.js
+16 -4
View File
@@ -3,9 +3,21 @@ os:
- osx
language: node_js
node_js:
- "stable"
- "4"
- "node"
- "6"
cache:
directories:
- "node_modules"
script:
- npm test
- HUSKY_IGNORE_CI=true node bin/install
- bash -n .git/hooks/pre-commit
# Test post-checkout
- npm run devinstall
- cat .git/hooks/post-checkout
- time git checkout
- node scripts/ci-post-checkout-check
# Should not fail due to missing script
- node scripts/ci-break-path.js
- cat .git/hooks/post-checkout
- time git checkout
# Test uninstall
- npm run devuninstall
+114 -4
View File
@@ -1,10 +1,120 @@
# CHANGELOG
## Unreleased
## 1.0.0-rc.14
* Support `sendemail-validate` hook [#173](https://github.com/typicode/husky/pull/173)
* Support `HUSKY_SKIP_INSTALL` environment variable for skipping git hooks installation
* Drop `nvm` installed with `brew` support as it's not supported by `nvm` itself (see [creationix/nvm#important-notes](https://github.com/creationix/nvm#important-notes)), `nvm` standard installation is still supported though
* Make `--no-verify` message more accurate and only show it for hooks that can be skipped
* Improve error messages
## 1.0.0-rc.13
* Show a message when Node can't be found in PATH
## 1.0.0-rc.12
* Fix exit code when used with unsupported Node version
* Update dependencies
## 1.0.0-rc.11
* Show a message instead of crashing if Node version is unsupported by Husky
## 1.0.0-rc.10
* Fix `HUSKY_GIT_PARAMS` containing only the first Git param
## 1.0.0-rc.9
* If `node_modules/husky` is missing, show message but do not crash
* Remove and upgrade some `dependencies`
## 1.0.0-rc.8
* Add `HUSKY_GIT_STDIN` environment variable for `pre-push`, `pre-receive`, `post-receive` and `post-rewrite` to access arguments passed by Git via stdin.
## 1.0.0-rc.7
* Update `cosmiconfig` dependency
* Fix `package.json` normalization error
## 1.0.0-rc.6
* Fix install error with `pnpm`
## 1.0.0-rc.5
* Auto migrate `yorkie` hooks
## 1.0.0-rc.4
* Re-enable Git submodule and worktree support
## 1.0.0-rc.3
* Re-enable subdirectory support
## 1.0.0-rc.2
* Upgrade `run-node` dependency
## 1.0.0-rc.1
* Namespace environment variable created by husky
* `GIT_PARAMS` becomes `HUSKY_GIT_PARAMS`
* Starting also with `1.0.0` versioning
## 0.15.0-rc.13
* Revert `GIT_STDIN` for the moment. Needs more testing.
`rc.10`, `rc.11` and `rc.12` are broken for some GUI clients due to `read` command
## 0.15.0-rc.9
* Handle case where `.git/hooks` directory doesn't exit
## 0.15.0-rc.8
* Handle case where `v0.14` git hooks wouldn't have been uninstalled
## 0.15.0-rc.7
* Move `postinstall` script to `install`
* Fix line ending error when running `upgrader` from OS X/Linux
## 0.15.0-rc.6
* Fix `[[` error
## 0.15.0-rc.5
* Fix error with GitHub Desktop on Windows
## 0.15.0-rc.4
* Catch error if `git` command fails
## 0.15.0-rc.3
* Fix `husky-upgrade`
* Drop `Node 4` support
## 0.15.0-rc.2
* Fix install error
## 0.15.0-rc.1
* `sendemail-validate` hook [#173](https://github.com/typicode/husky/pull/173)
* `HUSKY_SKIP_INSTALL` environment variable for skipping git hooks installation [#178](https://github.com/typicode/husky/pull/178)
* `.huskyrc` config [#209](https://github.com/typicode/husky/pull/209)
* `pnpm` support
* Support environments where `yarn` is the only package manager installed
* Move config from `scripts` field to `husky` field
* Prefer raw names for Git hooks (`pre-commit` rather than `precommit`)
* Drop integrated `nvm` support
* To ease upgrade:
* Provide `husky-upgrade` command
* Add deprecation warning for hooks that are defined in `scripts` (but still run them)
## 0.14.3
+72
View File
@@ -0,0 +1,72 @@
# Documentation
## Supported hooks
`husky` supports all Git hooks defined [here](https://git-scm.com/docs/githooks).
Server-side hooks (`pre-receive`, `update` and `post-receive`) aren't supported.
## Access Git params and stdin
Git hooks can get parameters via command-line arguments and stdin. Husky makes them accessible via `HUSKY_GIT_PARAMS` and `HUSKY_GIT_STDIN` environment variables.
```
{
"husky": {
"hooks": {
"commit-msg": "echo $HUSKY_GIT_PARAMS"
}
}
}
```
## Disable auto-install
If you don't want `husky` to automatically install Git hooks, simply set `HUSKY_SKIP_INSTALL` environment variable to `true`.
```
HUSKY_SKIP_INSTALL=true npm install
```
## Multi-package repository (monorepo)
If you have a multi-package repository, it's __recommended__ to use tools like [lerna](https://github.com/lerna/lerna) and have `husky` installed ONLY in the root `package.json` to act as the source of truth.
Generally speaking, you should AVOID defining `husky` in multiple `package.json`, as each package would overwrite previous `husky` installations.
```sh
.
└── root
├── .git
├── package.json 🐶 # Add husky here
└── packages
├── A
│ └── package.json
├── B
│ └── package.json
└── C
└── package.json
```
```js
// root/package.json
{
"private": true,
"devDependencies": {
"husky": "..."
},
"husky": {
"hooks": {
"pre-commit": "lerna run test"
}
}
}
```
## Node version management
If you're on Windows, husky will simply use the version installed globally on your system.
For macOS and Linux users:
- if you're running `git` commands in the terminal, husky will use the version defined in your shell `PATH`. So if you're a `nvm` user, husky will use the version that you've set with `nvm`.
- if you're using a GUI client and `nvm`, it may have a different `PATH` and not load `nvm`, in this case the highest `node` version installed by `nvm` will usually be picked. You can also check `~/.node_path` to see which version is used by GUIs and edit if you want to use something else.
-25
View File
@@ -1,25 +0,0 @@
# Hooks
Husky supports all git hooks (https://git-scm.com/docs/githooks). Simply add the corresponding `npm script` to your `package.json`.
| Git hook | npm script |
| -------- | ---------- |
| applypatch-msg | applypatchmsg |
| commit-msg | commitmsg |
| post-applypatch | postapplypatch |
| post-checkout | postcheckout |
| post-commit | postcommit |
| post-merge | postmerge |
| post-receive | postreceive |
| post-rewrite | postrewrite |
| post-update | postupdate |
| pre-applypatch | preapplypatch |
| pre-auto-gc | preautogc |
| pre-commit | precommit |
| pre-push | prepush |
| pre-rebase | prerebase |
| pre-receive | prereceive |
| prepare-commit-msg | preparecommitmsg |
| push-to-checkout | pushtocheckout |
| update | update |
| sendemail-validate | sendemailvalidate |
+3 -3
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
MIT License
Copyright (c) 2014
Copyright (c) 2017
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
+56 -76
View File
@@ -1,50 +1,41 @@
# husky [![](http://img.shields.io/npm/dm/husky.svg?style=flat)](https://www.npmjs.org/package/husky) [![npm version](https://badge.fury.io/js/husky.svg)](https://www.npmjs.com/package/husky) [![Mac/Linux Build Status](https://img.shields.io/travis/typicode/husky/master.svg?label=Mac%20OSX%20%26%20Linux)](https://travis-ci.org/typicode/husky) [![Windows Build status](https://img.shields.io/appveyor/ci/typicode/husky/master.svg?label=Windows)](https://ci.appveyor.com/project/typicode/husky/branch/master)
# husky [![](https://img.shields.io/npm/dm/husky.svg?style=flat)](https://www.npmjs.org/package/husky) [![Mac/Linux Build Status](https://img.shields.io/travis/typicode/husky/dev.svg?label=Mac%20OSX%20%26%20Linux)](https://travis-ci.org/typicode/husky) [![Windows Build status](https://img.shields.io/appveyor/ci/typicode/husky/dev.svg?label=Windows)](https://ci.appveyor.com/project/typicode/husky/dev)
> Git hooks made easy
Husky can prevent bad commit, push and more :dog: _woof!_
Husky can prevent bad `git commit`, `git push` and more :dog: _woof!_
__New beta version available__ [here](https://github.com/typicode/husky/tree/dev). Feedback and bug reports welcome :wink:
_You're viewing the documentation for the next version of husky, click [here](https://github.com/typicode/husky/tree/v0.14.3) if you prefer to view docs for the stable version (`v0.14.3`)_
_9/14: to all the amazing people who have answered the Husky survey I posted a few months ago, thanks so much <3 !_
<a href="https://www.patreon.com/typicode">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a>
## Install
```sh
npm install husky --save-dev
npm install husky@next --save-dev
```
```
yarn add husky --dev
```
```javascript
// Edit package.json
```js
// package.json
{
"scripts": {
"precommit": "npm test",
"prepush": "npm test",
"...": "..."
"husky": {
"hooks": {
"pre-commit": "npm test",
"pre-push": "npm test",
"...": "..."
}
}
}
```
```bash
git commit -m "Keep calm and commit"
```sh
git commit -m 'Keep calm and commit'
```
_Existing hooks aren't replaced and you can use [any Git hook](HOOKS.md)._
_If you're migrating from `ghooks`, simply run `npm uninstall ghooks --save-dev && npm install husky --save-dev` and edit `package.json`. Husky will automatically migrate `ghooks` hooks._
## Used by
* [jQuery](https://github.com/jquery/jquery)
* [Next.js](https://github.com/zeit/next.js)
* [Hyper](https://github.com/zeit/hyper)
* [Paper.js](https://github.com/paperjs/paper.js)
* [Kibana](https://github.com/elastic/kibana)
* [JSON Server](https://github.com/typicode/json-server)
* [Hotel](https://github.com/typicode/hotel)
* ... and 12000+ [other awesome projects](https://libraries.io/npm/husky/dependent-repositories).
Check [documentation](https://github.com/typicode/husky/blob/dev/DOCS.md) for more.
## Uninstall
@@ -52,66 +43,55 @@ _If you're migrating from `ghooks`, simply run `npm uninstall ghooks --save-dev
npm uninstall husky
```
```sh
yarn remove husky
## Upgrading from 0.14
If you're upgrading from `0.14`, simply move your hooks to `husky.hooks`:
```diff
{
"scripts": {
- "precommit": "npm test"
},
+ "husky": {
+ "hooks": {
+ "pre-commit": "npm test"
+ }
+ }
}
```
## Tricks
Or run the following command which will do the same automatically for you ;)
<details>
### Debug hooks easily
If you need to debug hooks, simply use `npm run <script-name>`. For example:
```bash
npm run precommit
```
./node_modules/.bin/husky-upgrade
```
### Git GUI clients support
Alternatively, you can also use any of the files/formats that are supported by [cosmiconfig](https://github.com/davidtheclark/cosmiconfig). This means that you can place your husky hooks config in a `.huskyrc` file or export them from a `husky.config.js` file as well. Cosmiconfig supports `js`, `json`, and `yaml` file formats.
If you've installed Node using the [standard installer](https://nodejs.org/en/), [nvm](https://github.com/creationix/nvm) or [homebrew](http://brew.sh/), Git hooks will be executed in GUI applications.
## Features
### Working with multiple version of Node
* Keeps existing user hooks
* Supports GUI Git clients
* Supports all Git hooks (`pre-commit`, `pre-push`, ...)
If [`nvm`](https://github.com/creationix/nvm) is installed, husky will try to use the `default`/`current` installed Node version or use the project `.nvmrc`.
## Used by
__Tip__ to use the system-installed version of node, `nvm` provides a [`system`](https://github.com/creationix/nvm#system-version-of-node) alias
### Accessing Git params
Git params can be found in `GIT_PARAMS` environment variable.
### Setting a different log level
By default, husky will run scripts using `--silent` to make the output more readable. If you want to override this, simply pass a different log level to your scripts:
```json
"precommit": "npm run some-script -q"
```
_`-q/--quiet` is equivalent to `--loglevel warn` which is npm default log level._
### Git submodule and subtree support
Yes
### Cygwin support
Yes
### Yarn support
Please use `yarn` `v0.24+`
</details>
* [jQuery](https://github.com/jquery/jquery)
* [babel](https://github.com/babel/babel)
* [create-react-app](https://github.com/facebookincubator/create-react-app)
* [Next.js](https://github.com/zeit/next.js)
* [Hyper](https://github.com/zeit/hyper)
* [Kibana](https://github.com/elastic/kibana)
* [JSON Server](https://github.com/typicode/json-server)
* [Hotel](https://github.com/typicode/hotel)
* ... and 23k+ [other awesome repos](https://libraries.io/npm/husky/dependent-repositories) :tada:
## See also
* [pkg-ok](https://github.com/typicode/pkg-ok) - Prevents publishing modules with bad paths
* [pkg-ok](https://github.com/typicode/pkg-ok) - Prevents publishing a module with bad paths or incorrect line endings
* [please-upgrade-node](https://github.com/typicode/please-upgrade-node) - Show a message to upgrade Node instead of a stacktrace in your CLIs
* [react-fake-props](https://github.com/typicode/react-fake-props) - Fake props for your React tests
## License
MIT - [Typicode :cactus:](https://github.com/typicode) - [Patreon](https://www.patreon.com/typicode)
MIT - [Typicode :cactus:](https://github.com/typicode) - [Patreon](https://www.patreon.com/typicode) - [Supporters](https://thanks.typicode.com)
-171
View File
@@ -1,171 +0,0 @@
'use strict'
const fs = require('fs')
const path = require('path')
const mkdirp = require('mkdirp')
const rimraf = require('rimraf')
const tempy = require('tempy')
const installFrom = require('../src/install')
const uninstallFrom = require('../src/uninstall')
function install(rootDir, dir) {
installFrom(path.join(rootDir, dir))
}
function uninstall(rootDir, dir) {
uninstallFrom(path.join(rootDir, dir))
}
function mkdir(rootDir, dir) {
mkdirp.sync(path.join(rootDir, dir))
}
function writeFile(dir, filePath, data) {
fs.writeFileSync(path.join(dir, filePath), data)
}
function readFile(dir, filePath) {
return fs.readFileSync(path.join(dir, filePath), 'utf-8')
}
function exists(dir, filePath) {
return fs.existsSync(path.join(dir, filePath))
}
describe('husky', () => {
let dir
beforeEach(() => (dir = tempy.directory()))
afterEach(() => rimraf.sync(dir))
it('should support basic layout', () => {
mkdir(dir, '.git/hooks')
mkdir(dir, 'node_modules/husky')
install(dir, '/node_modules/husky')
const hook = readFile(dir, '.git/hooks/pre-commit')
expect(hook).toMatch('#husky')
expect(hook).toMatch('cd "."')
expect(hook).toMatch('npm run -s precommit')
expect(hook).toMatch('--no-verify')
const prepareCommitMsg = readFile(dir, '.git/hooks/prepare-commit-msg')
expect(prepareCommitMsg).toMatch('cannot be bypassed')
uninstall(dir, 'node_modules/husky')
expect(exists(dir, '.git/hooks/pre-push')).toBeFalsy()
})
it('should support project installed in sub directory', () => {
mkdir(dir, '.git/hooks')
mkdir(dir, 'A/B/node_modules/husky')
install(dir, 'A/B/node_modules/husky')
const hook = readFile(dir, '.git/hooks/pre-commit')
expect(hook).toMatch('cd "A/B"')
uninstall(dir, 'A/B/node_modules/husky')
expect(exists(dir, '.git/hooks/pre-push')).toBeFalsy()
})
it('should support git submodule', () => {
mkdir(dir, '.git/modules/A/B')
mkdir(dir, 'A/B/node_modules/husky')
writeFile(dir, 'A/B/.git', 'git: ../../.git/modules/A/B')
install(dir, 'A/B/node_modules/husky')
const hook = readFile(dir, '.git/modules/A/B/hooks/pre-commit')
expect(hook).toMatch('cd "."')
uninstall(dir, 'A/B/node_modules/husky')
expect(exists(dir, '.git/hooks/pre-push')).toBeFalsy()
})
it('should support git submodule and sub directory', () => {
mkdir(dir, '.git/modules/A/B')
mkdir(dir, 'A/B/C/node_modules/husky')
writeFile(dir, 'A/B/.git', 'git: ../../.git/modules/A/B')
install(dir, 'A/B/C/node_modules/husky')
const hook = readFile(dir, '.git/modules/A/B/hooks/pre-commit')
expect(hook).toMatch('cd "C"')
uninstall(dir, 'A/B/app/node_modules/husky')
expect(exists(dir, '.git/hooks/pre-push')).toBeFalsy()
})
it('should support git worktrees', () => {
mkdir(dir, '.git/worktrees/B')
mkdir(dir, 'A/B/node_modules/husky')
// Git path for worktrees is absolute
const absolutePath = path.join(dir, '.git/worktrees/B')
writeFile(dir, 'A/B/.git', `git: ${absolutePath}`)
install(dir, 'A/B/node_modules/husky')
const hook = readFile(dir, '.git/worktrees/B/hooks/pre-commit')
expect(hook).toMatch('cd "."')
uninstall(dir, 'A/B/node_modules/husky')
expect(exists(dir, '.git/hooks/pre-commit')).toBeFalsy()
})
it('should not modify user hooks', () => {
mkdir(dir, '.git/hooks')
mkdir(dir, 'node_modules/husky')
writeFile(dir, '.git/hooks/pre-push', 'foo')
// Verify that it's not overwritten
install(dir, 'node_modules/husky')
const hook = readFile(dir, '.git/hooks/pre-push')
expect(hook).toBe('foo')
uninstall(dir, 'node_modules/husky')
expect(exists(dir, '.git/hooks/pre-push')).toBeTruthy()
})
it('should not install from /node_modules/A/node_modules', () => {
mkdir(dir, '.git/hooks')
mkdir(dir, 'node_modules/A/node_modules/husky')
install(dir, 'node_modules/A/node_modules/husky')
expect(exists(dir, '.git/hooks/pre-push')).toBeFalsy()
})
it("should not crash if there's no .git directory", () => {
mkdir(dir, 'node_modules/husky')
expect(() => install(dir, 'node_modules/husky')).not.toThrow()
expect(() => uninstall(dir, 'node_modules/husky')).not.toThrow()
})
it('should migrate existing scripts (ghooks)', () => {
mkdir(dir, '.git/hooks')
mkdir(dir, '/node_modules/husky')
writeFile(
dir,
'.git/hooks/pre-commit',
'// Generated by ghooks. Do not edit this file.'
)
install(dir, 'node_modules/husky')
const hook = readFile(dir, '.git/hooks/pre-commit')
expect(hook).toMatch('husky')
expect(hook).not.toMatch('ghooks')
})
it('should migrate existing scripts (pre-commit)', () => {
mkdir(dir, '.git/hooks')
mkdir(dir, '/node_modules/husky')
writeFile(dir, '.git/hooks/pre-commit', './node_modules/pre-commit/hook')
install(dir, 'node_modules/husky')
const hook = readFile(dir, '.git/hooks/pre-commit')
expect(hook).toMatch('husky')
expect(hook).not.toMatch('./node_modules/pre-commit/hook')
})
})
+19 -4
View File
@@ -1,21 +1,36 @@
# Test against the latest version of this Node.js version
environment:
nodejs_version: "8"
nodejs_version: "10"
# Install scripts. (runs after repo cloning)
install:
# Get the latest stable version of Node.js or io.js
- ps: Install-Product node $env:nodejs_version
# install modules
- npm install
# Install modules
- npm ci
cache:
- '%APPDATA%\npm-cache'
- node_modules
# Post-install test scripts.
test_script:
# Output useful info for debugging.
- node --version
- npm --version
# run tests
# Run tests
- npm test
# Run post-checkout hook test
- npm run devinstall
- cat .git/hooks/post-checkout
- git checkout
- node scripts/ci-post-checkout-check
# Should not fail due to missing script
- node scripts/ci-break-path.js
- cat .git/hooks/post-checkout
- git checkout
# Test uninstall
- npm run devuninstall
# Don't actually build.
build: off
-26
View File
@@ -1,26 +0,0 @@
'use strict'
// Run when package is installed
const path = require('path')
const isCI = require('is-ci')
const installFrom = require('../src/install')
console.log('husky')
if (isCI && !process.env.HUSKY_IGNORE_CI) {
console.log('CI detected, skipping Git hooks installation')
process.exit(0)
}
if (process.env.HUSKY_SKIP_INSTALL) {
console.log(
`env variable HUSKY_SKIP_INSTALL is set to ${process.env
.HUSKY_SKIP_INSTALL}, skipping Git hooks installation`
)
process.exit(0)
}
console.log('setting up Git hooks')
const huskyDir = path.join(__dirname, '..')
installFrom(huskyDir)
-11
View File
@@ -1,11 +0,0 @@
'use strict'
// Run when package is uninstalled
const path = require('path')
const uninstallFrom = require('../src/uninstall')
console.log('husky')
console.log('uninstalling Git hooks')
const huskyDir = path.join(__dirname, '..')
uninstallFrom(huskyDir)
+24
View File
@@ -0,0 +1,24 @@
// Used to generate trees for docs.md
const formatree = require('formatree')
console.log(formatree({
root: {
'.git': 0,
'package.json 🐶 # add husky here': 0,
packages: {
A: { 'package.json': 0 },
B: { 'package.json': 0 },
C: { 'package.json': 0 },
}
}
}))
console.log(formatree({
root: {
'.git': 0,
'package.json 🐶 # add husky here': 0,
'sub-directory': {
'package.json': 0
}
}
}))
+13
View File
@@ -0,0 +1,13 @@
const pleaseUpgradeNode = require('please-upgrade-node')
const pkg = require('./package.json')
// Node version isn't supported, skip install
pleaseUpgradeNode(pkg, {
exitCode: 0,
message: function(requiredVersion) {
return 'Husky requires Node ' + requiredVersion + ', skipping Git hooks installation.'
}
})
// Node version is supported, continue
require('./lib/installer/bin')
+5177 -1368
View File
File diff suppressed because it is too large Load Diff
+65 -23
View File
@@ -1,19 +1,30 @@
{
"name": "husky",
"version": "0.14.3",
"version": "1.0.0-rc.14",
"description": "Prevents bad commit or push (git hooks, pre-commit/precommit, pre-push/prepush, post-merge/postmerge and all that stuff...)",
"bin": {
"husky-upgrade": "./lib/upgrader/bin.js"
},
"engines": {
"node": ">=4"
"node": ">=6"
},
"scripts": {
"test": "jest",
"format": "prettier --single-quote --no-semi --write **/*.js",
"install": "node ./bin/install.js",
"uninstall": "node ./bin/uninstall.js"
"test": "npm run lint && jest",
"_install": "node husky install",
"preuninstall": "node husky uninstall",
"devinstall": "npm run build && npm run _install -- node_modules/husky && node scripts/dev-fix-path",
"devuninstall": "npm run build && npm run preuninstall -- node_modules/husky",
"build": "del-cli lib && tsc",
"version": "jest -u && git add -A src/installer/__tests__/__snapshots__",
"postversion": "git push && git push --tags",
"prepublishOnly": "npm run test && npm run build && pinst --enable && pkg-ok",
"postpublish": "pinst --disable",
"lint": "tslint 'src/**/*.ts'",
"fix": "npm run lint -- --fix"
},
"repository": {
"type": "git",
"url": "git://github.com/typicode/husky.git"
"url": "git+https://github.com/typicode/husky.git"
},
"keywords": [
"git",
@@ -27,29 +38,60 @@
"prepush",
"post-merge",
"postmerge",
"test"
"test",
"lint"
],
"author": "Typicode <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/typicode/husky/issues"
},
"homepage": "https://github.com/typicode/husky",
"devDependencies": {
"jest": "^20.0.4",
"mkdirp": "^0.5.1",
"prettier": "^1.4.4",
"rimraf": "^2.6.1",
"tempy": "^0.1.0"
},
"homepage": "https://github.com/typicode/husky#readme",
"dependencies": {
"is-ci": "^1.0.10",
"normalize-path": "^1.0.0",
"strip-indent": "^2.0.0"
"cosmiconfig": "^5.0.6",
"execa": "^0.9.0",
"find-up": "^3.0.0",
"get-stdin": "^6.0.0",
"is-ci": "^1.2.1",
"pkg-dir": "^3.0.0",
"please-upgrade-node": "^3.1.1",
"read-pkg": "^4.0.1",
"run-node": "^1.0.0",
"slash": "^2.0.0"
},
"standard": {
"env": {
"jest": true
}
"devDependencies": {
"@types/execa": "^0.9.0",
"@types/find-up": "^2.1.1",
"@types/get-stdin": "^5.0.1",
"@types/jest": "^23.3.2",
"@types/node": "^10.9.4",
"del": "^3.0.0",
"del-cli": "^1.1.0",
"formatree": "^1.0.2",
"jest": "^23.6.0",
"mkdirp": "^0.5.1",
"pinst": "^1.1.1",
"pkg-ok": "^2.2.0",
"prettier": "^1.14.2",
"tempy": "^0.2.1",
"ts-jest": "^23.1.4",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",
"tslint-plugin-prettier": "^1.3.0",
"typescript": "^3.0.3"
},
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
]
}
}
+4
View File
@@ -0,0 +1,4 @@
const replaceScriptPath = require('./replace-script-path')
// Replace scriptPath with missing script, which is the same as node_modules/husky being deleted
replaceScriptPath('missing/script.js')
+20
View File
@@ -0,0 +1,20 @@
const fs = require('fs')
const path = require('path')
const filename = path.join(__dirname, '../ci-post-checkout')
if (fs.existsSync(filename)) {
const data = fs.readFileSync(filename, 'utf-8')
if (data.split(' ').length !== 3) {
console.log('Not all params were set in HUSKY_GIT_PARAMS')
console.log('Got', data)
process.exit(1)
}
fs.unlinkSync(filename)
console.log('.git/hooks/post-checkout script has run successfully on CI')
} else {
console.log('.git/hooks/post-checkout script has failed running on CI')
process.exit(1)
}
+6
View File
@@ -0,0 +1,6 @@
const fs = require('fs')
const path = require('path')
const filename = path.join(__dirname, '../ci-post-checkout')
fs.writeFileSync(filename, process.env.HUSKY_GIT_PARAMS)
+4
View File
@@ -0,0 +1,4 @@
const replaceScriptPath = require('./replace-script-path')
// Replace scriptPath with local dev-runner to test locally
replaceScriptPath('scripts/dev-runner.js')
+5
View File
@@ -0,0 +1,5 @@
const path = require('path')
// Fake script path
process.argv[1] = path.join(__dirname, '../node_modules/husky/lib/runner/bin')
require('../lib/runner/bin')
+22
View File
@@ -0,0 +1,22 @@
const fs = require('fs')
const path = require('path')
module.exports = function replaceScriptPath(newPath) {
const regExp = /scriptPath="(.*)"/
fs.readdirSync('.git/hooks')
.filter(filename => path.extname(filename) !== '.sample')
.map(filename => {
console.log('devinstall: update', filename)
const absoluteFilename = path.join('.git/hooks', filename)
const data = fs.readFileSync(absoluteFilename, 'utf-8')
if (!regExp.test(data)) {
throw new Error("Can't find scriptPath")
}
const updatedData = data.replace(regExp, `scriptPath="${newPath}"`)
fs.writeFileSync(absoluteFilename, updatedData)
})
}
+38
View File
@@ -0,0 +1,38 @@
import * as fs from 'fs'
import * as path from 'path'
import * as tempy from 'tempy'
import getConf from '../getConf'
const testConf = { husky: { foo: 'bar' } }
describe('getConf', () => {
it('should return default conf', () => {
const tempDir = tempy.directory()
fs.writeFileSync(
path.join(tempDir, 'package.json'),
JSON.stringify(testConf)
)
expect(getConf(tempDir)).toEqual({ skipCI: true, foo: 'bar' })
})
it('should allow overriding default conf', () => {
const tempDir = tempy.directory()
fs.writeFileSync(
path.join(tempDir, 'package.json'),
JSON.stringify(testConf)
)
expect(getConf(tempDir)).toEqual({ skipCI: true, foo: 'bar' })
})
it('should support .huskyrc', () => {
const tempDir = tempy.directory()
fs.writeFileSync(
path.join(tempDir, '.huskyrc'),
JSON.stringify(testConf.husky)
)
expect(getConf(tempDir)).toEqual({ skipCI: true, foo: 'bar' })
})
})
+17
View File
@@ -0,0 +1,17 @@
import * as cosmiconfig from 'cosmiconfig'
interface IConf {
skipCI: boolean
hooks?: any
}
export default function getConf(dir: string): IConf {
const explorer = cosmiconfig('husky')
const { config = {} } = explorer.searchSync(dir) || {}
const defaults: IConf = {
skipCI: true
}
return { ...defaults, ...config }
}
+5
View File
@@ -0,0 +1,5 @@
declare module 'is-ci'
declare module 'pupa'
declare module 'pkg-dir'
declare module 'slash'
declare module 'read-pkg'
-21
View File
@@ -1,21 +0,0 @@
[
"applypatch-msg",
"pre-applypatch",
"post-applypatch",
"pre-commit",
"prepare-commit-msg",
"commit-msg",
"post-commit",
"pre-rebase",
"post-checkout",
"post-merge",
"pre-push",
"pre-receive",
"update",
"post-receive",
"post-update",
"push-to-checkout",
"pre-auto-gc",
"post-rewrite",
"sendemail-validate"
]
-115
View File
@@ -1,115 +0,0 @@
'use strict'
const fs = require('fs')
const path = require('path')
const findParent = require('./utils/find-parent')
const findHooksDir = require('./utils/find-hooks-dir')
const getHookScript = require('./utils/get-hook-script')
const is = require('./utils/is')
const hooks = require('./hooks.json')
const SKIP = 'SKIP'
const UPDATE = 'UPDATE'
const MIGRATE_GHOOKS = 'MIGRATE_GHOOKS'
const MIGRATE_PRE_COMMIT = 'MIGRATE_PRE_COMMIT'
const CREATE = 'CREATE'
function write(filename, data) {
fs.writeFileSync(filename, data)
fs.chmodSync(filename, parseInt('0755', 8))
}
function createHook(huskyDir, hooksDir, hookName, cmd) {
const filename = path.join(hooksDir, hookName)
// Assuming that this file is in node_modules/husky
const packageDir = path.join(huskyDir, '..', '..')
// Get project directory
// When used in submodule, the project dir is the first .git that is found
const projectDir = findParent(huskyDir, '.git')
// In order to support projects with package.json in a different directory
// than .git, find relative path from project directory to package.json
const relativePath = path.join('.', path.relative(projectDir, packageDir))
const hookScript = getHookScript(hookName, relativePath, cmd)
// Create hooks directory if needed
if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir)
if (!fs.existsSync(filename)) {
write(filename, hookScript)
return CREATE
}
if (is.ghooks(filename)) {
write(filename, hookScript)
return MIGRATE_GHOOKS
}
if (is.preCommit(filename)) {
write(filename, hookScript)
return MIGRATE_PRE_COMMIT
}
if (is.husky(filename)) {
write(filename, hookScript)
return UPDATE
}
return SKIP
}
function installFrom(huskyDir) {
try {
const isInSubNodeModule = (huskyDir.match(/node_modules/g) || []).length > 1
if (isInSubNodeModule) {
return console.log(
"trying to install from sub 'node_module' directory,",
'skipping Git hooks installation'
)
}
const hooksDir = findHooksDir(huskyDir)
if (hooksDir) {
hooks
.map(function(hookName) {
const npmScriptName = hookName.replace(/-/g, '')
return {
hookName: hookName,
action: createHook(huskyDir, hooksDir, hookName, npmScriptName)
}
})
.forEach(function(item) {
switch (item.action) {
case MIGRATE_GHOOKS:
console.log(`migrating existing ghooks ${item.hookName} script`)
break
case MIGRATE_PRE_COMMIT:
console.log(
`migrating existing pre-commit ${item.hookName} script`
)
break
case UPDATE:
break
case SKIP:
console.log(`skipping ${item.hookName} hook (existing user hook)`)
break
case CREATE:
break
default:
console.error('Unknown action')
}
})
console.log('done\n')
} else {
console.log("can't find .git directory, skipping Git hooks installation")
}
} catch (e) {
console.error(e)
}
}
module.exports = installFrom
@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`hookScript should match snapshot (OS X/Linux) 1`] = `
"#!/bin/sh
# husky
# v1.0.0-rc.14 darwin
scriptPath=\\"node_modules/husky/lib/runner/bin.js\\"
hookName=\`basename \\"$0\\"\`
gitParams=\\"$*\\"
if ! command -v node >/dev/null 2>&1; then
echo \\"Can't find node in PATH, trying to find a node binary on your system\\"
fi
if [ -f $scriptPath ]; then
node_modules/run-node/run-node $scriptPath $hookName \\"$gitParams\\"
else
echo \\"Can't find Husky, skipping $hookName hook\\"
echo \\"You can reinstall it using 'npm install husky --save-dev' or delete this hook\\"
fi
"
`;
exports[`hookScript should match snapshot (Windows) 1`] = `
"#!/bin/sh
# husky
# v1.0.0-rc.14 win32
scriptPath=\\"node_modules/husky/lib/runner/bin.js\\"
hookName=\`basename \\"$0\\"\`
gitParams=\\"$*\\"
if [ -f $scriptPath ]; then
node $scriptPath $hookName \\"$gitParams\\"
else
echo \\"Can't find Husky, skipping $hookName hook\\"
echo \\"You can reinstall it using 'npm install husky --save-dev' or delete this hook\\"
fi
"
`;
+23
View File
@@ -0,0 +1,23 @@
import getScript from '../getScript'
const rootDir = '/home/typicode/project'
const huskyDir = '/home/typicode/project/node_modules/husky'
// On OS X/Linux runNodePath gets resolved to the following value
// In order to make the test deterministic on AppVeyor, the value is hardcoded
const runNodePath = '/home/typicode/project/node_modules/run-node/run-node'
describe('hookScript', () => {
it('should match snapshot (OS X/Linux)', () => {
const script = getScript(rootDir, huskyDir, runNodePath, 'darwin')
expect(script).toMatchSnapshot()
expect(script).toMatch('run-node')
})
it('should match snapshot (Windows)', () => {
const script = getScript(rootDir, huskyDir, runNodePath, 'win32')
expect(script).toMatchSnapshot()
expect(script).not.toMatch('run-node')
})
})
+310
View File
@@ -0,0 +1,310 @@
import * as del from 'del'
import * as fs from 'fs'
import * as mkdirp from 'mkdirp'
import * as os from 'os'
import * as path from 'path'
import * as tempy from 'tempy'
import { install, uninstall } from '../'
import { huskyIdentifier } from '../getScript'
let tempDir
const pkg = JSON.stringify({})
function installFrom(
huskyDir: string,
requireRunNodePath?: string,
isCI = false
) {
install(
path.join(tempDir, huskyDir),
requireRunNodePath && path.join(tempDir, requireRunNodePath),
isCI
)
}
function uninstallFrom(dir: string) {
uninstall(path.join(tempDir, dir))
}
function mkdir(dir: string) {
mkdirp.sync(path.join(tempDir, dir))
}
function writeFile(filename: string, data: string) {
fs.writeFileSync(path.join(tempDir, filename), data)
}
function readFile(filename: string) {
return fs.readFileSync(path.join(tempDir, filename), 'utf-8')
}
function exists(filename: string) {
return fs.existsSync(path.join(tempDir, filename))
}
function expectHookToExist(filename: string) {
const hook = readFile(filename)
expect(hook).toMatch(huskyIdentifier)
}
describe('install', () => {
beforeEach(() => {
delete process.env.INIT_CWD
tempDir = tempy.directory()
})
afterEach(() => del(tempDir, { force: true }))
it('should install and uninstall', () => {
const huskyDir = 'node_modules/husky'
const hookFilename = '.git/hooks/pre-commit'
mkdir('.git/hooks')
mkdir(huskyDir)
writeFile('package.json', pkg)
installFrom(huskyDir)
expectHookToExist(hookFilename)
uninstallFrom(huskyDir)
expect(exists(hookFilename)).toBeFalsy()
})
it('should install and uninstall with pnpm', () => {
// pnpm installs husky in node_modules/.../node_modules/husky
// this tests ensures that husky will install from this path
process.env.INIT_CWD = tempDir
const huskyDir =
'node_modules/.registry.npmjs.org/husky/1.0.0/node_modules/husky'
const hookFilename = '.git/hooks/pre-commit'
mkdir('.git/hooks')
mkdir(huskyDir)
writeFile('package.json', pkg)
installFrom(huskyDir)
expectHookToExist(hookFilename)
uninstallFrom(huskyDir)
expect(exists(hookFilename)).toBeFalsy()
})
it('should update existing husky hooks', () => {
const huskyDir = 'node_modules/husky'
const hookFilename = '.git/hooks/pre-commit'
mkdir('.git/hooks')
mkdir(huskyDir)
writeFile('package.json', pkg)
// Create an existing husky hook
writeFile(hookFilename, '# husky\nfoo')
// Verify that it has been updated
installFrom(huskyDir)
const hook = readFile(hookFilename)
expect(hook).toContain('# husky')
expect(hook).not.toContain('foo')
})
it('should update existing husky hooks (v0.14 and earlier)', () => {
const huskyDir = 'node_modules/husky'
const hookFilename = '.git/hooks/pre-commit'
mkdir('.git/hooks')
mkdir(huskyDir)
writeFile('package.json', pkg)
// Create an existing husky hook
writeFile(hookFilename, '#!/bin/sh\n#husky 0.14.3\nfoo')
// Verify that it has been updated
installFrom(huskyDir)
const hook = readFile(hookFilename)
expect(hook).toContain('# husky')
expect(hook).not.toContain('foo')
})
it('should not modify user hooks', () => {
const huskyDir = 'node_modules/husky'
const hookFilename = '.git/hooks/pre-push'
mkdir('.git/hooks')
mkdir(huskyDir)
writeFile('package.json', pkg)
writeFile(hookFilename, 'foo')
// Verify that it's not overwritten
installFrom(huskyDir)
const hook = readFile(hookFilename)
expect(hook).toBe('foo')
// Verify that it's not deleted
uninstallFrom(huskyDir)
expect(exists(hookFilename)).toBeTruthy()
})
it('should support package.json installed in sub directory', () => {
mkdir('.git/hooks')
mkdir('A/B/node_modules/husky')
writeFile('A/B/package.json', pkg)
installFrom('A/B/node_modules/husky', 'A/B/node_modules/.bin/run-node')
const hook = readFile('.git/hooks/pre-commit')
const node =
os.platform() !== 'win32' ? 'A/B/node_modules/.bin/run-node' : 'node'
expect(hook).toMatch(node)
expect(hook).toMatch('A/B/node_modules/husky/lib/runner/bin.js')
uninstallFrom('A/B/node_modules/husky')
expect(exists('.git/hooks/pre-commit')).toBeFalsy()
})
it('should support git submodule', () => {
mkdir('.git/modules/A/B/hooks')
mkdir('A/B/node_modules/husky')
writeFile('A/B/package.json', pkg)
writeFile('A/B/.git', 'git: ../../.git/modules/A/B')
installFrom('A/B/node_modules/husky', 'A/B/node_modules/.bin/run-node')
const hook = readFile('.git/modules/A/B/hooks/pre-commit')
const node =
os.platform() !== 'win32' ? 'node_modules/.bin/run-node' : 'node'
expect(hook).toMatch(node)
expect(hook).toMatch('node_modules/husky/lib/runner/bin.js')
uninstallFrom('A/B/node_modules/husky')
expect(exists('.git/modules/A/B/hooks/pre-commit')).toBeFalsy()
})
it('should support git submodule and sub directory', () => {
mkdir('.git/modules/A/B/hooks')
mkdir('A/B/C/node_modules/husky')
writeFile('A/B/C/package.json', pkg)
writeFile('A/B/.git', 'git: ../../.git/modules/A/B')
installFrom('A/B/C/node_modules/husky', 'A/B/C/node_modules/.bin/run-node')
const hook = readFile('.git/modules/A/B/hooks/pre-commit')
const node =
os.platform() !== 'win32' ? 'C/node_modules/.bin/run-node' : 'node'
expect(hook).toMatch(node)
expect(hook).toMatch('C/node_modules/husky/lib/runner/bin.js')
uninstallFrom('A/B/C/node_modules/husky')
expect(exists('.git/hooks/pre-push')).toBeFalsy()
})
it('should support git worktrees', () => {
mkdir('.git/worktrees/B/hooks')
mkdir('A/B/node_modules/husky')
writeFile('A/B/package.json', pkg)
// Git path for worktrees is absolute
const absolutePath = path.join(tempDir, '.git/worktrees/B')
writeFile('A/B/.git', `git: ${absolutePath}`)
installFrom('A/B/node_modules/husky', 'A/B/node_modules/.bin/run-node')
const hook = readFile('.git/worktrees/B/hooks/pre-commit')
const node =
os.platform() !== 'win32' ? 'node_modules/.bin/run-node' : 'node'
expect(hook).toMatch(node)
expect(hook).toMatch('node_modules/husky/lib/runner/bin.js')
uninstallFrom('A/B/node_modules/husky')
expect(exists('.git/worktrees/B/hooks/pre-commit')).toBeFalsy()
})
it('should not install from /node_modules/A/node_modules', () => {
mkdir('.git/hooks')
mkdir('node_modules/A/node_modules/husky')
writeFile('node_modules/A/package.json', '{}')
writeFile('package.json', pkg)
installFrom('node_modules/A/node_modules/husky')
expect(exists('.git/hooks/pre-commit')).toBeFalsy()
})
it('should not install from node_modules using INIT_CWD', () => {
process.env.INIT_CWD = 'node_modules'
const huskyDir = 'node_modules/husky'
const hookFilename = '.git/hooks/pre-commit'
mkdir('.git/hooks')
writeFile('package.json', pkg)
installFrom(huskyDir)
expect(exists('.git/hooks/pre-commit')).toBeFalsy()
})
it('should migrate existing scripts (ghooks)', () => {
const huskyDir = 'node_modules/husky'
const hookFilename = '.git/hooks/pre-commit'
mkdir('.git/hooks')
mkdir(huskyDir)
writeFile('package.json', pkg)
writeFile(hookFilename, '// Generated by ghooks. Do not edit this file.')
installFrom(huskyDir)
const hook = readFile(hookFilename)
expect(hook).toMatch(huskyIdentifier)
})
it('should migrate existing scripts (pre-commit)', () => {
const huskyDir = 'node_modules/husky'
const hookFilename = '.git/hooks/pre-commit'
mkdir('.git/hooks')
mkdir(huskyDir)
writeFile('package.json', pkg)
writeFile(hookFilename, './node_modules/pre-commit/hook')
installFrom(huskyDir)
const hook = readFile(hookFilename)
expect(hook).toMatch(huskyIdentifier)
})
it('should not install hooks if HUSKY_SKIP_INSTALL=true', () => {
mkdir('.git/hooks')
mkdir('node_modules/husky')
writeFile('package.json', pkg)
process.env.HUSKY_SKIP_INSTALL = 'true'
installFrom('node_modules/husky')
delete process.env.HUSKY_SKIP_INSTALL
expect(exists('.git/hooks/pre-commit')).toBeFalsy()
})
it('should not install hooks in CI server by default', () => {
mkdir('.git/hooks')
mkdir('node_modules/husky')
writeFile('package.json', pkg)
const isCI = true
installFrom('node_modules/husky', undefined, isCI)
expect(exists('.git/hooks/pre-commit')).toBeFalsy()
})
it('should install in CI server if skipCI is set to false', () => {
mkdir('.git/hooks')
mkdir('node_modules/husky')
writeFile('package.json', JSON.stringify({ husky: { skipCI: false } }))
const isCI = true
installFrom('node_modules/husky', undefined, isCI)
expect(exists('.git/hooks/pre-commit')).toBeTruthy()
})
it("should not crash if .git/hooks doesn't exist", () => {
const huskyDir = 'node_modules/husky'
mkdir('.git')
mkdir(huskyDir)
writeFile('package.json', pkg)
expect(() => installFrom(huskyDir)).not.toThrow()
})
})
+25
View File
@@ -0,0 +1,25 @@
import * as isCI from 'is-ci'
import * as path from 'path'
import { install, uninstall } from './'
// Just for testing
if (process.env.HUSKY_DEBUG) {
console.log(process.env.INIT_CWD)
}
// Action can be "install" or "uninstall"
// huskyDir is ONLY used in dev, don't use this arguments
const [, , action, huskyDir = path.join(__dirname, '../..')] = process.argv
// Find Git dir
try {
// Run installer
if (action === 'install') {
install(huskyDir, undefined, isCI)
} else {
uninstall(huskyDir)
}
} catch (error) {
console.log(`husky > failed to ${action}`)
console.log(error.message)
}
+68
View File
@@ -0,0 +1,68 @@
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as slash from 'slash'
interface IContext {
node: string
platform: string
script: string
version: string
}
// Used to identify scripts created by Husky
export const huskyIdentifier = '# husky'
// Render script
const render = ({ node, platform, script, version }: IContext) => `#!/bin/sh
${huskyIdentifier}
# v${version} ${platform}
scriptPath="${script}.js"
hookName=\`basename "$0"\`
gitParams="$*"
${
platform !== 'win32'
? `
if ! command -v node >/dev/null 2>&1; then
echo "Can't find node in PATH, trying to find a node binary on your system"
fi
`
: ''
}
if [ -f $scriptPath ]; then
${node} $scriptPath $hookName "$gitParams"
else
echo "Can't find Husky, skipping $hookName hook"
echo "You can reinstall it using 'npm install husky --save-dev' or delete this hook"
fi
`
/**
* @param rootDir - e.g. /home/typicode/project/
* @param huskyDir - e.g. /home/typicode/project/node_modules/husky/
* @param requireRunNodePath - path to run-node resolved by require e.g. /home/typicode/project/node_modules/.bin/run-node
* @param platform - platform husky installer is running on (used to produce win32 specific script)
*/
export default function(
rootDir: string,
huskyDir: string,
requireRunNodePath: string,
// Additional param used for testing only
platform: string = os.platform()
) {
const runNodePath = slash(path.relative(rootDir, requireRunNodePath))
// On Windows do not rely on run-node
const node = platform === 'win32' ? 'node' : runNodePath
const { version } = JSON.parse(
fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8')
)
const script = slash(
path.join(path.relative(rootDir, huskyDir), 'lib/runner/bin')
)
return render({ node, platform, script, version })
}
+224
View File
@@ -0,0 +1,224 @@
import * as findUp from 'find-up'
import * as fs from 'fs'
import * as path from 'path'
import * as pkgDir from 'pkg-dir'
import getConf from '../getConf'
import getScript from './getScript'
import { isGhooks, isHusky, isPreCommit, isYorkie } from './is'
import resolveGitDir from './resolveGitDir'
const hookList = [
'applypatch-msg',
'pre-applypatch',
'post-applypatch',
'pre-commit',
'prepare-commit-msg',
'commit-msg',
'post-commit',
'pre-rebase',
'post-checkout',
'post-merge',
'pre-push',
'pre-receive',
'update',
'post-receive',
'post-update',
'push-to-checkout',
'pre-auto-gc',
'post-rewrite',
'sendemail-validate'
]
function writeHook(filename: string, script: string) {
fs.writeFileSync(filename, script, 'utf-8')
fs.chmodSync(filename, parseInt('0755', 8))
}
function createHook(filename: string, script: string) {
// Get name, used for logging
const name = path.basename(filename)
// Check if hook exist
if (fs.existsSync(filename)) {
const hook = fs.readFileSync(filename, 'utf-8')
// Migrate
if (isGhooks(hook)) {
console.log(`migrating existing ghooks script: ${name}`)
return writeHook(filename, script)
}
// Migrate
if (isPreCommit(hook)) {
console.log(`migrating existing pre-commit script: ${name}`)
return writeHook(filename, script)
}
// Update
if (isHusky(hook) || isYorkie(hook)) {
return writeHook(filename, script)
}
// Skip
console.log(`skipping existing user hook: ${name}`)
return
}
// Create hook if it doesn't exist
writeHook(filename, script)
}
function createHooks(filenames: string[], script: string) {
filenames.forEach(filename => createHook(filename, script))
}
function canRemove(filename: string): boolean {
if (fs.existsSync(filename)) {
const data = fs.readFileSync(filename, 'utf-8')
return isHusky(data)
}
return false
}
function removeHook(filename: string) {
fs.unlinkSync(filename)
}
function removeHooks(filenames: string[]) {
filenames.filter(canRemove).forEach(removeHook)
}
// This prevents the case where someone would want to debug a node_module that has
// husky as devDependency and run npm install from node_modules directory
function isInNodeModules(dir: string) {
// INIT_CWD holds the full path you were in when you ran npm install (supported also by yarn and pnpm)
// See https://docs.npmjs.com/cli/run-script
if (process.env.INIT_CWD) {
return process.env.INIT_CWD.indexOf('node_modules') !== -1
}
// Old technique
return (dir.match(/node_modules/g) || []).length > 1
}
function getHooks(gitDir: string): string[] {
const gitHooksDir = path.join(gitDir, 'hooks')
return hookList.map(hookName => path.join(gitHooksDir, hookName))
}
/**
* @param huskyDir - e.g. /home/typicode/project/node_modules/husky/
* @param requireRunNodePath - path to run-node resolved by require e.g. /home/typicode/project/node_modules/.bin/run-node
* @param isCI - true if running in CI
*/
export function install(
huskyDir: string,
requireRunNodePath: string = require.resolve('.bin/run-node'),
isCI: boolean
) {
console.log('husky > setting up git hooks')
// First directory containing user's package.json
const userPkgDir = pkgDir.sync(path.join(huskyDir, '..'))
// Get conf from package.json or .huskyrc
const conf = getConf(userPkgDir)
// Get directory containing .git directory or in the case of Git submodules, the .git file
const gitDirOrFile = findUp.sync('.git', { cwd: userPkgDir })
// Resolve git directory (e.g. .git/ or .git/modules/path/to/submodule)
const resolvedGitDir = resolveGitDir(userPkgDir)
// Checks
if (gitDirOrFile === null) {
console.log("Can't find .git, skipping Git hooks installation.")
console.log(
"Please check that you're in a cloned repository",
"or run 'git init' to create an empty Git repository and reinstall husky."
)
return
}
if (resolvedGitDir === null) {
console.log(
"Can't find resolved .git directory, skipping Git hooks installation."
)
return
}
if (process.env.HUSKY_SKIP_INSTALL === 'true') {
console.log(
"HUSKY_SKIP_INSTALL environment variable is set to 'true',",
'skipping Git hooks installation.'
)
return
}
if (isCI && conf.skipCI) {
console.log('CI detected, skipping Git hooks installation.')
return
}
if (userPkgDir === null) {
console.log("Can't find package.json, skipping Git hooks installation.")
console.log(
'Please check that your project has a package.json or create it and reinstall husky.'
)
return
}
if (isInNodeModules(huskyDir)) {
console.log(
'Trying to install from node_modules directory, skipping Git hooks installation.'
)
return
}
if (!fs.existsSync(path.join(resolvedGitDir, 'hooks'))) {
console.log(
`Can't find hooks directory in ${resolvedGitDir}, skipping Git hooks installation.`
)
console.log(
`Please create ${path.join(
resolvedGitDir,
'hooks'
)} directory and reinstall husky.`
)
return
}
// Create hooks
// Get root dir based on the first .git directory of file found
const rootDir = path.dirname(gitDirOrFile)
const hooks = getHooks(resolvedGitDir)
const script = getScript(rootDir, huskyDir, requireRunNodePath)
createHooks(hooks, script)
console.log(`husky > done`)
}
export function uninstall(huskyDir: string) {
console.log('husky > uninstalling git hooks')
const userPkgDir = pkgDir.sync(path.join(huskyDir, '..'))
const resolvedGitDir = resolveGitDir(userPkgDir)
if (resolvedGitDir === null) {
console.log(
"Can't find resolved .git directory, skipping Git hooks uninstallation."
)
return
}
if (isInNodeModules(huskyDir)) {
console.log(
'Trying to uninstall from node_modules directory, skipping Git hooks uninstallation.'
)
return
}
// Remove hooks
const hooks = getHooks(resolvedGitDir)
removeHooks(hooks)
console.log('husky > done')
}
+24
View File
@@ -0,0 +1,24 @@
import { huskyIdentifier } from './getScript'
export function isHusky(data: string): boolean {
// v0.14 and prior used #husky as an identifier.
// Just in case some previous hooks weren't correctly uninstalled,
// and for a better transition this will allow v0.15+ to uninstall them as well.
const previousHuskyIdentifier = '#husky'
return (
data.indexOf(huskyIdentifier) !== -1 ||
data.indexOf(previousHuskyIdentifier) !== -1
)
}
export function isYorkie(data: string): boolean {
return data.indexOf('#yorkie') !== -1
}
export function isGhooks(data: string): boolean {
return data.indexOf('// Generated by ghooks. Do not edit this file.') !== -1
}
export function isPreCommit(data: string): boolean {
return data.indexOf('./node_modules/pre-commit/hook') !== -1
}
+30
View File
@@ -0,0 +1,30 @@
import * as findUp from 'find-up'
import * as fs from 'fs'
import * as path from 'path'
export default function(cwd: string): string | null {
const foundPath = findUp.sync('.git', { cwd })
if (foundPath) {
const stats = fs.lstatSync(foundPath)
// If it's a .git file resolve path
if (stats.isFile()) {
// Expect following format
// git: pathToGit
// On Windows pathToGit can contain ':' (example "gitdir: C:/Some/Path")
const gitFileData = fs.readFileSync(foundPath, 'utf-8')
const resolvedGitDir = gitFileData
.split(':')
.slice(1)
.join(':')
.trim()
return path.resolve(path.dirname(foundPath), resolvedGitDir)
}
// Else return path to .git directory
return foundPath
}
return null
}
+190
View File
@@ -0,0 +1,190 @@
import * as execa from 'execa'
import * as fs from 'fs'
import * as mkdirp from 'mkdirp'
import * as path from 'path'
import * as tempy from 'tempy'
import index from '../'
let spy
function getScriptPath(dir) {
return path.join(dir, 'node_modules/husky/runner/index.js')
}
describe('run', () => {
beforeEach(() => {
spy = jest.spyOn(execa, 'shellSync')
})
afterEach(() => {
spy.mockReset()
spy.mockRestore()
})
it('should run working command and return 0 status', async () => {
const dir = tempy.directory()
fs.writeFileSync(
path.join(dir, 'package.json'),
JSON.stringify({
husky: {
hooks: {
'pre-commit': 'echo success'
}
}
})
)
const status = await index([, getScriptPath(dir), 'pre-commit'])
expect(execa.shellSync).toHaveBeenCalledWith('echo success', {
cwd: dir,
env: {},
stdio: 'inherit'
})
expect(status).toBe(0)
})
it('should run working command and return 0 status when husky is installed in a sub directory', async () => {
const dir = tempy.directory()
const subDir = path.join(dir, 'A/B')
mkdirp.sync(subDir)
fs.writeFileSync(
path.join(subDir, 'package.json'),
JSON.stringify({
husky: {
hooks: {
'pre-commit': 'echo success'
}
}
})
)
const status = await index([, getScriptPath(subDir), 'pre-commit'])
expect(execa.shellSync).toHaveBeenCalledWith('echo success', {
cwd: subDir,
env: {},
stdio: 'inherit'
})
expect(status).toBe(0)
})
// This shouldn't happen since the shell script checks for command existence
// but in case there's a false positive, we're testing this also
it('should return 0 status if the command is undefined', async () => {
const dir = tempy.directory()
fs.writeFileSync(
path.join(dir, 'package.json'),
JSON.stringify({
husky: {}
})
)
const status = await index([, getScriptPath(dir), 'pre-commit'])
expect(execa.shellSync).not.toBeCalled()
expect(status).toBe(0)
})
it('should run failing command and return 1 status', async () => {
const dir = tempy.directory()
fs.writeFileSync(
path.join(dir, 'package.json'),
JSON.stringify({
husky: {
hooks: {
'pre-commit': 'echo fail && exit 2'
}
}
})
)
const status = await index([, getScriptPath(dir), 'pre-commit'])
expect(execa.shellSync).toHaveBeenCalledWith('echo fail && exit 2', {
cwd: dir,
env: {},
stdio: 'inherit'
})
expect(status).toBe(2)
})
it('should support old scripts but show a deprecated message', async () => {
const dir = tempy.directory()
fs.writeFileSync(
path.join(dir, 'package.json'),
JSON.stringify({
scripts: {
precommit: 'echo success'
}
})
)
const status = await index([, getScriptPath(dir), 'pre-commit'])
expect(execa.shellSync).toHaveBeenCalledWith('echo success', {
cwd: dir,
env: {},
stdio: 'inherit'
})
expect(status).toBe(0)
})
it('should set HUSKY_GIT_STDIN env for some hooks', async () => {
const dir = tempy.directory()
fs.writeFileSync(
path.join(dir, 'package.json'),
JSON.stringify({
husky: {
hooks: {
'pre-push': 'echo success'
}
}
})
)
const status = await index([, getScriptPath(dir), 'pre-push'], () =>
Promise.resolve('foo')
)
expect(execa.shellSync).toHaveBeenCalledWith('echo success', {
cwd: dir,
env: {
HUSKY_GIT_STDIN: 'foo'
},
stdio: 'inherit'
})
expect(status).toBe(0)
})
it('should set HUSKY_GIT_PARAMS', async () => {
const dir = tempy.directory()
fs.writeFileSync(
path.join(dir, 'package.json'),
JSON.stringify({
husky: {
hooks: {
'commit-msg': 'echo success'
}
}
})
)
// commit-msg takes one parameter from git
const status = await index([
,
getScriptPath(dir),
'commit-msg',
'git fake param'
])
expect(execa.shellSync).toHaveBeenCalledWith('echo success', {
cwd: dir,
env: {
HUSKY_GIT_PARAMS: 'git fake param'
},
stdio: 'inherit'
})
expect(status).toBe(0)
})
})
+8
View File
@@ -0,0 +1,8 @@
import index from './'
index(process.argv)
.then(status => process.exit(status))
.catch(err => {
console.log('Husky > unexpected error', err)
process.exit(1)
})
+83
View File
@@ -0,0 +1,83 @@
import * as execa from 'execa'
import * as getStdin from 'get-stdin'
import * as path from 'path'
import * as readPkg from 'read-pkg'
import getConf from '../getConf'
export interface IEnv extends NodeJS.ProcessEnv {
HUSKY_GIT_STDIN?: string
HUSKY_GIT_PARAMS?: string
}
/**
* @param argv - process.argv
*/
export default async function run(
[, scriptPath, hookName = '', HUSKY_GIT_PARAMS]: string[],
getStdinFn = getStdin // Used for mocking
): Promise<number> {
const cwd = path.resolve(scriptPath.split('node_modules')[0])
const pkg = readPkg.sync({ cwd, normalize: false })
const config = getConf(cwd)
const command: string | undefined =
config && config.hooks && config.hooks[hookName]
const oldCommand: string | undefined =
pkg && pkg.scripts && pkg.scripts[hookName.replace('-', '')]
try {
const env: IEnv = {}
if (HUSKY_GIT_PARAMS) {
env.HUSKY_GIT_PARAMS = HUSKY_GIT_PARAMS
}
if (
['pre-push', 'pre-receive', 'post-receive', 'post-rewrite'].includes(
hookName
)
) {
env.HUSKY_GIT_STDIN = await getStdinFn()
}
if (command) {
console.log(`husky > ${hookName} (node ${process.version})`)
execa.shellSync(command, { cwd, env, stdio: 'inherit' })
return 0
}
if (oldCommand) {
console.log()
console.log(
`Warning: Setting ${hookName} script in package.json > scripts will be deprecated`
)
console.log(
`Please move it to husky.hooks in package.json, a .huskyrc file, or a husky.config.js file`
)
console.log(
`Or run ./node_modules/.bin/husky-upgrade for automatic update`
)
console.log()
console.log(`See https://github.com/typicode/husky for usage`)
console.log()
console.log(`husky > ${hookName} (node ${process.version})`)
execa.shellSync(oldCommand, { cwd, env, stdio: 'inherit' })
return 0
}
return 0
} catch (err) {
const noVerifyMessage = [
'commit-msg',
'pre-commit',
'pre-rebase',
'pre-push'
].includes(hookName)
? '(add --no-verify to bypass)'
: '(cannot be bypassed with --no-verify due to Git specs)'
console.log(`husky > ${hookName} hook failed ${noVerifyMessage}`)
return err.code
}
}
-29
View File
@@ -1,29 +0,0 @@
'use strict'
const fs = require('fs')
const hooks = require('./hooks.json')
const findHooksDir = require('./utils/find-hooks-dir')
const is = require('./utils/is')
function removeHook(dir, name) {
const filename = `${dir}/${name}`
if (fs.existsSync(filename) && is.husky(filename)) {
fs.unlinkSync(`${dir}/${name}`)
}
}
function uninstallFrom(huskyDir) {
try {
const hooksDir = findHooksDir(huskyDir)
hooks.forEach(function(hookName) {
removeHook(hooksDir, hookName)
})
console.log('done\n')
} catch (e) {
console.error(e)
}
}
module.exports = uninstallFrom
+32
View File
@@ -0,0 +1,32 @@
import * as fs from 'fs'
import * as path from 'path'
import * as tempy from 'tempy'
import index from '../'
describe('upgrade', () => {
it('should run working command and return 0 status', () => {
const dir = tempy.directory()
const filename = path.join(dir, 'package.json')
fs.writeFileSync(
filename,
JSON.stringify({
scripts: {
precommit: 'npm test'
}
}),
'utf-8'
)
index(dir)
expect(JSON.parse(fs.readFileSync(filename, 'utf-8'))).toEqual({
husky: {
hooks: {
'pre-commit': 'npm test'
}
},
scripts: {}
})
})
})
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env node
import index from './index'
index(process.cwd())
+63
View File
@@ -0,0 +1,63 @@
import * as fs from 'fs'
import * as path from 'path'
import * as readPkg from 'read-pkg'
interface IHookMap {
[key: string]: string
}
const hookList: IHookMap = {
applypatchmsg: 'applypatch-msg',
commitmsg: 'commit-msg',
postapplypatch: 'post-applypatch',
postcheckout: 'post-checkout',
postcommit: 'post-commit',
postmerge: 'post-merge',
postreceive: 'post-receive',
postrewrite: 'post-rewrite',
postupdate: 'post-update',
preapplypatch: 'pre-applypatch',
preautogc: 'pre-auto-gc',
precommit: 'pre-commit',
preparecommitmsg: 'prepare-commit-msg',
prepush: 'pre-push',
prerebase: 'pre-rebase',
prereceive: 'pre-receive',
pushtocheckout: 'push-to-checkout',
sendemailvalidate: 'sendemail-validate',
update: 'update'
}
export default function upgrade(cwd: string) {
const pkgFile = path.join(cwd, 'package.json')
if (fs.existsSync(pkgFile)) {
const pkg = readPkg.sync({ cwd, normalize: false })
console.log(`husky > upgrading ${pkgFile}`)
// Don't overwrite pkg.husky if it exists
if (pkg.husky) {
return console.log(
`husky field in package.json isn't empty, skipping automatic upgrade`
)
}
// Create empty husky.hooks field
pkg.husky = { hooks: {} }
// Loop trhough hooks and move them to husky.hooks
Object.keys(hookList).forEach(name => {
const script = pkg.scripts[name]
if (script) {
delete pkg.scripts[name]
const newName = hookList[name]
pkg.husky.hooks[newName] = script
console.log(`moved scripts.${name} to husky.hooks.${newName}`)
}
})
// Update package.json
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2), 'utf-8')
console.log(`husky > done`)
}
}
-30
View File
@@ -1,30 +0,0 @@
'use strict'
const fs = require('fs')
const path = require('path')
const findParent = require('./find-parent')
function findHooksDir(dirname) {
const dir = findParent(dirname, '.git')
if (dir) {
let gitDir = path.join(dir, '.git')
const stats = fs.lstatSync(gitDir)
if (stats.isFile()) {
// Expect following format
// git: pathToGit
// On Windows pathToGit can contain ':' (example "gitdir: C:/Some/Path")
const gitFileData = fs.readFileSync(gitDir, 'utf-8')
gitDir = gitFileData
.split(':')
.slice(1)
.join(':')
.trim()
}
return path.resolve(dir, gitDir, 'hooks')
}
}
module.exports = findHooksDir
-16
View File
@@ -1,16 +0,0 @@
'use strict'
const fs = require('fs')
const path = require('path')
module.exports = function findParent(currentDir, name) {
const dirs = currentDir.split(path.sep)
while (dirs.pop()) {
const dir = dirs.join(path.sep)
if (fs.existsSync(path.join(dir, name))) {
return path.resolve(dir)
}
}
}
-109
View File
@@ -1,109 +0,0 @@
'use strict'
const normalize = require('normalize-path')
const stripIndent = require('strip-indent')
const pkg = require('../../package.json')
function platformSpecific() {
// On OS X and Linux, try to use nvm if it's installed
if (process.platform === 'win32') {
// Add
// Node standard installation path /c/Program Files/nodejs
// for GUI apps
// https://github.com/typicode/husky/issues/49
return stripIndent(
`
# Node standard installation
export PATH="$PATH:/c/Program Files/nodejs"`
)
} else {
// Using normalize to support ' in path
// https://github.com/typicode/husky/issues/117
const home = normalize(process.env.HOME)
return stripIndent(
`
# Add common path where Node can be found
# Brew standard installation path /usr/local/bin
# Node standard installation path /usr/local
export PATH="$PATH:/usr/local/bin:/usr/local"
# Try to load nvm using path of standard installation
load_nvm ${home}/.nvm
run_nvm`
)
return arr.join('\n')
}
}
module.exports = function getHookScript(hookName, relativePath, npmScriptName) {
// On Windows normalize path (i.e. convert \ to /)
const normalizedPath = normalize(relativePath)
const noVerifyMessage =
hookName === 'prepare-commit-msg'
? '(cannot be bypassed with --no-verify due to Git specs)'
: '(add --no-verify to bypass)'
return [
stripIndent(
`
#!/bin/sh
#husky ${pkg.version}
command_exists () {
command -v "$1" >/dev/null 2>&1
}
has_hook_script () {
[ -f package.json ] && cat package.json | grep -q "\\"$1\\"[[:space:]]*:"
}
# OS X and Linux only
load_nvm () {
# If nvm is not loaded, load it
command_exists nvm || {
export NVM_DIR="$1"
[ -s "$1/nvm.sh" ] && . "$1/nvm.sh"
}
}
# OS X and Linux only
run_nvm () {
# If nvm has been loaded correctly, use project .nvmrc
command_exists nvm && [ -f .nvmrc ] && nvm use
}
cd "${normalizedPath}"
# Check if ${npmScriptName} script is defined, skip if not
has_hook_script ${npmScriptName} || exit 0`
).trim(),
platformSpecific(),
stripIndent(
`
# Check that npm exists
command_exists npm || {
echo >&2 "husky > can't find npm in PATH, skipping ${npmScriptName} script in package.json"
exit 0
}
# Export Git hook params
export GIT_PARAMS="$*"
# Run npm script
echo "husky > npm run -s ${npmScriptName} (node \`node -v\`)"
echo
npm run -s ${npmScriptName} || {
echo
echo "husky > ${hookName} hook failed ${noVerifyMessage}"
exit 1
}
`
)
].join('\n')
}
-28
View File
@@ -1,28 +0,0 @@
'use strict'
const fs = require('fs')
function readFile(filename) {
return fs.readFileSync(filename, 'utf-8')
}
function husky(filename) {
const data = readFile(filename)
return data.indexOf('#husky') !== -1
}
function ghooks(filename) {
const data = readFile(filename)
return data.indexOf('// Generated by ghooks. Do not edit this file.') !== -1
}
function preCommit(filename) {
const data = readFile(filename)
return data.indexOf('./node_modules/pre-commit/hook') !== -1
}
module.exports = {
husky,
ghooks,
preCommit
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2016",
"module": "commonjs",
"outDir": "./lib",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
},
"exclude": [
"**/__tests__/*"
]
}
+21
View File
@@ -0,0 +1,21 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended",
"tslint-config-prettier"
],
"jsRules": {},
"rulesDirectory": [
"tslint-plugin-prettier"
],
"rules": {
"prettier": [
true,
{
"semi": false,
"singleQuote": true
}
],
"no-console": false
}
}
+17
View File
@@ -0,0 +1,17 @@
declare module 'cosmiconfig' {
export = Cosmiconfig;
}
declare function Cosmiconfig(
moduleName: string
):Cosmiconfig.Explorer;
declare namespace Cosmiconfig {
export interface Explorer {
searchSync(searchFrom?: string): Cosmiconfig.Result | null;
}
export interface Result {
config: any;
}
}