Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add env var to bypass hooks execution #96

Merged
merged 10 commits into from
Jan 4, 2024
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ If you need multiple verbose commands per git hook, flexible configuration or au
# [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 Down Expand Up @@ -158,6 +158,18 @@ You should use `--no-verify` option

https://bobbyhadz.com/blog/git-commit-skip-hooks#skip-git-commit-hooks

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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's best to also include use cases. I've seen people struggle with skipping git hooks when using 3rd party git clients

Normally (from the terminal) you can bypass hooks by using --skip-hooks option if im not mistaken :-)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like so:

Using with 3rd party clients:

...


```sh
# Set the environment variable
export SKIP_SIMPLE_GIT_HOOKS=1

# Subsequent Git commands will skip the hooks
git add .
git commit -m "commit message" # pre-commit hooks are bypassed
git push origin main # pre-push hooks are bypassed
```

### When migrating from `husky` git hooks are not running

**Why is this happening?**
Expand Down
10 changes: 9 additions & 1 deletion simple-git-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ const VALID_GIT_HOOKS = [

const VALID_OPTIONS = ['preserveUnused']

const PREPEND_SCRIPT =
"#!/bin/sh\n\n" +
'if [ "$SKIP_SIMPLE_GIT_HOOKS" = "1" ]; then\n' +
' echo "[INFO] SKIP_SIMPLE_GIT_HOOKS is set to 1, skipping hook."\n' +
" exit 0\n" +
"fi\n\n";

/**
* Recursively gets the .git folder path from provided directory
* @param {string} directory
Expand Down Expand Up @@ -178,7 +185,7 @@ function _setHook(hook, command, projectRoot=process.cwd()) {
return
}

const hookCommand = "#!/bin/sh\n" + command
const hookCommand = PREPEND_SCRIPT + command
const hookDirectory = gitRoot + '/hooks/'
const hookPath = path.normalize(hookDirectory + hook)

Expand Down Expand Up @@ -361,4 +368,5 @@ module.exports = {
getProjectRootDirectoryFromNodeModules,
getGitProjectRoot,
removeHooks,
PREPEND_SCRIPT
}
131 changes: 100 additions & 31 deletions simple-git-hooks.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const fs = require('fs')
const spc = require("./simple-git-hooks");
const path = require("path")
const { execSync } = require('child_process');

const { version: packageVersion } = require('./package.json');

Expand Down Expand Up @@ -126,108 +127,124 @@ function getInstalledGitHooks(hooksDir) {
return result
}

afterEach(() => {
afterEach(() => {
[
projectWithConfigurationInAlternativeSeparateJsPath,
projectWithConfigurationInAlternativeSeparateCjsPath,
projectWithConfigurationInSeparateCjsPath,
projectWithConfigurationInSeparateJsPath,
projectWithConfigurationInAlternativeSeparateJsonPath,
projectWithConfigurationInSeparateJsonPath,
projectWithConfigurationInPackageJsonPath,
projectWithIncorrectConfigurationInPackageJson,
projectWithoutConfiguration,
projectWithConfigurationInPackageJsonPath,
projectWithUnusedConfigurationInPackageJsonPath,
projectWithCustomConfigurationFilePath,
].forEach((testCase) => {
delete process.env.SKIP_SIMPLE_GIT_HOOKS;
removeGitHooksFolder(testCase);
});
});
});

test('creates git hooks if configuration is correct from .simple-git-hooks.js', () => {
createGitHooksFolder(projectWithConfigurationInAlternativeSeparateJsPath)

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`}))

removeGitHooksFolder(projectWithConfigurationInAlternativeSeparateJsPath)
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`${spc.PREPEND_SCRIPT}exit 1`, 'pre-push':`${spc.PREPEND_SCRIPT}exit 1`}))
})


test('creates git hooks if configuration is correct from .simple-git-hooks.cjs', () => {
createGitHooksFolder(projectWithConfigurationInAlternativeSeparateCjsPath)

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`}))

removeGitHooksFolder(projectWithConfigurationInAlternativeSeparateCjsPath)
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`${spc.PREPEND_SCRIPT}exit 1`, 'pre-push':`${spc.PREPEND_SCRIPT}exit 1`}))
})


test('creates git hooks if configuration is correct from simple-git-hooks.cjs', () => {
createGitHooksFolder(projectWithConfigurationInSeparateCjsPath)

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`}))

removeGitHooksFolder(projectWithConfigurationInSeparateCjsPath)
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`${spc.PREPEND_SCRIPT}exit 1`, 'pre-push':`${spc.PREPEND_SCRIPT}exit 1`}))
})


test('creates git hooks if configuration is correct from simple-git-hooks.js', () => {
createGitHooksFolder(projectWithConfigurationInSeparateJsPath)

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`}))

removeGitHooksFolder(projectWithConfigurationInSeparateJsPath)
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`${spc.PREPEND_SCRIPT}exit 1`, 'pre-push':`${spc.PREPEND_SCRIPT}exit 1`}))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, its best to move this to separate variables, so the code not repeated:

Im thinking of something like this: wyt?

const TEST_SCRIPT = ${spc.PREPEND_SCRIPT}${exit 1}`

Then we can use it in tests like so:

expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':``${TEST_SCRIPT}, 'pre-push':${spc.TEST_SCRIPT}}))

you can also try to use loadash compare functions, they can compare two objects deeply, so we won't need to use STRINGIFY hack

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used lodash.isequal package for this

})


test('creates git hooks if configuration is correct from .simple-git-hooks.json', () => {
createGitHooksFolder(projectWithConfigurationInAlternativeSeparateJsonPath)

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`}))

removeGitHooksFolder(projectWithConfigurationInAlternativeSeparateJsonPath)
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`${spc.PREPEND_SCRIPT}exit 1`, 'pre-push':`${spc.PREPEND_SCRIPT}exit 1`}))
})


test('creates git hooks if configuration is correct from simple-git-hooks.json', () => {
createGitHooksFolder(projectWithConfigurationInSeparateJsonPath)

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`}))

removeGitHooksFolder(projectWithConfigurationInSeparateJsonPath)
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`${spc.PREPEND_SCRIPT}exit 1`, 'pre-push':`${spc.PREPEND_SCRIPT}exit 1`}))
})


test('creates git hooks if configuration is correct from package.json', () => {
createGitHooksFolder(projectWithConfigurationInPackageJsonPath)

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':`${spc.PREPEND_SCRIPT}exit 1`}))

removeGitHooksFolder(projectWithConfigurationInPackageJsonPath)
})


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

expect(() => spc.setHooksFromConfig(projectWithIncorrectConfigurationInPackageJson)).toThrow('[ERROR] Config was not in correct format. Please check git hooks or options name')

removeGitHooksFolder(projectWithIncorrectConfigurationInPackageJson)
})


test('fails to create git hooks if not configured', () => {
createGitHooksFolder(projectWithoutConfiguration)

expect(() => spc.setHooksFromConfig(projectWithoutConfiguration)).toThrow('[ERROR] Config was not found! Please add `.simple-git-hooks.js` or `simple-git-hooks.js` or `.simple-git-hooks.json` or `simple-git-hooks.json` or `simple-git-hooks` entry in package.json.')

removeGitHooksFolder(projectWithoutConfiguration)
})


test('removes git hooks', () => {
createGitHooksFolder(projectWithConfigurationInPackageJsonPath)

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':`${spc.PREPEND_SCRIPT}exit 1`}))

spc.removeHooks(projectWithConfigurationInPackageJsonPath)

installedHooks = getInstalledGitHooks(path.normalize(path.join(projectWithConfigurationInPackageJsonPath, '.git', 'hooks')))
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({}))

removeGitHooksFolder(projectWithConfigurationInPackageJsonPath)
})


test('creates git hooks and removes unused git hooks', () => {
createGitHooksFolder(projectWithConfigurationInPackageJsonPath)

Expand All @@ -241,9 +258,8 @@ 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':`${spc.PREPEND_SCRIPT}exit 1`}))

removeGitHooksFolder(projectWithConfigurationInPackageJsonPath)
})


Expand All @@ -261,11 +277,11 @@ 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':`${spc.PREPEND_SCRIPT}exit 1`}))

removeGitHooksFolder(projectWithUnusedConfigurationInPackageJsonPath)
})


test.each([
['npx', 'simple-git-hooks', './git-hooks.js'],
['node', require.resolve(`./cli`), './git-hooks.js'],
Expand All @@ -275,7 +291,60 @@ 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`}))

removeGitHooksFolder(projectWithCustomConfigurationFilePath)
expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`${spc.PREPEND_SCRIPT}exit 1`, 'pre-push':`${spc.PREPEND_SCRIPT}exit 1`}))
})


test("bypasses hooks when SKIP_SIMPLE_GIT_HOOKS is set to 1", () => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also try grouping these test cases using describe syntax. It it a good practice

execSync("git init \
&& git config user.name github-actions \
&& git config user.email [email protected]", { cwd: projectWithConfigurationInPackageJsonPath });

createGitHooksFolder(projectWithConfigurationInPackageJsonPath);

spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPath);
process.env.SKIP_SIMPLE_GIT_HOOKS = '1'; // Set environment variable
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you might need to unset variable at the end of test case.. Maybe move to after Each ? Not sure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I was clearing it in global afterEach, now moved to local afterEach in describe simple git hooks env var tests

let errorOccured = false;
try {
execSync('git add . && git commit --allow-empty -m "Test commit" && git commit --allow-empty -am "Change commit msg"', {
cwd: projectWithConfigurationInPackageJsonPath,
env: { ...process.env },
});
} catch (e) {
console.log(e)
console.log(e.stdout.toString())
console.log(e.stderr.toString())
errorOccured = true;
}

expect(errorOccured).toBe(false);
});


test("hook executes when bypass var is not set", () => {
execSync(
"git init \
&& git config user.name github-actions \
&& git config user.email [email protected]",
{ cwd: projectWithConfigurationInPackageJsonPath }
);

createGitHooksFolder(projectWithConfigurationInPackageJsonPath);

spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPath);

let errorOccured = false;
try {
execSync(
'git add . && git commit --allow-empty -m "Test commit" && git commit --allow-empty -am "Change commit msg"',
{
cwd: projectWithConfigurationInPackageJsonPath,
}
);
} catch (e) {
errorOccured = true;
}

expect(errorOccured).toBe(true);
});