├── .gitignore ├── LICENSE ├── README.md ├── git-dir.js ├── install.js ├── null.js ├── package.json ├── screenshot.png └── uninstall.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Vercel, Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @vercel/git-hooks 2 | 3 | No nonsense [Git hook](https://git-scm.com/docs/githooks) management. 4 | 5 | ![](screenshot.png) 6 | 7 | ## Usage 8 | 9 | Install this module, preferably as a dev-dependency: 10 | 11 | ```console 12 | yarn add --dev @vercel/git-hooks 13 | ``` 14 | 15 | That's it. You can now use the module in two ways: 16 | 17 | ```json 18 | { 19 | "scripts": { 20 | "git-pre-commit": "eslint" 21 | } 22 | } 23 | ``` 24 | 25 | The above will run a single command line, just like running `npm run git-pre-commit` or `yarn git-pre-commit`, 26 | every time you `git commit`. 27 | 28 | Alternatively, if you'd like to run several scripts in succession upon a hook, you may define a `git` top-level 29 | property and specify an array of scripts to run: 30 | 31 | ```json 32 | { 33 | "git": { 34 | "pre-commit": "lint" 35 | } 36 | } 37 | ``` 38 | 39 | or 40 | 41 | ```json 42 | { 43 | "git": { 44 | "pre-commit": ["lint", "test"] 45 | } 46 | } 47 | ``` 48 | 49 | Note that any `"scripts"` hooks supplant any corresponding `"git"` hooks. That is to say, if you define both a 50 | `{"scripts": {"git-pre-commit": "..."}}` hook and a `{"git": {"pre-commit": []}}` hook, the hook in `"scripts"` 51 | will be the only hook that is executed. 52 | 53 | ## Why? There are hundreds of these. 54 | 55 | - No dependencies 56 | - Supports NPM, Yarn, <insert package manager> - this package will detect and use whatever package manager you installed it with* 57 | - Tiny footprint - two script files and a couple of symlinks 58 | - Existing hook / anti-overwrite checks are very reliable since two proprietary scripts are added and all of 'our' hooks are just symlinks 59 | to those, so there's virtually no way the uninstall script will mistake a pre-existing hook for its own 60 | 61 | > *Caveat: The package manager needs to be npm compliant in terms of environment variables. 62 | > Worst case, define the environment variables `npm_node_execpath` (node binary) and `npm_execpath` (package manager entry point) 63 | > as environment variables prior to installing. 64 | 65 | # License 66 | Copyright © 2021 by Vercel, Inc. 67 | 68 | Released under the [MIT License](LICENSE). 69 | -------------------------------------------------------------------------------- /git-dir.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function getPathDir() { 4 | if (process.env.INIT_CWD) { 5 | return process.env.INIT_CWD; 6 | } 7 | 8 | const thisPkgName = require(path.join(__dirname, 'package.json')).name; 9 | const pkgNamePath = path.join('node_modules', path.format(path.posix.parse(thisPkgName))); 10 | const paths = process.env.PATH 11 | .split(path.delimiter) 12 | .filter(p => p.toLowerCase().indexOf(pkgNamePath) !== -1); 13 | 14 | if (paths.length === 0) { 15 | // Last-ditch attempt... 16 | return process.cwd(); 17 | } 18 | 19 | return paths[0]; 20 | } 21 | 22 | function detectGitDir() { 23 | let cur = getPathDir(); 24 | let lastCur; 25 | let lastFound = cur; 26 | 27 | do { 28 | lastCur = cur; 29 | 30 | if (path.basename(cur) === 'node_modules') { 31 | lastFound = path.dirname(cur); 32 | } 33 | 34 | cur = path.dirname(cur); 35 | } while (cur !== lastCur); 36 | 37 | return path.join(lastFound, '.git'); 38 | } 39 | 40 | module.exports = detectGitDir; 41 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const detectGitDir = require('./git-dir'); 5 | 6 | // Hard to catch this output if there's no new-line after the script runs. 7 | process.on('exit', () => console.log()); 8 | 9 | function env(name) { 10 | if (!process.env[name]) { 11 | throw new Error(`Required environment variable is empty or not defined - are you using an NPM-compatible package manager?: ${name}`); 12 | } 13 | 14 | return process.env[name]; 15 | } 16 | 17 | const nodeBin = env('npm_node_execpath'); 18 | const packageManagerBin = env('npm_execpath'); 19 | 20 | const gitDir = detectGitDir(); 21 | 22 | if (!gitDir || !fs.existsSync(gitDir) || !fs.statSync(gitDir).isDirectory()) { 23 | console.error('△ @vercel/git-hooks: .git directory not found or is not a directory; ignoring Git hook installation:', gitDir || 'reached filesystem boundary (root or drive)'); 24 | process.exit(0); 25 | } 26 | 27 | const hooksDir = path.join(gitDir, 'hooks'); 28 | 29 | const hook = `#!/usr/bin/env bash 30 | # !! AUTO-GENERATED FILE !! 31 | # Do not edit manually. This file was generated by @vercel/git-hooks, located at ${__dirname}. 32 | 33 | # These values were generated at install-time. 34 | # If they are incorrect, either re-install the package's dependencies (by 35 | # wiping node_modules), or submitting an issue at https://github.com/vercel/git-hooks/issues 36 | # for any incorrect detection functionality. 37 | 38 | set -euo pipefail 39 | 40 | if [ ! -z "\${ZEIT_GITHOOKS_RUNNING-}" ]; then 41 | echo "△ ERROR: Refusing to run a nested git hook for fear of an endless loop." >&2 42 | echo " You may \`unset ZEIT_GITHOOKS_RUNNING\` prior to running a nested" >&2 43 | echo " Git command." >&2 44 | echo >&2 45 | exit 1 46 | fi 47 | 48 | export ZEIT_GITHOOKS_RUNNING=1 49 | 50 | arg0="$(basename \${0})" 51 | 52 | if [[ "\${arg0}" == "_do_hook.cjs" ]]; then 53 | echo "You probably didn't mean to call this directly." >&2 54 | exit 2 55 | fi 56 | 57 | "${nodeBin}" "$(dirname "\${0}")/_detect_package_hooks.cjs" "\${arg0}" | while IFS='' read -r hook || [[ -n "\${hook}" ]]; do 58 | echo "△ run hook: \${arg0} -> \${hook}" >&2 59 | if [[ $# -gt 0 ]]; then 60 | "${nodeBin}" "${packageManagerBin}" run "\${hook}" -- "$@" 61 | else 62 | "${nodeBin}" "${packageManagerBin}" run "\${hook}" 63 | fi 64 | echo >&2 65 | done 66 | `; 67 | 68 | const hookDetector = ` 69 | const fs = require('fs'); 70 | const path = require('path'); 71 | 72 | const hook = process.argv[2]; 73 | const scriptHook = \`git-\${hook}\`; 74 | 75 | const packagePath = path.join(__dirname, '../../package.json'); 76 | if (fs.existsSync(packagePath) && fs.statSync(packagePath).isFile()) { 77 | const pkg = require(packagePath); 78 | const hooks = []; 79 | 80 | if (pkg && typeof pkg.scripts === 'object' && scriptHook in pkg.scripts) { 81 | hooks.push(scriptHook); 82 | } else if (pkg && typeof pkg.git === 'object' && hook in pkg.git) { 83 | if (typeof pkg.git[hook] === 'string') { 84 | hooks.push(pkg.git[hook]); 85 | } else if (Array.isArray(pkg.git[hook])) { 86 | hooks.push(...pkg.git[hook]); 87 | } 88 | } 89 | 90 | if (hooks.length > 0) { 91 | console.log(hooks.join('\\n')); 92 | } 93 | } 94 | `; 95 | 96 | // Create hooks directory if necessary 97 | if (!fs.existsSync(hooksDir)) { 98 | fs.mkdirSync(hooksDir); 99 | } else if (!fs.statSync(hooksDir).isDirectory()) { 100 | throw new Error(`@vercel/git-hooks: .git/hooks directory exists but is not a directory - this is most likely an error on your part: ${hooksDir}`); 101 | } 102 | 103 | // Write the hook and detector script 104 | function writeExecutable(path, ...args) { 105 | fs.writeFileSync(path, ...args); 106 | fs.chmodSync(path, 0o755); 107 | } 108 | 109 | console.error(`△ @vercel/git-hooks: installing base hook to ${hooksDir}`); 110 | writeExecutable(path.join(hooksDir, '_do_hook.cjs'), hook, 'utf-8'); 111 | writeExecutable(path.join(hooksDir, '_detect_package_hooks.cjs'), hookDetector, 'utf-8'); 112 | 113 | // Populate each of the hooks 114 | function installHook(name) { 115 | const hookPath = path.join(hooksDir, name); 116 | 117 | if (fs.existsSync(hookPath)) { 118 | console.error(`△ @vercel/git-hooks: hook '${name}' already exists; skipping: ${hookPath}`); 119 | return; 120 | } 121 | 122 | fs.symlinkSync('./_do_hook.cjs', hookPath); 123 | } 124 | 125 | [ 126 | 'applypatch-msg', 127 | 'pre-applypatch', 128 | 'post-applypatch', 129 | 'pre-commit', 130 | 'prepare-commit-msg', 131 | 'commit-msg', 132 | 'post-commit', 133 | 'pre-rebase', 134 | 'post-checkout', 135 | 'post-merge', 136 | 'pre-push', 137 | 'pre-receive', 138 | 'update', 139 | 'post-receive', 140 | 'post-update', 141 | 'push-to-checkout', 142 | 'pre-auto-gc', 143 | 'post-rewrite', 144 | 'rebase', 145 | 'sendemail-validate' 146 | ].forEach(installHook); 147 | 148 | console.error('△ @vercel/git-hooks: hooks installed successfully'); 149 | -------------------------------------------------------------------------------- /null.js: -------------------------------------------------------------------------------- 1 | // Intentionally empty 2 | // 3 | // This file is required in order to make require.resolve() work, 4 | // which is a requirement of some helper scripts that check 5 | // for the existence of @vercel/git-hooks being installed. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vercel/git-hooks", 3 | "version": "1.0.0", 4 | "description": "No nonsense Git hook management", 5 | "repository": "vercel/git-hooks", 6 | "author": "Josh Junon ", 7 | "license": "MIT", 8 | "main": "null.js", 9 | "scripts": { 10 | "install": "node ./install.js", 11 | "uninstall": "node ./uninstall.js" 12 | }, 13 | "files": [ 14 | "install.js", 15 | "uninstall.js", 16 | "git-dir.js", 17 | "null.js" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/git-hooks/9d2d0fb48ee3478112327990fefecb0a5e455f2c/screenshot.png -------------------------------------------------------------------------------- /uninstall.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const detectGitDir = require('./git-dir'); 5 | 6 | // Hard to catch this output if there's no new-line after the script runs. 7 | process.on('exit', () => console.log()); 8 | 9 | function env(name) { 10 | if (!process.env[name]) { 11 | throw new Error(`Required environment variable is empty or not defined - are you using an NPM-compatible package manager?: ${name}`); 12 | } 13 | 14 | return process.env[name]; 15 | } 16 | 17 | const nodeBin = env('npm_node_execpath'); 18 | const packageManagerBin = env('npm_execpath'); 19 | 20 | const gitDir = detectGitDir(); 21 | 22 | if (!gitDir || !fs.existsSync(gitDir) || !fs.statSync(gitDir).isDirectory()) { 23 | console.error('△ @vercel/git-hooks: .git/hooks directory not found or is not a directory; ignoring Git hook uninstallation:', gitDir || 'reached filesystem boundary (root or drive)'); 24 | process.exit(0); 25 | } 26 | 27 | const hooksDir = path.join(gitDir, 'hooks'); 28 | 29 | // Uninstall each of the hooks 30 | function uninstallHook(name) { 31 | const hookPath = path.join(hooksDir, name); 32 | 33 | if (!fs.existsSync(hookPath)) { 34 | return; 35 | } 36 | 37 | const isOneOfOurs = fs.lstatSync(hookPath).isSymbolicLink() && fs.readlinkSync(hookPath).match(/\.\/_do_hook(\.cjs)?/); 38 | 39 | if (!isOneOfOurs) { 40 | console.error(`△ @vercel/git-hooks: hook '${name}' appears to be a user-defined hook; skipping: ${hookPath}`); 41 | return; 42 | } 43 | 44 | fs.unlinkSync(hookPath); 45 | } 46 | 47 | [ 48 | 'applypatch-msg', 49 | 'pre-applypatch', 50 | 'post-applypatch', 51 | 'pre-commit', 52 | 'prepare-commit-msg', 53 | 'commit-msg', 54 | 'post-commit', 55 | 'pre-rebase', 56 | 'post-checkout', 57 | 'post-merge', 58 | 'pre-push', 59 | 'pre-receive', 60 | 'update', 61 | 'post-receive', 62 | 'post-update', 63 | 'push-to-checkout', 64 | 'pre-auto-gc', 65 | 'post-rewrite', 66 | 'rebase', 67 | 'sendemail-validate' 68 | ].forEach(uninstallHook); 69 | 70 | function removeIfExists(path) { 71 | if (fs.existsSync(path)) { 72 | fs.unlinkSync(path); 73 | } 74 | } 75 | 76 | removeIfExists(path.join(hooksDir, '_do_hook.cjs')); 77 | removeIfExists(path.join(hooksDir, '_detect_package_hooks.cjs')); 78 | // XXX The following are legacy paths that should also be removed 79 | // XXX if they are found. This will help migration to the new commonjs 80 | // XXX extenions. See https://github.com/vercel/git-hooks/pull/7. 81 | // XXX These checks will most likely be removed at a later date. 82 | removeIfExists(path.join(hooksDir, '_do_hook')); 83 | removeIfExists(path.join(hooksDir, '_detect_package_hooks')); 84 | 85 | console.error('△ @vercel/git-hooks: hooks uninstalled successfully'); 86 | --------------------------------------------------------------------------------