├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── lint.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── FUNDING.json ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── _tests ├── project_with_configuration_in_alternative_separate_cjs │ ├── .simple-git-hooks.cjs │ └── package.json ├── project_with_configuration_in_alternative_separate_js │ ├── .simple-git-hooks.js │ └── package.json ├── project_with_configuration_in_alternative_separate_json │ ├── .simple-git-hooks.json │ └── package.json ├── project_with_configuration_in_alternative_separate_mjs │ ├── .simple-git-hooks.mjs │ └── package.json ├── project_with_configuration_in_package_json │ ├── initrc_that_does_nothing.sh │ ├── initrc_that_prevents_hook_fail.sh │ └── package.json ├── project_with_configuration_in_separate_cjs │ ├── package.json │ └── simple-git-hooks.cjs ├── project_with_configuration_in_separate_js │ ├── package.json │ └── simple-git-hooks.js ├── project_with_configuration_in_separate_json │ ├── package.json │ └── simple-git-hooks.json ├── project_with_configuration_in_separate_mjs │ ├── package.json │ └── simple-git-hooks.mjs ├── project_with_custom_configuration │ ├── git-hooks.js │ └── package.json ├── project_with_incorrect_configuration_in_package_json │ ├── package.json │ └── simple-git-hooks.json ├── project_with_simple_git_hooks_in_deps │ └── package.json ├── project_with_simple_git_hooks_in_dev_deps │ └── package.json ├── project_with_unused_configuration_in_package_json │ └── package.json ├── project_without_configuration │ └── package.json └── project_without_simple_git_hooks │ └── package.json ├── cli.js ├── package.json ├── postinstall.js ├── simple-git-hooks.js ├── simple-git-hooks.test.js ├── tea.yaml ├── uninstall.js └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "toplenboren/simple-git-hooks" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "master", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://EditorConfig.org 2 | 3 | root = true 4 | 5 | # Unix-style newlines with a newline ending every file 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .git 4 | package-lock.json 5 | package.json 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": 12 11 | }, 12 | "overrides": [ 13 | { 14 | "files": ["*.mjs"], 15 | "parserOptions": { 16 | "sourceType": "module" 17 | } 18 | } 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '28 7 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Lint 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18.x 20 | cache: yarn 21 | - run: yarn --frozen-lockfile --ignore-engines 22 | - run: yarn lint 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@v4 19 | with: 20 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 21 | fetch-depth: 0 22 | 23 | - name: Setup Node.js LTS 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: lts/* 27 | cache: yarn 28 | 29 | - name: Install Dependencies 30 | run: yarn --frozen-lockfile 31 | 32 | - name: Create Release Pull Request or Publish to npm 33 | id: changesets 34 | uses: changesets/action@v1 35 | with: 36 | commit: 'chore: release simple-git-hooks' 37 | title: 'chore: release simple-git-hooks' 38 | publish: yarn release 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Lint 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18.x 20 | cache: yarn 21 | - run: yarn --frozen-lockfile --ignore-engines 22 | - run: yarn test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.13.0 4 | 5 | ### Minor Changes 6 | 7 | - [#121](https://github.com/toplenboren/simple-git-hooks/pull/121) [`d9d7823`](https://github.com/toplenboren/simple-git-hooks/commit/d9d7823b8cef0a91a5aef37c2d5a9b913b61c1a0) Thanks [@chouchouji](https://github.com/chouchouji)! - feat: only remove some hooks that are not in `preserveUnused` option 8 | 9 | - [#125](https://github.com/toplenboren/simple-git-hooks/pull/125) [`8486a22`](https://github.com/toplenboren/simple-git-hooks/commit/8486a2211340fcf14f6b534c862fb000961be115) Thanks [@nathanwhit](https://github.com/nathanwhit)! - feat: support `deno`'s `node_modules` structure 10 | 11 | - [#127](https://github.com/toplenboren/simple-git-hooks/pull/127) [`8bb9818`](https://github.com/toplenboren/simple-git-hooks/commit/8bb9818876f11a2295ea8f80f666a5ee8e8ae13a) Thanks [@yyz945947732](https://github.com/yyz945947732)! - feat: optimize the migration experience from `husky` 12 | 13 | ## 2.12.1 14 | 15 | ### Minor Changes 16 | 17 | - [#120](https://github.com/toplenboren/simple-git-hooks/pull/120) [`fc2acfc`](https://github.com/toplenboren/simple-git-hooks/commit/fc2acfc92830b0195e17a3fbcca4db90e3fa275b) Thanks [@JounQin](https://github.com/JounQin)! - feat: support esm format configs 18 | 19 | ## Previous Releases 20 | 21 | See [**GitHub Releases**](https://github.com/toplenboren/simple-git-hooks/releases) for the changelog. 22 | -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "ethereum": { 4 | "ownedBy": "0x8A370C5EF45B9a869CaAcDc98D748886A53C7541" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mikhail Gorbunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-git-hooks 2 | 3 | ![](https://img.shields.io/badge/dependencies-zero-green) [![Tests](https://github.com/toplenboren/simple-git-hooks/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/toplenboren/simple-git-hooks/actions/workflows/tests.yml) 4 | 5 | A tool that lets you easily manage git hooks 6 | 7 | > The package was recently renamed from `simple-pre-commit`. 8 | 9 | > See **Releases** for the `simple-pre-commit` documentation and changelog 10 | 11 | - Zero dependency 12 | - Small configuration (1 object in package.json) 13 | - Lightweight: 14 | 15 | | Package | Unpacked size | With deps | 16 | |-------------------------------|--------------|-----------| 17 | | husky v4 `4.3.8` | `53.5 kB` | `~1 mB` | 18 | | husky v8 `8.0.3` | `6.44 kB` | `6.44 kB` | 19 | | pre-commit `1.2.2` | `~80 kB` | `~850 kB` | 20 | | **simple-git-hooks** `2.11.0` | `10.9 kB` | `10.9 kB` | 21 | 22 | ### Who uses simple-git-hooks? 23 | 24 | - [Autoprefixer](https://github.com/postcss/autoprefixer) 25 | - [PostCSS](https://github.com/postcss/postcss.org) 26 | - [Browserslist](https://github.com/browserslist/browserslist) 27 | - [Nano ID](https://github.com/ai/nanoid) 28 | - [Size Limit](https://github.com/ai/size-limit) 29 | - [Storeon](https://github.com/storeon/storeon) 30 | - [Directus](https://github.com/directus/directus) 31 | - [Vercel/pkg](https://github.com/vercel/pkg) 32 | - More, see [full list](https://github.com/toplenboren/simple-git-hooks/network/dependents?package_id=UGFja2FnZS0xOTk1ODMzMTA4) 33 | 34 | ### What is a git hook? 35 | 36 | A git hook is a command or script that is going to be run every time you perform a git action, like `git commit` or `git push`. 37 | 38 | If the execution of a git hook fails, then the git action aborts. 39 | 40 | For example, if you want to run `linter` on every commit to ensure code quality in your project, then you can create a `pre-commit` hook that would call `npx lint-staged`. 41 | 42 | Check out [lint-staged](https://github.com/okonet/lint-staged#readme). It works really well with `simple-git-hooks`. 43 | 44 | You can look up about git hooks on the [Pro Git book](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks). 45 | 46 | ### When to use it 47 | 48 | `simple-git-hooks` works well for small-sized projects when you need quickly set up hooks and forget about it. 49 | 50 | However, this package requires you to manually apply the changes to git hooks. If you update them often, this is probably not the best choice. 51 | 52 | Also, this package allows you to set only one command per git hook. 53 | 54 | If you need multiple verbose commands per git hook, flexible configuration or automatic update of git hooks, please check out the other packages: 55 | 56 | - [Lefthook](https://github.com/Arkweid/lefthook) 57 | - [husky](https://typicode.github.io/husky/) 58 | - [pre-commit](https://github.com/pre-commit/pre-commit) 59 | 60 | ## Usage 61 | 62 | ### Add simple-git-hooks to the project 63 | 64 | 1. Install simple-git-hooks as a dev dependency: 65 | 66 | ```sh 67 | npm install simple-git-hooks --save-dev 68 | ``` 69 | 70 | 2. Add `simple-git-hooks` to your `package.json`. Fill it with git hooks and the corresponding commands. 71 | 72 | For example: 73 | 74 | ```jsonc 75 | { 76 | "simple-git-hooks": { 77 | "pre-commit": "npx lint-staged", 78 | "pre-push": "npm run format", 79 | 80 | // All unused hooks will be removed automatically by default 81 | // but you can use the `preserveUnused` option like following to prevent this behavior 82 | 83 | // if you'd prefer preserve all unused hooks 84 | "preserveUnused": true, 85 | 86 | // if you'd prefer preserve specific unused hooks 87 | "preserveUnused": ["commit-msg"] 88 | } 89 | } 90 | ``` 91 | 92 | This configuration is going to run all linters on every `commit` and formatter on `push`. 93 | 94 | > There are more ways to configure the package. Check out [Additional configuration options](#additional-configuration-options). 95 | 96 | 3. Run the CLI script to update the git hooks with the commands from the config: 97 | 98 | ```sh 99 | npx simple-git-hooks 100 | ``` 101 | 102 | Now all the git hooks are created. 103 | 104 | ### Update git hooks command 105 | 106 | 1. Change the configuration. 107 | 108 | 2. Run `npx simple-git-hooks` **from the root of your project**. 109 | 110 | Note for **yarn2** users: Please run `yarn dlx simple-git-hooks` instead of the command above. More info on [dlx](https://yarnpkg.com/cli/dlx) 111 | 112 | Note for **yarn1** users: Please run `ynpx simple-git-hooks` instead of the command above. More info on [ynpx](https://npm.io/package/ynpx) 113 | 114 | Note that you should manually run `npx simple-git-hooks` **every time you change a command**. 115 | 116 | ### Additional configuration options 117 | 118 | You can also add a `.simple-git-hooks.cjs`, `.simple-git-hooks.js`, `.simple-git-hooks.mjs`, `simple-git-hooks.cjs`, `simple-git-hooks.js`, `simple-git-hooks.mjs`, `.simple-git-hooks.json` or `simple-git-hooks.json` file to the project and write the configuration inside it. 119 | 120 | This way `simple-git-hooks` configuration in `package.json` will not take effect any more. 121 | 122 | `.simple-git-hooks.cjs`, `.simple-git-hooks.js`, `.simple-git-hooks.mjs` or `simple-git-hooks.cjs`, `simple-git-hooks.js`, `simple-git-hooks.mjs` should look like the following. 123 | 124 | #### CommonJS 125 | 126 | ```js 127 | module.exports = { 128 | "pre-commit": "npx lint-staged", 129 | "pre-push": "npm run format", 130 | }; 131 | ``` 132 | 133 | 134 | #### ES Modules 135 | 136 | ```js 137 | export default { 138 | "pre-commit": "npx lint-staged", 139 | "pre-push": "npm run format", 140 | }; 141 | ``` 142 | 143 | `.simple-git-hooks.json` or `simple-git-hooks.json` should look like the following. 144 | 145 | ```json 146 | { 147 | "pre-commit": "npx lint-staged", 148 | "pre-push": "npm run format" 149 | } 150 | ``` 151 | 152 | If you need to have multiple configuration files or just your-own configuration file, you install hooks manually from it by `npx simple-git-hooks ./my-config.js`. 153 | 154 | ### Note for `npm` package developers 155 | 156 | Please do not add `postinstall: "npx simple-git-hooks"` script in your `package.json`. Or at least remove it before `npm publish` 157 | 158 | It causes errors for end users of your package 159 | 160 | ### Uninstall simple-git-hooks 161 | 162 | > Uninstallation will remove all the existing git hooks. 163 | 164 | ```sh 165 | npm uninstall simple-git-hooks 166 | ``` 167 | 168 | ## Common issues 169 | 170 | ### I want to skip git hooks! 171 | 172 | If you need to bypass install hooks at all, for example on CI, you can use `SKIP_INSTALL_SIMPLE_GIT_HOOKS` environment variable at the first place. 173 | 174 | ```sh 175 | export SKIP_INSTALL_SIMPLE_GIT_HOOKS=1 176 | 177 | npm install simple-git-hooks --save-dev 178 | ``` 179 | 180 | Or if you only need to bypass hooks for a single git operation, you should use `--no-verify` option 181 | 182 | ```sh 183 | git commit -m "commit message" --no-verify # -n for shorthand 184 | ``` 185 | 186 | you can read more about it here https://bobbyhadz.com/blog/git-commit-skip-hooks#skip-git-commit-hooks 187 | 188 | 189 | If you need to bypass hooks for multiple Git operations, setting the SKIP_SIMPLE_GIT_HOOKS environment variable can be more convenient. Once set, all subsequent Git operations in the same terminal session will bypass the associated hooks. 190 | 191 | ```sh 192 | # Set the environment variable 193 | export SKIP_SIMPLE_GIT_HOOKS=1 194 | 195 | # Subsequent Git commands will skip the hooks 196 | git add . 197 | git commit -m "commit message" # pre-commit hooks are bypassed 198 | git push origin main # pre-push hooks are bypassed 199 | ``` 200 | 201 | ### Skipping Hooks in 3rd party git clients 202 | 203 | If your client provides a toggle to skip Git hooks, you can utilize it to bypass the hooks. For instance, in VSCode, you can toggle git.allowNoVerifyCommit in the settings. 204 | 205 | If you have the option to set arguments or environment variables, you can use the --no-verify option or the SKIP_SIMPLE_GIT_HOOKS environment variable. 206 | 207 | If these options are not available, you may need to resort to using the terminal for skipping hooks. 208 | 209 | ### I am getting "npx: command not found" error in a GUI git client 210 | 211 | This happens when using a node version manager such as `nodenv`, `nvm`, `mise` which require 212 | init script to provide project-specific node binaries. 213 | 214 | Create init script in `~/.simple-git-hooks.rc` that should be executed prior to git hooks. 215 | Please refer to your node manager documentation for details. For example, for mise, that will 216 | be: 217 | 218 | ```sh 219 | export PATH="$HOME/.local/share/mise/shims:$PATH" 220 | ``` 221 | 222 | Add `SIMPLE_GIT_HOOKS_RC` global environment variable pointing to that new script. For 223 | example, on macOS, add this to `~/.zshenv`: 224 | 225 | ```sh 226 | export SIMPLE_GIT_HOOKS_RC="$HOME/.simple-git-hooks.rc" 227 | ``` 228 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | all. | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | drop an email to toplenboren@gmail.com 12 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_alternative_separate_cjs/.simple-git-hooks.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1" 4 | } 5 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_alternative_separate_cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-alternative-cjs", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_alternative_separate_js/.simple-git-hooks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1" 4 | } 5 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_alternative_separate_js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-alternative-js", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_alternative_separate_json/.simple-git-hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1" 4 | } 5 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_alternative_separate_json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-alternative-json", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_alternative_separate_mjs/.simple-git-hooks.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1" 4 | } 5 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_alternative_separate_mjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-alternative-mjs", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_package_json/initrc_that_does_nothing.sh: -------------------------------------------------------------------------------- 1 | echo SIMPLE_GIT_HOOKS_RC that does nothing. 2 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_package_json/initrc_that_prevents_hook_fail.sh: -------------------------------------------------------------------------------- 1 | alias exit=echo 2 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_package_json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-pkg-json", 3 | "version": "1.0.0", 4 | "simple-git-hooks": { 5 | "pre-commit": "exit 1" 6 | }, 7 | "devDependencies": { 8 | "simple-pre-commit": "1.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_separate_cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-cjs", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_separate_cjs/simple-git-hooks.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1" 4 | } 5 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_separate_js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-js", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_separate_js/simple-git-hooks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1" 4 | } 5 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_separate_json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-json", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_separate_json/simple-git-hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1" 4 | } 5 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_separate_mjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-mjs", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_configuration_in_separate_mjs/simple-git-hooks.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1" 4 | } 5 | -------------------------------------------------------------------------------- /_tests/project_with_custom_configuration/git-hooks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1" 4 | } 5 | -------------------------------------------------------------------------------- /_tests/project_with_custom_configuration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-custom", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_incorrect_configuration_in_package_json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-incorrect", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_incorrect_configuration_in_package_json/simple-git-hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre-push": "exit 1", 3 | "pre-commit": "exit 1", 4 | "i-love-dogs": "peace" 5 | } 6 | -------------------------------------------------------------------------------- /_tests/project_with_simple_git_hooks_in_deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-deps", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_simple_git_hooks_in_dev_deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-dev-deps", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_with_unused_configuration_in_package_json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-unused", 3 | "version": "1.0.0", 4 | "simple-git-hooks": { 5 | "pre-commit": "exit 1", 6 | "preserveUnused": ["commit-msg"] 7 | }, 8 | "devDependencies": { 9 | "simple-git-hooks": "1.0.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /_tests/project_without_configuration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-without-config", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "simple-git-hooks": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /_tests/project_without_simple_git_hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks-test-package-without", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | }, 6 | "dependencies": { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 4 | /** 5 | * A CLI tool to change the git hooks to commands from config 6 | */ 7 | const {setHooksFromConfig} = require('./simple-git-hooks') 8 | 9 | const {SKIP_INSTALL_SIMPLE_GIT_HOOKS} = process.env 10 | 11 | if (['1', 'true'].includes(SKIP_INSTALL_SIMPLE_GIT_HOOKS)) { 12 | console.log(`[INFO] SKIP_INSTALL_SIMPLE_GIT_HOOKS is set to "${SKIP_INSTALL_SIMPLE_GIT_HOOKS}", skipping installing hook.`) 13 | return 14 | } 15 | 16 | setHooksFromConfig(process.cwd(), process.argv) 17 | .then(() => console.log('[INFO] Successfully set all git hooks')) 18 | .catch(e => console.log('[ERROR], Was not able to set git hooks. Error: ' + e)) 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-git-hooks", 3 | "type": "commonjs", 4 | "version": "2.13.0", 5 | "description": "A simple, zero dependency tool for setting up git hooks for small projects", 6 | "author": "Mikhail Gorbunov ", 7 | "main": "simple-git-hooks.js", 8 | "bin": "./cli.js", 9 | "packageManager": "yarn@1.22.22", 10 | "files": [ 11 | "cli.js", 12 | "postinstall.js", 13 | "simple-git-hooks.js", 14 | "uninstall.js" 15 | ], 16 | "scripts": { 17 | "postinstall": "node ./postinstall.js", 18 | "lint": "eslint *.js", 19 | "test": "node --experimental-vm-modules ./node_modules/.bin/jest", 20 | "release": "clean-pkg-json && changeset publish", 21 | "uninstall": "node ./uninstall.js" 22 | }, 23 | "keywords": [ 24 | "pre-commit", 25 | "pre-push", 26 | "git", 27 | "hook", 28 | "lint", 29 | "linter" 30 | ], 31 | "license": "MIT", 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/toplenboren/simple-git-hooks.git" 35 | }, 36 | "lint-staged": { 37 | "*.js": "eslint" 38 | }, 39 | "simple-git-hooks": { 40 | "pre-commit": "yarn lint" 41 | }, 42 | "devDependencies": { 43 | "@changesets/changelog-github": "^0.5.1", 44 | "@changesets/cli": "^2.28.1", 45 | "clean-pkg-json": "^1.2.1", 46 | "eslint": "^7.19.0", 47 | "jest": "^26.6.3", 48 | "lint-staged": "^10.5.4", 49 | "lodash.isequal": "^4.5.0", 50 | "simple-git-hooks": "link:." 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {checkSimpleGitHooksInDependencies, getProjectRootDirectoryFromNodeModules, setHooksFromConfig} = require("./simple-git-hooks"); 4 | 5 | 6 | /** 7 | * Creates the pre-commit from command in config by default 8 | */ 9 | async function postinstall() { 10 | let projectDirectory; 11 | 12 | /* When script is run after install, the process.cwd() would be like /node_modules/simple-git-hooks 13 | Here we try to get the original project directory by going upwards by 2 levels 14 | If we were not able to get new directory we assume, we are already in the project root */ 15 | const parsedProjectDirectory = getProjectRootDirectoryFromNodeModules(process.cwd()) 16 | if (parsedProjectDirectory !== undefined) { 17 | projectDirectory = parsedProjectDirectory 18 | } else { 19 | projectDirectory = process.cwd() 20 | } 21 | 22 | if (checkSimpleGitHooksInDependencies(projectDirectory)) { 23 | try { 24 | await setHooksFromConfig(projectDirectory) 25 | } catch (err) { 26 | console.log('[ERROR] Was not able to set git hooks. Reason: ' + err) 27 | } 28 | } 29 | } 30 | 31 | postinstall() 32 | -------------------------------------------------------------------------------- /simple-git-hooks.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const url = require('url') 4 | const { execSync } = require('child_process'); 5 | 6 | const CONFIG_ERROR = '[ERROR] Config was not found! Please add `.simple-git-hooks.cjs` or `.simple-git-hooks.js` or `.simple-git-hooks.mjs` or `simple-git-hooks.cjs` or `simple-git-hooks.js` or `simple-git-hooks.mjs` or `.simple-git-hooks.json` or `simple-git-hooks.json` or `simple-git-hooks` entry in package.json.\r\nCheck README for details' 7 | 8 | const VALID_GIT_HOOKS = [ 9 | 'applypatch-msg', 10 | 'pre-applypatch', 11 | 'post-applypatch', 12 | 'pre-commit', 13 | 'pre-merge-commit', 14 | 'prepare-commit-msg', 15 | 'commit-msg', 16 | 'post-commit', 17 | 'pre-rebase', 18 | 'post-checkout', 19 | 'post-merge', 20 | 'pre-push', 21 | 'pre-receive', 22 | 'update', 23 | 'proc-receive', 24 | 'post-receive', 25 | 'post-update', 26 | 'reference-transaction', 27 | 'push-to-checkout', 28 | 'pre-auto-gc', 29 | 'post-rewrite', 30 | 'sendemail-validate', 31 | 'fsmonitor-watchman', 32 | 'p4-changelist', 33 | 'p4-prepare-changelist', 34 | 'p4-post-changelist', 35 | 'p4-pre-submit', 36 | 'post-index-change', 37 | ] 38 | 39 | const VALID_OPTIONS = ['preserveUnused'] 40 | 41 | const PREPEND_SCRIPT = 42 | `#!/bin/sh 43 | 44 | if [ "$SKIP_SIMPLE_GIT_HOOKS" = "1" ]; then 45 | echo "[INFO] SKIP_SIMPLE_GIT_HOOKS is set to 1, skipping hook." 46 | exit 0 47 | fi 48 | 49 | if [ -f "$SIMPLE_GIT_HOOKS_RC" ]; then 50 | . "$SIMPLE_GIT_HOOKS_RC" 51 | fi 52 | 53 | ` 54 | 55 | /** 56 | * Recursively gets the .git folder path from provided directory 57 | * @param {string} directory 58 | * @return {string | undefined} .git folder path or undefined if it was not found 59 | */ 60 | function getGitProjectRoot(directory=process.cwd()) { 61 | let start = directory 62 | if (typeof start === 'string') { 63 | if (start[start.length - 1] !== path.sep) { 64 | start += path.sep 65 | } 66 | start = path.normalize(start) 67 | start = start.split(path.sep) 68 | } 69 | if (!start.length) { 70 | return undefined 71 | } 72 | start.pop() 73 | 74 | let dir = start.join(path.sep) 75 | let fullPath = path.join(dir, '.git') 76 | 77 | if (fs.existsSync(fullPath)) { 78 | if(!fs.lstatSync(fullPath).isDirectory()) { 79 | let content = fs.readFileSync(fullPath, { encoding: 'utf-8' }) 80 | let match = /^gitdir: (.*)\s*$/.exec(content) 81 | if (match) { 82 | let gitDir = match[1] 83 | let commonDir = path.join(gitDir, 'commondir'); 84 | if (fs.existsSync(commonDir)) { 85 | commonDir = fs.readFileSync(commonDir, 'utf8').trim(); 86 | return path.resolve(gitDir, commonDir) 87 | } 88 | return path.normalize(gitDir) 89 | } 90 | } 91 | return path.normalize(fullPath) 92 | } else { 93 | return getGitProjectRoot(start) 94 | } 95 | } 96 | 97 | /** 98 | * Transforms the /node_modules/simple-git-hooks to 99 | * @param projectPath - path to the simple-git-hooks in node modules 100 | * @return {string | undefined} - an absolute path to the project or undefined if projectPath is not in node_modules 101 | */ 102 | function getProjectRootDirectoryFromNodeModules(projectPath) { 103 | function _arraysAreEqual(a1, a2) { 104 | return JSON.stringify(a1) === JSON.stringify(a2) 105 | } 106 | 107 | const projDir = projectPath.split(/[\\/]/) // <- would split both on '/' and '\' 108 | 109 | const indexOfPnpmDir = projDir.indexOf('.pnpm') 110 | if (indexOfPnpmDir > -1) { 111 | return projDir.slice(0, indexOfPnpmDir - 1).join('/'); 112 | } 113 | const indexOfDenoDir = projDir.indexOf('.deno') 114 | if (indexOfDenoDir > -1) { 115 | return projDir.slice(0, indexOfDenoDir - 1).join('/'); 116 | } 117 | 118 | const indexOfStoreDir = projDir.indexOf('.store') 119 | if (indexOfStoreDir > -1) { 120 | return projDir.slice(0, indexOfStoreDir - 1).join('/'); 121 | } 122 | 123 | // A yarn2 STAB 124 | if (projDir.includes('.yarn') && projDir.includes('unplugged')) { 125 | return undefined 126 | } 127 | 128 | if (projDir.length > 2 && 129 | _arraysAreEqual(projDir.slice(projDir.length - 2, projDir.length), [ 130 | 'node_modules', 131 | 'simple-git-hooks' 132 | ])) { 133 | 134 | return projDir.slice(0, projDir.length - 2).join('/') 135 | } 136 | 137 | return undefined 138 | } 139 | 140 | /** 141 | * Checks the 'simple-git-hooks' in dependencies of the project 142 | * @param {string} projectRootPath 143 | * @throws TypeError if packageJsonData not an object 144 | * @return {Boolean} 145 | */ 146 | function checkSimpleGitHooksInDependencies(projectRootPath) { 147 | if (typeof projectRootPath !== 'string') { 148 | throw TypeError("Package json path is not a string!") 149 | } 150 | 151 | const {packageJsonContent} = _getPackageJson(projectRootPath) 152 | 153 | // if simple-git-hooks in dependencies -> note user that he should remove move it to devDeps! 154 | if ('dependencies' in packageJsonContent && 'simple-git-hooks' in packageJsonContent.dependencies) { 155 | console.warn('[WARN] You should move simple-git-hooks to the devDependencies!') 156 | return true // We only check that we are in the correct package, e.g not in a dependency of a dependency 157 | } 158 | if (!('devDependencies' in packageJsonContent)) { 159 | return false 160 | } 161 | return 'simple-git-hooks' in packageJsonContent.devDependencies 162 | } 163 | 164 | /** 165 | * Parses the config and sets git hooks 166 | * @param {string} projectRootPath 167 | * @param {string[]} [argv] 168 | */ 169 | async function setHooksFromConfig(projectRootPath=process.cwd(), argv=process.argv) { 170 | const customConfigPath = _getCustomConfigPath(argv) 171 | const config = await _getConfig(projectRootPath, customConfigPath) 172 | 173 | if (!config) { 174 | throw(CONFIG_ERROR) 175 | } 176 | 177 | const preserveUnused = Array.isArray(config.preserveUnused) ? config.preserveUnused : config.preserveUnused ? VALID_GIT_HOOKS: [] 178 | 179 | for (let hook of VALID_GIT_HOOKS) { 180 | if (Object.prototype.hasOwnProperty.call(config, hook)) { 181 | _setHook(hook, config[hook], projectRootPath) 182 | } else if (!preserveUnused.includes(hook)) { 183 | _removeHook(hook, projectRootPath) 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * Returns the absolute path to the Git hooks directory. 190 | * Respects user-defined core.hooksPath from Git config if present; 191 | * otherwise defaults to /.git/hooks. 192 | * 193 | * @param {string} gitRoot - The absolute path to the Git project root 194 | * @returns {string} - The resolved absolute path to the hooks directory 195 | * @private 196 | */ 197 | function _getHooksDirPath(projectRoot) { 198 | const defaultHooksDirPath = path.join(projectRoot, '.git', 'hooks') 199 | try { 200 | const customHooksDirPath = execSync('git config core.hooksPath', { 201 | cwd: projectRoot, 202 | encoding: 'utf8' 203 | }).trim() 204 | 205 | if (!customHooksDirPath) { 206 | return defaultHooksDirPath 207 | } 208 | 209 | return path.isAbsolute(customHooksDirPath) 210 | ? customHooksDirPath 211 | : path.resolve(projectRoot, customHooksDirPath) 212 | } catch { 213 | return defaultHooksDirPath 214 | } 215 | } 216 | 217 | /** 218 | * Creates or replaces an existing executable script in the git hooks directory with provided command 219 | * @param {string} hook 220 | * @param {string} command 221 | * @param {string} projectRoot 222 | * @private 223 | */ 224 | function _setHook(hook, command, projectRoot=process.cwd()) { 225 | const gitRoot = getGitProjectRoot(projectRoot) 226 | 227 | if (!gitRoot) { 228 | console.info('[INFO] No `.git` root folder found, skipping') 229 | return 230 | } 231 | 232 | const hookCommand = PREPEND_SCRIPT + command 233 | const hookDirectory = _getHooksDirPath(projectRoot) 234 | const hookPath = path.join(hookDirectory, hook) 235 | 236 | const normalizedHookDirectory = path.normalize(hookDirectory) 237 | if (!fs.existsSync(normalizedHookDirectory)) { 238 | fs.mkdirSync(normalizedHookDirectory, { recursive: true }) 239 | } 240 | 241 | fs.writeFileSync(hookPath, hookCommand) 242 | fs.chmodSync(hookPath, 0o0755) 243 | 244 | console.info(`[INFO] Successfully set the ${hook} with command: ${command}`) 245 | } 246 | 247 | /** 248 | * Deletes all git hooks 249 | * @param {string} projectRoot 250 | */ 251 | async function removeHooks(projectRoot = process.cwd()) { 252 | const customConfigPath = _getCustomConfigPath(process.argv) 253 | const config = await _getConfig(projectRoot, customConfigPath) 254 | 255 | if (!config) { 256 | throw (CONFIG_ERROR) 257 | } 258 | 259 | const preserveUnused = Array.isArray(config.preserveUnused) ? config.preserveUnused : [] 260 | for (const configEntry of VALID_GIT_HOOKS) { 261 | if(!preserveUnused.includes(configEntry)) { 262 | _removeHook(configEntry, projectRoot) 263 | } 264 | } 265 | } 266 | 267 | /** 268 | * Removes the pre-commit hook 269 | * @param {string} hook 270 | * @param {string} projectRoot 271 | * @private 272 | */ 273 | function _removeHook(hook, projectRoot=process.cwd()) { 274 | const hookDirectory = _getHooksDirPath(projectRoot) 275 | const hookPath = path.join(hookDirectory, hook) 276 | 277 | if (fs.existsSync(hookPath)) { 278 | fs.unlinkSync(hookPath) 279 | } 280 | } 281 | 282 | /** Reads package.json file, returns package.json content and path 283 | * @param {string} projectPath - a path to the project, defaults to process.cwd 284 | * @return {{packageJsonContent: any, packageJsonPath: string}} 285 | * @throws TypeError if projectPath is not a string 286 | * @throws Error if cant read package.json 287 | * @private 288 | */ 289 | function _getPackageJson(projectPath = process.cwd()) { 290 | if (typeof projectPath !== "string") { 291 | throw TypeError("projectPath is not a string") 292 | } 293 | 294 | const targetPackageJson = path.normalize(projectPath + '/package.json') 295 | 296 | if (!fs.statSync(targetPackageJson).isFile()) { 297 | throw Error("Package.json doesn't exist") 298 | } 299 | 300 | const packageJsonDataRaw = fs.readFileSync(targetPackageJson) 301 | return { packageJsonContent: JSON.parse(packageJsonDataRaw), packageJsonPath: targetPackageJson } 302 | } 303 | 304 | /** 305 | * Takes the first argument from current process argv and returns it 306 | * Returns empty string when argument wasn't passed 307 | * @param {string[]} [argv] 308 | * @returns {string} 309 | */ 310 | function _getCustomConfigPath(argv=[]) { 311 | // We'll run as one of the following: 312 | // npx simple-git-hooks ./config.js 313 | // node path/to/simple-git-hooks/cli.js ./config.js 314 | return argv[2] || '' 315 | } 316 | 317 | /** 318 | * Gets user-set command either from sources 319 | * First try to get command from .simple-pre-commit.json 320 | * If not found -> try to get command from package.json 321 | * @param {string} projectRootPath 322 | * @param {string} [configFileName] 323 | * @throws TypeError if projectRootPath is not string 324 | * @return {Promise<{[key: string]: unknown} | undefined>} 325 | * @private 326 | */ 327 | async function _getConfig(projectRootPath, configFileName='') { 328 | if (typeof projectRootPath !== 'string') { 329 | throw TypeError("Check project root path! Expected a string, but got " + typeof projectRootPath) 330 | } 331 | 332 | // every function here should accept projectRootPath as first argument and return object 333 | const sources = [ 334 | () => _getConfigFromFile(projectRootPath, '.simple-git-hooks.cjs'), 335 | () => _getConfigFromFile(projectRootPath, '.simple-git-hooks.js'), 336 | () => _getConfigFromFile(projectRootPath, '.simple-git-hooks.mjs'), 337 | () => _getConfigFromFile(projectRootPath, 'simple-git-hooks.cjs'), 338 | () => _getConfigFromFile(projectRootPath, 'simple-git-hooks.js'), 339 | () => _getConfigFromFile(projectRootPath, 'simple-git-hooks.mjs'), 340 | () => _getConfigFromFile(projectRootPath, '.simple-git-hooks.json'), 341 | () => _getConfigFromFile(projectRootPath, 'simple-git-hooks.json'), 342 | () => _getConfigFromPackageJson(projectRootPath), 343 | ] 344 | 345 | // if user pass his-own config path prepend custom path before the default ones 346 | if (configFileName) { 347 | sources.unshift(() => _getConfigFromFile(projectRootPath, configFileName)) 348 | } 349 | 350 | for (let executeSource of sources) { 351 | let config = await executeSource() 352 | if (config) { 353 | if (_validateHooks(config)) { 354 | return config 355 | } 356 | 357 | throw('[ERROR] Config was not in correct format. Please check git hooks or options name') 358 | } 359 | } 360 | 361 | return undefined 362 | } 363 | 364 | /** 365 | * Gets current config from package.json[simple-pre-commit] 366 | * @param {string} projectRootPath 367 | * @throws TypeError if packageJsonPath is not a string 368 | * @throws Error if package.json couldn't be read or was not validated 369 | * @return {Promise<{[key: string]: unknown}> | {[key: string]: unknown} | undefined} 370 | */ 371 | function _getConfigFromPackageJson(projectRootPath = process.cwd()) { 372 | const {packageJsonContent} = _getPackageJson(projectRootPath) 373 | const config = packageJsonContent['simple-git-hooks']; 374 | return typeof config === 'string' ? _getConfig(config) : config 375 | } 376 | 377 | /** 378 | * Gets user-set config from file 379 | * Since the file is not required in node.js projects it returns undefined if something is off 380 | * @param {string} projectRootPath 381 | * @param {string} fileName 382 | * @return {Promise<{[key: string]: unknown} | undefined>} 383 | */ 384 | async function _getConfigFromFile(projectRootPath, fileName) { 385 | if (typeof projectRootPath !== "string") { 386 | throw TypeError("projectRootPath is not a string") 387 | } 388 | 389 | if (typeof fileName !== "string") { 390 | throw TypeError("fileName is not a string") 391 | } 392 | 393 | try { 394 | const filePath = path.isAbsolute(fileName) 395 | ? fileName 396 | : path.normalize(projectRootPath + '/' + fileName) 397 | if (filePath === __filename) { 398 | return undefined 399 | } 400 | // handle `.json` with `require`, `import()` requires `with:{type:'json'}` which does not work for all engines 401 | if (filePath.endsWith('.json')) { 402 | return require(filePath) 403 | } 404 | const result = await import(url.pathToFileURL(filePath)) // handle `.cjs`, `.js`, `.mjs` 405 | return result.default || result 406 | } catch (err) { 407 | return undefined 408 | } 409 | } 410 | 411 | /** 412 | * Validates the config, checks that every git hook or option is named correctly 413 | * @return {boolean} 414 | * @param {{string: string}} config 415 | */ 416 | function _validateHooks(config) { 417 | 418 | for (let hookOrOption in config) { 419 | if (!VALID_GIT_HOOKS.includes(hookOrOption) && !VALID_OPTIONS.includes(hookOrOption)) { 420 | return false 421 | } 422 | } 423 | 424 | return true 425 | } 426 | 427 | module.exports = { 428 | checkSimpleGitHooksInDependencies, 429 | setHooksFromConfig, 430 | getProjectRootDirectoryFromNodeModules, 431 | getGitProjectRoot, 432 | removeHooks, 433 | PREPEND_SCRIPT 434 | } 435 | -------------------------------------------------------------------------------- /simple-git-hooks.test.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { execSync } = require("child_process"); 4 | 5 | const isEqual = require("lodash.isequal"); 6 | 7 | const simpleGitHooks = require("./simple-git-hooks"); 8 | const { version: packageVersion } = require("./package.json"); 9 | 10 | // Util functions: 11 | 12 | 13 | 14 | 15 | 16 | describe("Simple Git Hooks tests", () => { 17 | /** 18 | * This section of tests is used to test how simple util functions perform 19 | * If you are adding a new util function, you should create unit test suite (describe) for it 20 | */ 21 | describe('Unit tests', () => { 22 | describe("getProjectRootDirectory", () => { 23 | it("returns correct dir in typical case:", () => { 24 | expect( 25 | simpleGitHooks.getProjectRootDirectoryFromNodeModules( 26 | "var/my-project/node_modules/simple-git-hooks" 27 | ) 28 | ).toBe("var/my-project"); 29 | }); 30 | 31 | it("returns correct dir when used with windows delimiters:", () => { 32 | expect( 33 | simpleGitHooks.getProjectRootDirectoryFromNodeModules( 34 | "user\\allProjects\\project\\node_modules\\simple-git-hooks" 35 | ) 36 | ).toBe("user/allProjects/project"); 37 | }); 38 | 39 | it("falls back to undefined when we are not in node_modules:", () => { 40 | expect( 41 | simpleGitHooks.getProjectRootDirectoryFromNodeModules( 42 | "var/my-project/simple-git-hooks" 43 | ) 44 | ).toBe(undefined); 45 | }); 46 | 47 | it("return correct dir when installed using pnpm:", () => { 48 | expect( 49 | simpleGitHooks.getProjectRootDirectoryFromNodeModules( 50 | `var/my-project/node_modules/.pnpm/simple-git-hooks@${packageVersion}` 51 | ) 52 | ).toBe("var/my-project"); 53 | expect( 54 | simpleGitHooks.getProjectRootDirectoryFromNodeModules( 55 | `var/my-project/node_modules/.pnpm/simple-git-hooks@${packageVersion}/node_modules/simple-git-hooks` 56 | ) 57 | ).toBe("var/my-project"); 58 | }); 59 | 60 | it("return correct dir when installed using yarn3 nodeLinker pnpm:", () => { 61 | expect( 62 | simpleGitHooks.getProjectRootDirectoryFromNodeModules( 63 | `var/my-project/node_modules/.store/simple-git-hooks@${packageVersion}/node_modules/simple-git-hooks` 64 | ) 65 | ).toBe("var/my-project"); 66 | }); 67 | 68 | it("return correct dir when installed using deno:", () => { 69 | expect( 70 | simpleGitHooks.getProjectRootDirectoryFromNodeModules( 71 | `var/my-project/node_modules/.deno/simple-git-hooks@${packageVersion}/node_modules/simple-git-hooks` 72 | ) 73 | ).toBe("var/my-project"); 74 | }); 75 | }); 76 | 77 | describe("getGitProjectRoot", () => { 78 | const gitProjectRoot = path.normalize(path.join(__dirname, ".git")); 79 | const currentPath = path.normalize(path.join(__dirname)); 80 | const currentFilePath = path.normalize(path.join(__filename)); 81 | 82 | it("works from .git directory itself", () => { 83 | expect(simpleGitHooks.getGitProjectRoot(gitProjectRoot)).toBe(gitProjectRoot); 84 | }); 85 | 86 | it("works from any directory", () => { 87 | expect(simpleGitHooks.getGitProjectRoot(currentPath)).toBe(gitProjectRoot); 88 | }); 89 | 90 | it("works from any file", () => { 91 | expect(simpleGitHooks.getGitProjectRoot(currentFilePath)).toBe(gitProjectRoot); 92 | }); 93 | }); 94 | 95 | describe("checkSimpleGitHooksInDependencies", () => { 96 | const PROJECT_WITH_SIMPLE_GIT_HOOKS_IN_DEPS = path.normalize( 97 | path.join(process.cwd(), "_tests", "project_with_simple_git_hooks_in_deps") 98 | ); 99 | const PROJECT_WITH_SIMPLE_GIT_HOOKS_IN_DEV_DEPS = path.normalize( 100 | path.join( 101 | process.cwd(), "_tests", "project_with_simple_git_hooks_in_dev_deps" 102 | ) 103 | ); 104 | const PROJECT_WITHOUT_SIMPLE_GIT_HOOKS = path.normalize( 105 | path.join(process.cwd(), "_tests", "project_without_simple_git_hooks") 106 | ); 107 | it("returns true if simple-git-hooks really in deps", () => { 108 | expect( 109 | simpleGitHooks.checkSimpleGitHooksInDependencies(PROJECT_WITH_SIMPLE_GIT_HOOKS_IN_DEPS) 110 | ).toBe(true); 111 | }); 112 | 113 | it("returns true if simple-git-hooks really in devDeps", () => { 114 | expect( 115 | simpleGitHooks.checkSimpleGitHooksInDependencies(PROJECT_WITH_SIMPLE_GIT_HOOKS_IN_DEV_DEPS) 116 | ).toBe(true); 117 | }); 118 | 119 | it("returns false if simple-git-hooks isn`t in deps", () => { 120 | expect( 121 | simpleGitHooks.checkSimpleGitHooksInDependencies(PROJECT_WITHOUT_SIMPLE_GIT_HOOKS) 122 | ).toBe(false); 123 | }); 124 | }); 125 | }); 126 | 127 | /** 128 | * This section of tests should test end 2 end use scenarios. 129 | * If you are adding a new feature, you should create an e2e test suite (describe) for it 130 | */ 131 | describe('E2E tests', () => { 132 | 133 | const TEST_SCRIPT = `${simpleGitHooks.PREPEND_SCRIPT}exit 1`; 134 | const COMMON_GIT_HOOKS = { "pre-commit": TEST_SCRIPT, "pre-push": TEST_SCRIPT }; 135 | 136 | // To test this package, we often need to create and manage files. 137 | // Best to use real file system and real files under _tests folder 138 | const testsFolder = path.normalize(path.join(process.cwd(), "_tests")); 139 | 140 | // Configuration in Package.json 141 | const PROJECT_WITH_CONF_IN_PACKAGE_JSON = path.normalize( 142 | path.join(testsFolder, "project_with_configuration_in_package_json") 143 | ); 144 | 145 | // Configuration in .js file 146 | const PROJECT_WITH_CONF_IN_SEPARATE_JS = path.normalize( 147 | path.join(testsFolder, "project_with_configuration_in_separate_js") 148 | ); 149 | const PROJECT_WITH_CONF_IN_SEPARATE_JS_ALT = path.normalize( 150 | path.join(testsFolder, "project_with_configuration_in_alternative_separate_js") 151 | ); 152 | 153 | // Configuration in .cjs file 154 | const PROJECT_WITH_CONF_IN_SEPARATE_CJS = path.normalize( 155 | path.join(testsFolder, "project_with_configuration_in_separate_cjs") 156 | ); 157 | const PROJECT_WITH_CONF_IN_SEPARATE_CJS_ALT = path.normalize( 158 | path.join(testsFolder, "project_with_configuration_in_alternative_separate_cjs") 159 | ); 160 | 161 | // Configuration in .mjs file 162 | const PROJECT_WITH_CONF_IN_SEPARATE_MJS = path.normalize( 163 | path.join(testsFolder, "project_with_configuration_in_separate_mjs") 164 | ); 165 | const PROJECT_WITH_CONF_IN_SEPARATE_MJS_ALT = path.normalize( 166 | path.join(testsFolder, "project_with_configuration_in_alternative_separate_mjs") 167 | ); 168 | 169 | // Configuration in .json file 170 | const PROJECT_WITH_CONF_IN_SEPARATE_JSON = path.normalize( 171 | path.join(testsFolder, "project_with_configuration_in_separate_json") 172 | ); 173 | const PROJECT_WITH_CONF_IN_SEPARATE_JSON_ALT = path.normalize( 174 | path.join(testsFolder, "project_with_configuration_in_alternative_separate_json") 175 | ); 176 | 177 | // Other correct configurations 178 | const PROJECT_WITH_UNUSED_CONF_IN_PACKAGE_JSON = path.normalize( 179 | path.join(testsFolder, "project_with_unused_configuration_in_package_json") 180 | ); 181 | const PROJECT_WITH_CUSTOM_CONF = path.normalize( 182 | path.join(testsFolder, "project_with_custom_configuration") 183 | ); 184 | 185 | // Incorrect configurations 186 | const PROJECT_WITH_BAD_CONF_IN_PACKAGE_JSON_ = path.normalize( 187 | path.join(testsFolder, "project_with_incorrect_configuration_in_package_json") 188 | ); 189 | const PROJECT_WO_CONF = path.normalize( 190 | path.join(testsFolder, "project_without_configuration") 191 | ); 192 | 193 | /** 194 | * Creates .git/hooks dir from root 195 | * @param {string} root 196 | */ 197 | function createGitHooksFolder(root) { 198 | if (fs.existsSync(root + "/.git")) { 199 | return; 200 | } 201 | fs.mkdirSync(root + "/.git"); 202 | fs.mkdirSync(root + "/.git/hooks"); 203 | } 204 | 205 | /** 206 | * Removes .git directory from root 207 | * @param {string} root 208 | */ 209 | function removeGitHooksFolder(root) { 210 | if (fs.existsSync(root + "/.git")) { 211 | fs.rmdirSync(root + "/.git", { recursive: true }); 212 | } 213 | } 214 | 215 | /** 216 | * Returns all installed git hooks 217 | * @return { {string: string} } 218 | */ 219 | function getInstalledGitHooks(hooksDir) { 220 | const result = {}; 221 | 222 | const hooks = fs.readdirSync(hooksDir); 223 | 224 | for (let hook of hooks) { 225 | result[hook] = fs 226 | .readFileSync(path.normalize(path.join(hooksDir, hook))) 227 | .toString(); 228 | } 229 | 230 | return result; 231 | } 232 | 233 | describe('Configuration tests', function () { 234 | describe("Valid configurations", () => { 235 | it("creates git hooks if configuration is correct from .simple-git-hooks.js", async () => { 236 | createGitHooksFolder(PROJECT_WITH_CONF_IN_SEPARATE_JS_ALT); 237 | 238 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CONF_IN_SEPARATE_JS_ALT); 239 | const installedHooks = getInstalledGitHooks( 240 | path.normalize( 241 | path.join( 242 | PROJECT_WITH_CONF_IN_SEPARATE_JS_ALT, 243 | ".git", 244 | "hooks" 245 | ) 246 | ) 247 | ); 248 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 249 | }); 250 | 251 | it("creates git hooks if configuration is correct from .simple-git-hooks.cjs", async () => { 252 | createGitHooksFolder(PROJECT_WITH_CONF_IN_SEPARATE_CJS_ALT); 253 | 254 | await simpleGitHooks.setHooksFromConfig( 255 | PROJECT_WITH_CONF_IN_SEPARATE_CJS_ALT 256 | ); 257 | const installedHooks = getInstalledGitHooks( 258 | path.normalize( 259 | path.join( 260 | PROJECT_WITH_CONF_IN_SEPARATE_CJS_ALT, 261 | ".git", 262 | "hooks" 263 | ) 264 | ) 265 | ); 266 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 267 | }); 268 | 269 | it("creates git hooks if configuration is correct from simple-git-hooks.cjs", async () => { 270 | createGitHooksFolder(PROJECT_WITH_CONF_IN_SEPARATE_CJS); 271 | 272 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CONF_IN_SEPARATE_CJS); 273 | const installedHooks = getInstalledGitHooks( 274 | path.normalize( 275 | path.join(PROJECT_WITH_CONF_IN_SEPARATE_CJS, ".git", "hooks") 276 | ) 277 | ); 278 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 279 | }); 280 | 281 | it("creates git hooks if configuration is correct from .simple-git-hooks.mjs", async () => { 282 | createGitHooksFolder(PROJECT_WITH_CONF_IN_SEPARATE_MJS_ALT); 283 | 284 | await simpleGitHooks.setHooksFromConfig( 285 | PROJECT_WITH_CONF_IN_SEPARATE_MJS_ALT 286 | ); 287 | const installedHooks = getInstalledGitHooks( 288 | path.normalize( 289 | path.join( 290 | PROJECT_WITH_CONF_IN_SEPARATE_MJS_ALT, 291 | ".git", 292 | "hooks" 293 | ) 294 | ) 295 | ); 296 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 297 | }); 298 | 299 | it("creates git hooks if configuration is correct from simple-git-hooks.mjs", async () => { 300 | createGitHooksFolder(PROJECT_WITH_CONF_IN_SEPARATE_MJS); 301 | 302 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CONF_IN_SEPARATE_MJS); 303 | const installedHooks = getInstalledGitHooks( 304 | path.normalize( 305 | path.join(PROJECT_WITH_CONF_IN_SEPARATE_MJS, ".git", "hooks") 306 | ) 307 | ); 308 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 309 | }); 310 | 311 | it("creates git hooks if configuration is correct from simple-git-hooks.js", async () => { 312 | createGitHooksFolder(PROJECT_WITH_CONF_IN_SEPARATE_JS); 313 | 314 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CONF_IN_SEPARATE_JS); 315 | const installedHooks = getInstalledGitHooks( 316 | path.normalize( 317 | path.join(PROJECT_WITH_CONF_IN_SEPARATE_JS, ".git", "hooks") 318 | ) 319 | ); 320 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 321 | }); 322 | 323 | it("creates git hooks if configuration is correct from .simple-git-hooks.json", async () => { 324 | createGitHooksFolder(PROJECT_WITH_CONF_IN_SEPARATE_JSON_ALT); 325 | 326 | await simpleGitHooks.setHooksFromConfig( 327 | PROJECT_WITH_CONF_IN_SEPARATE_JSON_ALT 328 | ); 329 | const installedHooks = getInstalledGitHooks( 330 | path.normalize( 331 | path.join( 332 | PROJECT_WITH_CONF_IN_SEPARATE_JSON_ALT, 333 | ".git", 334 | "hooks" 335 | ) 336 | ) 337 | ); 338 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 339 | }); 340 | 341 | it("creates git hooks if configuration is correct from simple-git-hooks.json", async () => { 342 | createGitHooksFolder(PROJECT_WITH_CONF_IN_SEPARATE_JSON); 343 | 344 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CONF_IN_SEPARATE_JSON); 345 | const installedHooks = getInstalledGitHooks( 346 | path.normalize( 347 | path.join(PROJECT_WITH_CONF_IN_SEPARATE_JSON, ".git", "hooks") 348 | ) 349 | ); 350 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 351 | }); 352 | 353 | it("creates git hooks if configuration is correct from package.json", async () => { 354 | createGitHooksFolder(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 355 | 356 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 357 | const installedHooks = getInstalledGitHooks( 358 | path.normalize( 359 | path.join(PROJECT_WITH_CONF_IN_PACKAGE_JSON, ".git", "hooks") 360 | ) 361 | ); 362 | expect(isEqual(installedHooks, { "pre-commit": TEST_SCRIPT })).toBe(true); 363 | }); 364 | }); 365 | 366 | describe("Invalid configurations", () => { 367 | it("fails to create git hooks if configuration contains bad git hooks", async () => { 368 | createGitHooksFolder(PROJECT_WITH_BAD_CONF_IN_PACKAGE_JSON_); 369 | 370 | await expect( 371 | simpleGitHooks.setHooksFromConfig(PROJECT_WITH_BAD_CONF_IN_PACKAGE_JSON_) 372 | ).rejects.toMatch( 373 | "[ERROR] Config was not in correct format. Please check git hooks or options name" 374 | ); 375 | }); 376 | 377 | it("fails to create git hooks if not configured", async () => { 378 | createGitHooksFolder(PROJECT_WO_CONF); 379 | 380 | await expect(() => simpleGitHooks.setHooksFromConfig(PROJECT_WO_CONF)).rejects.toMatch( 381 | "[ERROR] Config was not found! Please add `.simple-git-hooks.cjs` or `.simple-git-hooks.js` or `.simple-git-hooks.mjs` or `simple-git-hooks.cjs` or `simple-git-hooks.js` or `simple-git-hooks.mjs` or `.simple-git-hooks.json` or `simple-git-hooks.json` or `simple-git-hooks` entry in package.json." 382 | ); 383 | }); 384 | }); 385 | }); 386 | 387 | describe("Remove hooks tests", () => { 388 | it("removes git hooks", async () => { 389 | createGitHooksFolder(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 390 | 391 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 392 | 393 | let installedHooks = getInstalledGitHooks( 394 | path.normalize( 395 | path.join(PROJECT_WITH_CONF_IN_PACKAGE_JSON, ".git", "hooks") 396 | ) 397 | ); 398 | expect(isEqual(installedHooks, { "pre-commit": TEST_SCRIPT })).toBe(true); 399 | 400 | await simpleGitHooks.removeHooks(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 401 | 402 | installedHooks = getInstalledGitHooks( 403 | path.normalize( 404 | path.join(PROJECT_WITH_CONF_IN_PACKAGE_JSON, ".git", "hooks") 405 | ) 406 | ); 407 | expect(isEqual(installedHooks, {})).toBe(true); 408 | }); 409 | 410 | it("creates git hooks and removes unused git hooks", async () => { 411 | createGitHooksFolder(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 412 | 413 | const installedHooksDir = path.normalize( 414 | path.join(PROJECT_WITH_CONF_IN_PACKAGE_JSON, ".git", "hooks") 415 | ); 416 | 417 | fs.writeFileSync( 418 | path.resolve(installedHooksDir, "pre-push"), 419 | "# do nothing" 420 | ); 421 | 422 | let installedHooks = getInstalledGitHooks(installedHooksDir); 423 | expect(isEqual(installedHooks, { "pre-push": "# do nothing" })).toBe(true); 424 | 425 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 426 | 427 | installedHooks = getInstalledGitHooks(installedHooksDir); 428 | expect(isEqual(installedHooks, { "pre-commit": TEST_SCRIPT })).toBe(true); 429 | }); 430 | 431 | it("creates git hooks and removes unused but preserves specific git hooks", async () => { 432 | createGitHooksFolder(PROJECT_WITH_UNUSED_CONF_IN_PACKAGE_JSON); 433 | 434 | const installedHooksDir = path.normalize( 435 | path.join( 436 | PROJECT_WITH_UNUSED_CONF_IN_PACKAGE_JSON, 437 | ".git", 438 | "hooks" 439 | ) 440 | ); 441 | 442 | fs.writeFileSync( 443 | path.resolve(installedHooksDir, "commit-msg"), 444 | "# do nothing" 445 | ); 446 | fs.writeFileSync( 447 | path.resolve(installedHooksDir, "pre-push"), 448 | "# do nothing" 449 | ); 450 | 451 | let installedHooks = getInstalledGitHooks(installedHooksDir); 452 | expect( 453 | isEqual(installedHooks, { 454 | "commit-msg": "# do nothing", 455 | "pre-push": "# do nothing", 456 | }) 457 | ).toBe(true); 458 | 459 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_UNUSED_CONF_IN_PACKAGE_JSON); 460 | 461 | installedHooks = getInstalledGitHooks(installedHooksDir); 462 | expect( 463 | isEqual(installedHooks, { 464 | "commit-msg": "# do nothing", 465 | "pre-commit": TEST_SCRIPT, 466 | }) 467 | ).toBe(true); 468 | }); 469 | 470 | it("creates git hooks and removes hooks which are not in preserveUnused", async () => { 471 | createGitHooksFolder(PROJECT_WITH_UNUSED_CONF_IN_PACKAGE_JSON); 472 | 473 | const installedHooksDir = path.normalize( 474 | path.join( 475 | PROJECT_WITH_UNUSED_CONF_IN_PACKAGE_JSON, 476 | ".git", 477 | "hooks" 478 | ) 479 | ); 480 | 481 | fs.writeFileSync( 482 | path.resolve(installedHooksDir, "commit-msg"), 483 | "# do nothing" 484 | ); 485 | 486 | let installedHooks = getInstalledGitHooks(installedHooksDir); 487 | 488 | expect(isEqual(installedHooks, { "commit-msg": "# do nothing" })).toBe(true); 489 | 490 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_UNUSED_CONF_IN_PACKAGE_JSON); 491 | 492 | installedHooks = getInstalledGitHooks(installedHooksDir); 493 | expect(isEqual(installedHooks, { "pre-commit": TEST_SCRIPT, "commit-msg": "# do nothing" })).toBe(true); 494 | 495 | await simpleGitHooks.removeHooks(PROJECT_WITH_UNUSED_CONF_IN_PACKAGE_JSON); 496 | 497 | installedHooks = getInstalledGitHooks(installedHooksDir); 498 | expect(isEqual(installedHooks, { "commit-msg": "# do nothing" })).toBe(true); 499 | }); 500 | }); 501 | 502 | describe("Custom core.hooksPath", () => { 503 | beforeAll(() => { 504 | execSync('git config core.hooksPath .husky'); 505 | }); 506 | 507 | afterAll(() => { 508 | execSync('git config --unset core.hooksPath'); 509 | }); 510 | 511 | const TEST_HUSKY_PROJECT = PROJECT_WITH_CONF_IN_SEPARATE_JS_ALT; 512 | 513 | it("creates git hooks in .husky if core.hooksPath is set to .husky", async () => { 514 | const huskyDir = path.join(TEST_HUSKY_PROJECT, ".husky"); 515 | 516 | if (!fs.existsSync(huskyDir)) { 517 | fs.mkdirSync(huskyDir); 518 | } 519 | 520 | await simpleGitHooks.setHooksFromConfig(TEST_HUSKY_PROJECT); 521 | 522 | const installedHooks = getInstalledGitHooks(huskyDir); 523 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 524 | }) 525 | 526 | it("remove git hooks in .husky if core.hooksPath is set to .husky", async () => { 527 | await simpleGitHooks.removeHooks(TEST_HUSKY_PROJECT); 528 | const huskyDir = path.join(TEST_HUSKY_PROJECT, ".husky"); 529 | const installedHooks = getInstalledGitHooks(huskyDir); 530 | expect(isEqual(installedHooks, {})).toBe(true); 531 | }) 532 | }) 533 | 534 | describe("CLI tests", () => { 535 | const testCases = [ 536 | ["npx", "simple-git-hooks", "./git-hooks.js"], 537 | ["node", require.resolve(`./cli`), "./git-hooks.js"], 538 | [ 539 | "node", 540 | require.resolve(`./cli`), 541 | require.resolve(`${PROJECT_WITH_CUSTOM_CONF}/git-hooks.js`), 542 | ], 543 | ]; 544 | 545 | testCases.forEach((args) => { 546 | it(`creates git hooks and removes unused but preserves specific git hooks for command: ${args.join( 547 | " " 548 | )}`, async () => { 549 | createGitHooksFolder(PROJECT_WITH_CUSTOM_CONF); 550 | 551 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CUSTOM_CONF, args); 552 | const installedHooks = getInstalledGitHooks( 553 | path.normalize( 554 | path.join(PROJECT_WITH_CUSTOM_CONF, ".git", "hooks") 555 | ) 556 | ); 557 | expect(JSON.stringify(installedHooks)).toBe(JSON.stringify(COMMON_GIT_HOOKS)); 558 | expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); 559 | }); 560 | }); 561 | 562 | describe("SKIP_INSTALL_SIMPLE_GIT_HOOKS", () => { 563 | afterEach(() => { 564 | removeGitHooksFolder(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 565 | }); 566 | 567 | it("does not create git hooks when SKIP_INSTALL_SIMPLE_GIT_HOOKS is set to 1", () => { 568 | createGitHooksFolder(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 569 | execSync(`node ${require.resolve("./cli")}`, { 570 | cwd: PROJECT_WITH_CONF_IN_PACKAGE_JSON, 571 | env: { 572 | ...process.env, 573 | SKIP_INSTALL_SIMPLE_GIT_HOOKS: "1", 574 | }, 575 | }); 576 | const installedHooks = getInstalledGitHooks( 577 | path.normalize( 578 | path.join(PROJECT_WITH_CONF_IN_PACKAGE_JSON, ".git", "hooks") 579 | ) 580 | ); 581 | expect(installedHooks).toEqual({}); 582 | }); 583 | 584 | it("creates git hooks when SKIP_INSTALL_SIMPLE_GIT_HOOKS is set to 0", () => { 585 | createGitHooksFolder(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 586 | execSync(`node ${require.resolve("./cli")}`, { 587 | cwd: PROJECT_WITH_CONF_IN_PACKAGE_JSON, 588 | env: { 589 | ...process.env, 590 | SKIP_INSTALL_SIMPLE_GIT_HOOKS: "0", 591 | }, 592 | }); 593 | const installedHooks = getInstalledGitHooks( 594 | path.normalize( 595 | path.join(PROJECT_WITH_CONF_IN_PACKAGE_JSON, ".git", "hooks") 596 | ) 597 | ); 598 | expect(installedHooks).toEqual({ "pre-commit": TEST_SCRIPT }); 599 | }); 600 | 601 | it("creates git hooks when SKIP_INSTALL_SIMPLE_GIT_HOOKS is not set", () => { 602 | createGitHooksFolder(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 603 | execSync(`node ${require.resolve("./cli")}`, { 604 | cwd: PROJECT_WITH_CONF_IN_PACKAGE_JSON, 605 | }); 606 | const installedHooks = getInstalledGitHooks( 607 | path.normalize( 608 | path.join(PROJECT_WITH_CONF_IN_PACKAGE_JSON, ".git", "hooks") 609 | ) 610 | ); 611 | expect(installedHooks).toEqual({ "pre-commit": TEST_SCRIPT }); 612 | }); 613 | }); 614 | }); 615 | 616 | describe("ENV vars features tests", () => { 617 | const GIT_USER_NAME = "github-actions"; 618 | const GIT_USER_EMAIL = "github-actions@github.com"; 619 | 620 | const initializeGitRepository = (path) => { 621 | execSync( 622 | `git init \ 623 | && git config user.name ${GIT_USER_NAME} \ 624 | && git config user.email ${GIT_USER_EMAIL}`, 625 | { cwd: path } 626 | ); 627 | }; 628 | 629 | const tryToPerformTestCommit = (path, env = process.env) => { 630 | try { 631 | execSync( 632 | 'git add . && git commit --allow-empty -m "Test commit" && git commit --allow-empty -am "Change commit msg"', 633 | { cwd: path, env: env, } 634 | ); 635 | return true; 636 | } catch (e) { 637 | return false; 638 | } 639 | }; 640 | 641 | const expectCommitToSucceed = (path) => { 642 | expect(tryToPerformTestCommit(path)).toBe(true); 643 | }; 644 | 645 | const expectCommitToFail = (path) => { 646 | expect(tryToPerformTestCommit(path)).toBe(false); 647 | }; 648 | 649 | beforeEach(async () => { 650 | initializeGitRepository(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 651 | createGitHooksFolder(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 652 | await simpleGitHooks.setHooksFromConfig(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 653 | }); 654 | 655 | describe("SKIP_SIMPLE_GIT_HOOKS", () => { 656 | afterEach(() => { 657 | delete process.env.SKIP_SIMPLE_GIT_HOOKS; 658 | }); 659 | 660 | it('commits successfully when SKIP_SIMPLE_GIT_HOOKS is set to "1"', () => { 661 | process.env.SKIP_SIMPLE_GIT_HOOKS = "1"; 662 | expectCommitToSucceed(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 663 | }); 664 | 665 | it("fails to commit when SKIP_SIMPLE_GIT_HOOKS is not set", () => { 666 | expectCommitToFail(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 667 | }); 668 | 669 | it('fails to commit when SKIP_SIMPLE_GIT_HOOKS is set to "0"', () => { 670 | process.env.SKIP_SIMPLE_GIT_HOOKS = "0"; 671 | expectCommitToFail(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 672 | }); 673 | 674 | it("fails to commit when SKIP_SIMPLE_GIT_HOOKS is set to a random string", () => { 675 | process.env.SKIP_SIMPLE_GIT_HOOKS = "simple-git-hooks"; 676 | expectCommitToFail(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 677 | }); 678 | }) 679 | 680 | describe("SIMPLE_GIT_HOOKS_RC", () => { 681 | afterEach(() => { 682 | delete process.env.SIMPLE_GIT_HOOKS_RC; 683 | }); 684 | 685 | it("fails to commit when SIMPLE_GIT_HOOKS_RC is not set", () => { 686 | expectCommitToFail(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 687 | }); 688 | 689 | it('commits successfully when SIMPLE_GIT_HOOKS_RC points to initrc_that_prevents_hook_fail.sh', () => { 690 | process.env.SIMPLE_GIT_HOOKS_RC = path.join(PROJECT_WITH_CONF_IN_PACKAGE_JSON, "initrc_that_prevents_hook_fail.sh"); 691 | expectCommitToSucceed(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 692 | }); 693 | 694 | it('fails to commit when SIMPLE_GIT_HOOKS_RC points to initrc_that_does_nothing.sh', () => { 695 | process.env.SIMPLE_GIT_HOOKS_RC = path.join(PROJECT_WITH_CONF_IN_PACKAGE_JSON, "initrc_that_does_nothing.sh"); 696 | expectCommitToFail(PROJECT_WITH_CONF_IN_PACKAGE_JSON); 697 | }); 698 | }) 699 | }); 700 | 701 | afterEach(() => { 702 | [ 703 | PROJECT_WITH_CONF_IN_SEPARATE_JS_ALT, 704 | PROJECT_WITH_CONF_IN_SEPARATE_CJS_ALT, 705 | PROJECT_WITH_CONF_IN_SEPARATE_CJS, 706 | PROJECT_WITH_CONF_IN_SEPARATE_JS, 707 | PROJECT_WITH_CONF_IN_SEPARATE_JSON_ALT, 708 | PROJECT_WITH_CONF_IN_SEPARATE_JSON, 709 | PROJECT_WITH_CONF_IN_PACKAGE_JSON, 710 | PROJECT_WITH_BAD_CONF_IN_PACKAGE_JSON_, 711 | PROJECT_WO_CONF, 712 | PROJECT_WITH_CONF_IN_PACKAGE_JSON, 713 | PROJECT_WITH_UNUSED_CONF_IN_PACKAGE_JSON, 714 | PROJECT_WITH_CUSTOM_CONF, 715 | ].forEach((testCase) => { 716 | removeGitHooksFolder(testCase); 717 | }); 718 | }); 719 | }) 720 | }) 721 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x305d6B3703c4FA5f489E3895cfe94D96574516D6' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /uninstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {removeHooks} = require("./simple-git-hooks"); 4 | 5 | 6 | /** 7 | * Removes the pre-commit from command in config by default 8 | */ 9 | async function uninstall() { 10 | console.log("[INFO] Removing git hooks from .git/hooks") 11 | 12 | try { 13 | await removeHooks() 14 | console.log("[INFO] Successfully removed all git hooks") 15 | } catch (e) { 16 | console.log("[INFO] Couldn't remove git hooks. Reason: " + e) 17 | } 18 | } 19 | 20 | uninstall() 21 | --------------------------------------------------------------------------------