Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ You can look up about git hooks on the [Pro Git book](https://git-scm.com/book/e

`simple-git-hooks` works well for small-sized projects when you need quickly set up hooks and forget about it.

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.

Also, this package allows you to set only one command per git hook.
This package allows you to set only one command per git hook.

If you need multiple verbose commands per git hook, flexible configuration or automatic update of git hooks, please check out the other packages:

Expand Down Expand Up @@ -93,13 +91,13 @@ If you need multiple verbose commands per git hook, flexible configuration or au

> There are more ways to configure the package. Check out [Additional configuration options](#additional-configuration-options).

3. Run the CLI script to update the git hooks with the commands from the config:
3. Run the CLI script to initialize the git hooks with the commands from the config:

```sh
# [Optional] These 2 steps can be skipped for non-husky users
git config core.hooksPath .git/hooks/
rm -rf .git/hooks

# Update ./git/hooks
npx simple-git-hooks
```
Expand All @@ -114,7 +112,9 @@ Now all the git hooks are created.

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)

Note that you should manually run `npx simple-git-hooks` **every time you change a command**.
The package will automatically update this command for you every time you commit. This works by hijacking the pre-commit hook and running `npx simple-git-hooks` behind the scenes. In addition, to make sure you run the newest pre-commit command, it will use the command from `package.json` and only leave a fallback command in the `.git/hooks` folder for usage with `preserveUnused`.

Note that you should manually run `npx simple-git-hooks` if you want to apply changes from non-commit related hooks. This command will also run initially when running `yarn` using a `postinstall` hook. So ideally you will only call the command once when you setup simple-git-hooks, and never having to worry about it again.

### Additional configuration options

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "simple-pre-commit-test-package",
"version": "1.0.0",
"simple-git-hooks": {
"pre-commit": "exit 1"
},
"devDependencies": {
"simple-pre-commit": "1.0.0"
}
}
Empty file.
11 changes: 9 additions & 2 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@
const {setHooksFromConfig} = require('./simple-git-hooks')

try {
setHooksFromConfig(process.cwd(), process.argv)
console.log('[INFO] Successfully set all git hooks')
const argv = process.argv.slice(2);
const args = {
silent: argv.includes('--silent'),
auto: argv.includes('--auto'),
}
setHooksFromConfig(process.cwd(), process.argv, args)
if (!args.silent) {
console.log('[INFO] Successfully set all git hooks')
}
} catch (e) {
console.log('[ERROR], Was not able to set git hooks. Error: ' + e)
}
55 changes: 48 additions & 7 deletions simple-git-hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const fs = require('fs')
const path = require('path')
const { spawn } = require('child_process')

const VALID_GIT_HOOKS = [
'applypatch-msg',
Expand Down Expand Up @@ -84,7 +85,7 @@ function getProjectRootDirectoryFromNodeModules(projectPath) {

const indexOfPnpmDir = projDir.indexOf('.pnpm')
if (indexOfPnpmDir > -1) {
return projDir.slice(0, indexOfPnpmDir - 1).join('/');
return projDir.slice(0, indexOfPnpmDir - 1).join('/')
}

// A yarn2 STAB
Expand All @@ -108,7 +109,7 @@ function getProjectRootDirectoryFromNodeModules(projectPath) {
* Checks the 'simple-git-hooks' in dependencies of the project
* @param {string} projectRootPath
* @throws TypeError if packageJsonData not an object
* @return {Boolean}
* @return {boolean}
*/
function checkSimpleGitHooksInDependencies(projectRootPath) {
if (typeof projectRootPath !== 'string') {
Expand All @@ -132,8 +133,9 @@ function checkSimpleGitHooksInDependencies(projectRootPath) {
* Parses the config and sets git hooks
* @param {string} projectRootPath
* @param {string[]} [argv]
* @param {object} [args]
*/
function setHooksFromConfig(projectRootPath=process.cwd(), argv=process.argv) {
function setHooksFromConfig(projectRootPath=process.cwd(), argv=process.argv, args={}) {
const customConfigPath = _getCustomConfigPath(argv)
const config = _getConfig(projectRootPath, customConfigPath)

Expand All @@ -145,10 +147,14 @@ function setHooksFromConfig(projectRootPath=process.cwd(), argv=process.argv) {

for (let hook of VALID_GIT_HOOKS) {
if (Object.prototype.hasOwnProperty.call(config, hook)) {
_setHook(hook, config[hook], projectRootPath)
_setHook(hook, config[hook], projectRootPath, args)
} else if (!preserveUnused.includes(hook)) {
_removeHook(hook, projectRootPath)
}

if (hook === 'pre-commit' && args.auto) {
_runPreCommitCommand(config[hook])
}
}
}

Expand All @@ -157,12 +163,14 @@ function setHooksFromConfig(projectRootPath=process.cwd(), argv=process.argv) {
* @param {string} hook
* @param {string} command
* @param {string} projectRoot
* @param {object} args
* @private
*/
function _setHook(hook, command, projectRoot=process.cwd()) {
function _setHook(hook, command, projectRoot=process.cwd(), args) {
const gitRoot = getGitProjectRoot(projectRoot)

const hookCommand = "#!/bin/sh\n" + command
const updateHooksComamnd = `${_getPackageManagerRunCommand(projectRoot)} simple-git-hooks --auto --silent`
const hookCommand = "#!/bin/sh\n" + (hook === 'pre-commit' ? updateHooksComamnd : command)
const hookDirectory = gitRoot + '/hooks/'
const hookPath = path.normalize(hookDirectory + hook)

Expand All @@ -174,7 +182,9 @@ function _setHook(hook, command, projectRoot=process.cwd()) {
fs.writeFileSync(hookPath, hookCommand)
fs.chmodSync(hookPath, 0o0755)

console.log(`[INFO] Successfully set the ${hook} with command: ${command}`)
if (!args.silent) {
console.log(`[INFO] Successfully set the ${hook} with command: ${command}`)
}
}

/**
Expand Down Expand Up @@ -202,6 +212,20 @@ function _removeHook(hook, projectRoot=process.cwd()) {
}
}

/**
* Runs the configured pre-commit if it exists, or exit with an error to propagate to hook command
* @param {string} command
* @private
*/
function _runPreCommitCommand(command) {
if (command) {
spawn(command, {
shell: true,
stdio: [process.stdin, process.stdout, process.stderr, 'pipe']
}).on('exit', code => process.exit(code))
}
}

/** Reads package.json file, returns package.json content and path
* @param {string} projectPath - a path to the project, defaults to process.cwd
* @return {{packageJsonContent: any, packageJsonPath: string}}
Expand All @@ -224,11 +248,25 @@ function _getPackageJson(projectPath = process.cwd()) {
return { packageJsonContent: JSON.parse(packageJsonDataRaw), packageJsonPath: targetPackageJson }
}

/**
* Gets a run command based on the current package manager
* @param {string} projectRoot
* @return {string}
* @private
*/
function _getPackageManagerRunCommand(projectRoot=process.cwd()) {
const usesYarn = fs.existsSync(path.resolve(projectRoot, 'yarn.lock'))
const usesYarn2 = fs.existsSync(path.resolve(projectRoot, '.yarn'))

return usesYarn ? 'yarn run --silent' : usesYarn2 ? 'yarn dxl' : 'npx'
}

/**
* Takes the first argument from current process argv and returns it
* Returns empty string when argument wasn't passed
* @param {string[]} [argv]
* @returns {string}
* @private
*/
function _getCustomConfigPath(argv=[]) {
// We'll run as one of the following:
Expand Down Expand Up @@ -287,6 +325,7 @@ function _getConfig(projectRootPath, configFileName='') {
* @throws TypeError if packageJsonPath is not a string
* @throws Error if package.json couldn't be read or was not validated
* @return {{string: string} | undefined}
* @private
*/
function _getConfigFromPackageJson(projectRootPath = process.cwd()) {
const {packageJsonContent} = _getPackageJson(projectRootPath)
Expand All @@ -299,6 +338,7 @@ function _getConfigFromPackageJson(projectRootPath = process.cwd()) {
* @param {string} projectRootPath
* @param {string} fileName
* @return {{string: string} | undefined}
* @private
*/
function _getConfigFromFile(projectRootPath, fileName) {
if (typeof projectRootPath !== "string") {
Expand Down Expand Up @@ -326,6 +366,7 @@ function _getConfigFromFile(projectRootPath, fileName) {
* Validates the config, checks that every git hook or option is named correctly
* @return {boolean}
* @param {{string: string}} config
* @private
*/
function _validateHooks(config) {

Expand Down
34 changes: 23 additions & 11 deletions simple-git-hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ test('returns false if simple pre commit isn`t in deps', () => {
// Set and remove git hooks

const testsFolder = path.normalize(path.join(process.cwd(), '_tests'))
const updateHookCommand = `npx simple-git-hooks --auto --silent`;

// Correct configurations

const projectWithConfigurationInPackageJsonPath = path.normalize(path.join(testsFolder, 'project_with_configuration_in_package_json'))
const projectWithConfigurationInPackageJsonPathUsingYarn = path.normalize(path.join(testsFolder, 'project_with_configuration_in_package_json_with_yarn'))
const projectWithConfigurationInSeparateCjsPath = path.normalize(path.join(testsFolder, 'project_with_configuration_in_separate_cjs'))
const projectWithConfigurationInSeparateJsPath = path.normalize(path.join(testsFolder, 'project_with_configuration_in_separate_js'))
const projectWithConfigurationInAlternativeSeparateCjsPath = path.normalize(path.join(testsFolder, 'project_with_configuration_in_alternative_separate_cjs'))
Expand Down Expand Up @@ -127,7 +129,7 @@ test('creates git hooks if configuration is correct from .simple-git-hooks.js',

spc.setHooksFromConfig(projectWithConfigurationInAlternativeSeparateJsPath)
const installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInAlternativeSeparateJsPath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))

removeGitHooksFolder(projectWithConfigurationInAlternativeSeparateJsPath)
})
Expand All @@ -137,7 +139,7 @@ test('creates git hooks if configuration is correct from .simple-git-hooks.cjs',

spc.setHooksFromConfig(projectWithConfigurationInAlternativeSeparateCjsPath)
const installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInAlternativeSeparateCjsPath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))

removeGitHooksFolder(projectWithConfigurationInAlternativeSeparateCjsPath)
})
Expand All @@ -147,7 +149,7 @@ test('creates git hooks if configuration is correct from simple-git-hooks.cjs',

spc.setHooksFromConfig(projectWithConfigurationInSeparateCjsPath)
const installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInSeparateCjsPath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))

removeGitHooksFolder(projectWithConfigurationInSeparateCjsPath)
})
Expand All @@ -157,7 +159,7 @@ test('creates git hooks if configuration is correct from simple-git-hooks.js', (

spc.setHooksFromConfig(projectWithConfigurationInSeparateJsPath)
const installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInSeparateJsPath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))

removeGitHooksFolder(projectWithConfigurationInSeparateJsPath)
})
Expand All @@ -167,7 +169,7 @@ test('creates git hooks if configuration is correct from .simple-git-hooks.json'

spc.setHooksFromConfig(projectWithConfigurationInAlternativeSeparateJsonPath)
const installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInAlternativeSeparateJsonPath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))

removeGitHooksFolder(projectWithConfigurationInAlternativeSeparateJsonPath)
})
Expand All @@ -177,7 +179,7 @@ test('creates git hooks if configuration is correct from simple-git-hooks.json',

spc.setHooksFromConfig(projectWithConfigurationInSeparateJsonPath)
const installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInSeparateJsonPath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))

removeGitHooksFolder(projectWithConfigurationInSeparateJsonPath)
})
Expand All @@ -187,11 +189,21 @@ test('creates git hooks if configuration is correct from package.json', () => {

spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPath)
const installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInPackageJsonPath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`}))

removeGitHooksFolder(projectWithConfigurationInPackageJsonPath)
})

test('updates hooks using Yarn when a yarn.lock is present', () => {
createGitHooksFolder(projectWithConfigurationInPackageJsonPathUsingYarn)

spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPathUsingYarn)
const installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInPackageJsonPathUsingYarn, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand.replace('npx', 'yarn run --silent')} || exit 1`}))

removeGitHooksFolder(projectWithConfigurationInPackageJsonPathUsingYarn)
})

test('fails to create git hooks if configuration contains bad git hooks', () => {
createGitHooksFolder(projectWithIncorrectConfigurationInPackageJson)

Expand All @@ -214,7 +226,7 @@ test('removes git hooks', () => {
spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPath)

let installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInPackageJsonPath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`}))

spc.removeHooks(projectWithConfigurationInPackageJsonPath)

Expand All @@ -237,7 +249,7 @@ test('creates git hooks and removes unused git hooks', () => {
spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPath)

installedHooks = getInstalledGitHooks(installedHooksDir);
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`}))

removeGitHooksFolder(projectWithConfigurationInPackageJsonPath)
})
Expand All @@ -257,7 +269,7 @@ test('creates git hooks and removes unused but preserves specific git hooks', ()
spc.setHooksFromConfig(projectWithUnusedConfigurationInPackageJsonPath)

installedHooks = getInstalledGitHooks(installedHooksDir);
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'commit-msg': '# do nothing', 'pre-commit':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'commit-msg': '# do nothing', 'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`}))

removeGitHooksFolder(projectWithUnusedConfigurationInPackageJsonPath)
})
Expand All @@ -271,7 +283,7 @@ test.each([

spc.setHooksFromConfig(projectWithCustomConfigurationFilePath, args)
const installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithCustomConfigurationFilePath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\n${updateHookCommand} || exit 1`, 'pre-push':`#!/bin/sh\nexit 1`}))

removeGitHooksFolder(projectWithCustomConfigurationFilePath)
})