diff --git a/README.md b/README.md index db2f7eb..117c78e 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -156,7 +156,32 @@ npm uninstall simple-git-hooks You should use `--no-verify` option -https://bobbyhadz.com/blog/git-commit-skip-hooks#skip-git-commit-hooks +```sh +git commit -m "commit message" --no-verify # -n for shorthand +``` + +you can read more about it here 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. + +```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 +``` + +### Skipping Hooks in 3rd party git clients + +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. + +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. + +If these options are not available, you may need to resort to using the terminal for skipping hooks. ### When migrating from `husky` git hooks are not running diff --git a/package.json b/package.json index 026c599..dde1e2b 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "clean-publish": "^4.2.0", "eslint": "^7.19.0", "jest": "^26.6.3", - "lint-staged": "^10.5.4" + "lint-staged": "^10.5.4", + "lodash.isequal": "^4.5.0" } } diff --git a/simple-git-hooks.js b/simple-git-hooks.js index c499586..65db7f1 100644 --- a/simple-git-hooks.js +++ b/simple-git-hooks.js @@ -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 @@ -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) @@ -361,4 +368,5 @@ module.exports = { getProjectRootDirectoryFromNodeModules, getGitProjectRoot, removeHooks, + PREPEND_SCRIPT } diff --git a/simple-git-hooks.test.js b/simple-git-hooks.test.js index d4338e7..9710133 100644 --- a/simple-git-hooks.test.js +++ b/simple-git-hooks.test.js @@ -1,103 +1,180 @@ -const fs = require('fs') +const fs = require("fs"); const spc = require("./simple-git-hooks"); -const path = require("path") - -const { version: packageVersion } = require('./package.json'); +const path = require("path"); +const { execSync } = require("child_process"); +const isEqual = require("lodash.isequal"); +const { version: packageVersion } = require("./package.json"); // Get project root directory -test('getProjectRootDirectory returns correct dir in typical case:', () => { - expect(spc.getProjectRootDirectoryFromNodeModules('var/my-project/node_modules/simple-git-hooks')).toBe('var/my-project') -}) - -test('getProjectRootDirectory returns correct dir when used with windows delimiters:', () => { - expect(spc.getProjectRootDirectoryFromNodeModules('user\\allProjects\\project\\node_modules\\simple-git-hooks')).toBe('user/allProjects/project') -}) - -test('getProjectRootDirectory falls back to undefined when we are not in node_modules:', () => { - expect(spc.getProjectRootDirectoryFromNodeModules('var/my-project/simple-git-hooks')).toBe(undefined) -}) - -test('getProjectRootDirectory return correct dir when installed using pnpm:', () => { - expect(spc.getProjectRootDirectoryFromNodeModules(`var/my-project/node_modules/.pnpm/simple-git-hooks@${packageVersion}`)).toBe('var/my-project') - expect(spc.getProjectRootDirectoryFromNodeModules(`var/my-project/node_modules/.pnpm/simple-git-hooks@${packageVersion}/node_modules/simple-git-hooks`)).toBe('var/my-project') -}) - -test('getProjectRootDirectory return correct dir when installed using yarn3 nodeLinker pnpm:', () => { - expect(spc.getProjectRootDirectoryFromNodeModules(`var/my-project/node_modules/.store/simple-git-hooks@${packageVersion}/node_modules/simple-git-hooks`)).toBe('var/my-project') -}) - +describe("getProjectRootDirectory", () => { + it("returns correct dir in typical case:", () => { + expect( + spc.getProjectRootDirectoryFromNodeModules( + "var/my-project/node_modules/simple-git-hooks" + ) + ).toBe("var/my-project"); + }); + + it("returns correct dir when used with windows delimiters:", () => { + expect( + spc.getProjectRootDirectoryFromNodeModules( + "user\\allProjects\\project\\node_modules\\simple-git-hooks" + ) + ).toBe("user/allProjects/project"); + }); + + it("falls back to undefined when we are not in node_modules:", () => { + expect( + spc.getProjectRootDirectoryFromNodeModules( + "var/my-project/simple-git-hooks" + ) + ).toBe(undefined); + }); + + it("return correct dir when installed using pnpm:", () => { + expect( + spc.getProjectRootDirectoryFromNodeModules( + `var/my-project/node_modules/.pnpm/simple-git-hooks@${packageVersion}` + ) + ).toBe("var/my-project"); + expect( + spc.getProjectRootDirectoryFromNodeModules( + `var/my-project/node_modules/.pnpm/simple-git-hooks@${packageVersion}/node_modules/simple-git-hooks` + ) + ).toBe("var/my-project"); + }); + + it("return correct dir when installed using yarn3 nodeLinker pnpm:", () => { + expect( + spc.getProjectRootDirectoryFromNodeModules( + `var/my-project/node_modules/.store/simple-git-hooks@${packageVersion}/node_modules/simple-git-hooks` + ) + ).toBe("var/my-project"); + }); +}); // Get git root -const gitProjectRoot = path.normalize(path.join(__dirname, '.git')) -const currentPath = path.normalize(path.join(__dirname)) -const currentFilePath = path.normalize(path.join(__filename)) +describe("getGitProjectRoot", () => { + const gitProjectRoot = path.normalize(path.join(__dirname, ".git")); + const currentPath = path.normalize(path.join(__dirname)); + const currentFilePath = path.normalize(path.join(__filename)); -test('get git root works from .git directory itself', () => { - expect(spc.getGitProjectRoot(gitProjectRoot)).toBe(gitProjectRoot) -}) + it("works from .git directory itself", () => { + expect(spc.getGitProjectRoot(gitProjectRoot)).toBe(gitProjectRoot); + }); -test('get git root works from any directory', () => { - expect(spc.getGitProjectRoot(currentPath)).toBe(gitProjectRoot) -}) - -test('get git root works from any file', () => { - expect(spc.getGitProjectRoot(currentFilePath)).toBe(gitProjectRoot) -}) + it("works from any directory", () => { + expect(spc.getGitProjectRoot(currentPath)).toBe(gitProjectRoot); + }); + it("works from any file", () => { + expect(spc.getGitProjectRoot(currentFilePath)).toBe(gitProjectRoot); + }); +}); // Check if simple-pre-commit is in devDependencies or dependencies in package json -const correctPackageJsonProjectPath = path.normalize(path.join(process.cwd(), '_tests', 'project_with_simple_pre_commit_in_deps')) -const correctPackageJsonProjectPath_2 = path.normalize(path.join(process.cwd(), '_tests', 'project_with_simple_pre_commit_in_dev_deps')) -const incorrectPackageJsonProjectPath = path.normalize(path.join(process.cwd(), '_tests', 'project_without_simple_pre_commit')) - -test('returns true if simple pre commit really in devDeps', () => { - expect(spc.checkSimpleGitHooksInDependencies(correctPackageJsonProjectPath)).toBe(true) -}) - -test('returns true if simple pre commit really in deps', () => { - expect(spc.checkSimpleGitHooksInDependencies(correctPackageJsonProjectPath_2)).toBe(true) -}) - -test('returns false if simple pre commit isn`t in deps', () => { - expect(spc.checkSimpleGitHooksInDependencies(incorrectPackageJsonProjectPath)).toBe(false) -}) - +describe("Check if simple-pre-commit is in devDependencies or dependencies in package json", () => { + const correctPackageJsonProjectPath = path.normalize( + path.join(process.cwd(), "_tests", "project_with_simple_pre_commit_in_deps") + ); + const correctPackageJsonProjectPath_2 = path.normalize( + path.join( + process.cwd(), + "_tests", + "project_with_simple_pre_commit_in_dev_deps" + ) + ); + const incorrectPackageJsonProjectPath = path.normalize( + path.join(process.cwd(), "_tests", "project_without_simple_pre_commit") + ); + it("returns true if simple pre commit really in devDeps", () => { + expect( + spc.checkSimpleGitHooksInDependencies(correctPackageJsonProjectPath) + ).toBe(true); + }); + + it("returns true if simple pre commit really in deps", () => { + expect( + spc.checkSimpleGitHooksInDependencies(correctPackageJsonProjectPath_2) + ).toBe(true); + }); + + it("returns false if simple pre commit isn`t in deps", () => { + expect( + spc.checkSimpleGitHooksInDependencies(incorrectPackageJsonProjectPath) + ).toBe(false); + }); +}); // Set and remove git hooks -const testsFolder = path.normalize(path.join(process.cwd(), '_tests')) +const testsFolder = path.normalize(path.join(process.cwd(), "_tests")); // Correct configurations -const projectWithConfigurationInPackageJsonPath = path.normalize(path.join(testsFolder, 'project_with_configuration_in_package_json')) -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')) -const projectWithConfigurationInAlternativeSeparateJsPath = path.normalize(path.join(testsFolder, 'project_with_configuration_in_alternative_separate_js')) -const projectWithConfigurationInSeparateJsonPath = path.normalize(path.join(testsFolder, 'project_with_configuration_in_separate_json')) -const projectWithConfigurationInAlternativeSeparateJsonPath = path.normalize(path.join(testsFolder, 'project_with_configuration_in_alternative_separate_json')) -const projectWithUnusedConfigurationInPackageJsonPath = path.normalize(path.join(testsFolder, 'project_with_unused_configuration_in_package_json')) -const projectWithCustomConfigurationFilePath = path.normalize(path.join(testsFolder, 'project_with_custom_configuration')) +const projectWithConfigurationInPackageJsonPath = path.normalize( + path.join(testsFolder, "project_with_configuration_in_package_json") +); +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" + ) +); +const projectWithConfigurationInAlternativeSeparateJsPath = path.normalize( + path.join( + testsFolder, + "project_with_configuration_in_alternative_separate_js" + ) +); +const projectWithConfigurationInSeparateJsonPath = path.normalize( + path.join(testsFolder, "project_with_configuration_in_separate_json") +); +const projectWithConfigurationInAlternativeSeparateJsonPath = path.normalize( + path.join( + testsFolder, + "project_with_configuration_in_alternative_separate_json" + ) +); +const projectWithUnusedConfigurationInPackageJsonPath = path.normalize( + path.join(testsFolder, "project_with_unused_configuration_in_package_json") +); +const projectWithCustomConfigurationFilePath = path.normalize( + path.join(testsFolder, "project_with_custom_configuration") +); // Incorrect configurations -const projectWithIncorrectConfigurationInPackageJson = path.normalize(path.join(testsFolder, 'project_with_incorrect_configuration_in_package_json')) -const projectWithoutConfiguration = path.normalize(path.join(testsFolder, 'project_without_configuration')) +const projectWithIncorrectConfigurationInPackageJson = path.normalize( + path.join(testsFolder, "project_with_incorrect_configuration_in_package_json") +); +const projectWithoutConfiguration = path.normalize( + path.join(testsFolder, "project_without_configuration") +); + +const TEST_SCRIPT = `${spc.PREPEND_SCRIPT}exit 1`; +const COMMON_GIT_HOOKS = { "pre-commit": TEST_SCRIPT, "pre-push": TEST_SCRIPT }; /** * Creates .git/hooks dir from root * @param {string} root */ function createGitHooksFolder(root) { - if (fs.existsSync(root + '/.git')) { - return - } - fs.mkdirSync(root + '/.git') - fs.mkdirSync(root + '/.git/hooks') + if (fs.existsSync(root + "/.git")) { + return; + } + fs.mkdirSync(root + "/.git"); + fs.mkdirSync(root + "/.git/hooks"); } /** @@ -105,9 +182,9 @@ function createGitHooksFolder(root) { * @param {string} root */ function removeGitHooksFolder(root) { - if (fs.existsSync(root + '/.git')) { - fs.rmdirSync(root + '/.git', { recursive: true }) - } + if (fs.existsSync(root + "/.git")) { + fs.rmdirSync(root + "/.git", { recursive: true }); + } } /** @@ -115,167 +192,337 @@ function removeGitHooksFolder(root) { * @return { {string: string} } */ function getInstalledGitHooks(hooksDir) { - const result = {} + const result = {}; - const hooks = fs.readdirSync(hooksDir) + const hooks = fs.readdirSync(hooksDir); - for (let hook of hooks) { - result[hook] = fs.readFileSync(path.normalize(path.join(hooksDir, hook))).toString() - } + for (let hook of hooks) { + result[hook] = fs + .readFileSync(path.normalize(path.join(hooksDir, hook))) + .toString(); + } - return result + return result; } -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) -}) - -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) -}) - -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) -}) - -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) -}) - -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) -}) - -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) -}) - -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`})) - - 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`})) - - 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) - - const installedHooksDir = path.normalize(path.join(projectWithConfigurationInPackageJsonPath, '.git', 'hooks')) - - fs.writeFileSync(path.resolve(installedHooksDir, 'pre-push'), "# do nothing") +afterEach(() => { + [ + projectWithConfigurationInAlternativeSeparateJsPath, + projectWithConfigurationInAlternativeSeparateCjsPath, + projectWithConfigurationInSeparateCjsPath, + projectWithConfigurationInSeparateJsPath, + projectWithConfigurationInAlternativeSeparateJsonPath, + projectWithConfigurationInSeparateJsonPath, + projectWithConfigurationInPackageJsonPath, + projectWithIncorrectConfigurationInPackageJson, + projectWithoutConfiguration, + projectWithConfigurationInPackageJsonPath, + projectWithUnusedConfigurationInPackageJsonPath, + projectWithCustomConfigurationFilePath, + ].forEach((testCase) => { + removeGitHooksFolder(testCase); + }); +}); + +describe("Hook Creation with Valid Configurations", () => { + it("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(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); + }); + + it("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(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); + }); + + it("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(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); + }); + + it("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(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); + }); + + it("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(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); + }); + + it("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(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); + }); + + it("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(isEqual(installedHooks, { "pre-commit": TEST_SCRIPT })).toBe(true); + }); +}); + +describe("Tests to fail to create git hooks if configuration is incorrect", () => { + it("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" + ); + }); + + it("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." + ); + }); +}); + +describe("Hook Removal and Preservation Functionality", () => { + it("removes git hooks", () => { + createGitHooksFolder(projectWithConfigurationInPackageJsonPath); + + spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPath); + + let installedHooks = getInstalledGitHooks( + path.normalize( + path.join(projectWithConfigurationInPackageJsonPath, ".git", "hooks") + ) + ); + expect(isEqual(installedHooks, { "pre-commit": TEST_SCRIPT })).toBe(true); + + spc.removeHooks(projectWithConfigurationInPackageJsonPath); + + installedHooks = getInstalledGitHooks( + path.normalize( + path.join(projectWithConfigurationInPackageJsonPath, ".git", "hooks") + ) + ); + expect(isEqual(installedHooks, {})).toBe(true); + }); + + it("creates git hooks and removes unused git hooks", () => { + createGitHooksFolder(projectWithConfigurationInPackageJsonPath); + + const installedHooksDir = path.normalize( + path.join(projectWithConfigurationInPackageJsonPath, ".git", "hooks") + ); + + fs.writeFileSync( + path.resolve(installedHooksDir, "pre-push"), + "# do nothing" + ); let installedHooks = getInstalledGitHooks(installedHooksDir); - expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-push':'# do nothing'})) + expect(isEqual(installedHooks, { "pre-push": "# do nothing" })).toBe(true); - spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPath) + spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPath); installedHooks = getInstalledGitHooks(installedHooksDir); - expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'pre-commit':`#!/bin/sh\nexit 1`})) - - removeGitHooksFolder(projectWithConfigurationInPackageJsonPath) -}) - - -test('creates git hooks and removes unused but preserves specific git hooks', () => { - createGitHooksFolder(projectWithUnusedConfigurationInPackageJsonPath) - - const installedHooksDir = path.normalize(path.join(projectWithUnusedConfigurationInPackageJsonPath, '.git', 'hooks')) - - fs.writeFileSync(path.resolve(installedHooksDir, 'commit-msg'), "# do nothing") - fs.writeFileSync(path.resolve(installedHooksDir, 'pre-push'), "# do nothing") + expect(isEqual(installedHooks, { "pre-commit": TEST_SCRIPT })).toBe(true); + }); + + it("creates git hooks and removes unused but preserves specific git hooks", () => { + createGitHooksFolder(projectWithUnusedConfigurationInPackageJsonPath); + + const installedHooksDir = path.normalize( + path.join( + projectWithUnusedConfigurationInPackageJsonPath, + ".git", + "hooks" + ) + ); + + fs.writeFileSync( + path.resolve(installedHooksDir, "commit-msg"), + "# do nothing" + ); + fs.writeFileSync( + path.resolve(installedHooksDir, "pre-push"), + "# do nothing" + ); let installedHooks = getInstalledGitHooks(installedHooksDir); - expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'commit-msg': '# do nothing', 'pre-push':'# do nothing'})) + expect( + isEqual(installedHooks, { + "commit-msg": "# do nothing", + "pre-push": "# do nothing", + }) + ).toBe(true); - spc.setHooksFromConfig(projectWithUnusedConfigurationInPackageJsonPath) + spc.setHooksFromConfig(projectWithUnusedConfigurationInPackageJsonPath); installedHooks = getInstalledGitHooks(installedHooksDir); - expect(JSON.stringify(installedHooks)).toBe(JSON.stringify({'commit-msg': '# do nothing', 'pre-commit':`#!/bin/sh\nexit 1`})) - - removeGitHooksFolder(projectWithUnusedConfigurationInPackageJsonPath) -}) - -test.each([ - ['npx', 'simple-git-hooks', './git-hooks.js'], - ['node', require.resolve(`./cli`), './git-hooks.js'], - ['node', require.resolve(`./cli`), require.resolve(`${projectWithCustomConfigurationFilePath}/git-hooks.js`)], -])('creates git hooks and removes unused but preserves specific git hooks for command: %s %s %s', (...args) => { - createGitHooksFolder(projectWithCustomConfigurationFilePath) - - 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( + isEqual(installedHooks, { + "commit-msg": "# do nothing", + "pre-commit": TEST_SCRIPT, + }) + ).toBe(true); + }); +}); + +describe("CLI Command-Based Hook Management", () => { + const testCases = [ + ["npx", "simple-git-hooks", "./git-hooks.js"], + ["node", require.resolve(`./cli`), "./git-hooks.js"], + [ + "node", + require.resolve(`./cli`), + require.resolve(`${projectWithCustomConfigurationFilePath}/git-hooks.js`), + ], + ]; + + testCases.forEach((args) => { + it(`creates git hooks and removes unused but preserves specific git hooks for command: ${args.join( + " " + )}`, () => { + createGitHooksFolder(projectWithCustomConfigurationFilePath); + + spc.setHooksFromConfig(projectWithCustomConfigurationFilePath, args); + const installedHooks = getInstalledGitHooks( + path.normalize( + path.join(projectWithCustomConfigurationFilePath, ".git", "hooks") + ) + ); + expect(JSON.stringify(installedHooks)).toBe(JSON.stringify(COMMON_GIT_HOOKS)); + expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); + }); + }); +}); + +describe("Tests for skipping git hooks using SKIP_SIMPLE_GIT_HOOKS env var", () => { + const GIT_USER_NAME = "github-actions"; + const GIT_USER_EMAIL = "github-actions@github.com"; + + const initializeGitRepository = (path) => { + execSync( + `git init \ + && git config user.name ${GIT_USER_NAME} \ + && git config user.email ${GIT_USER_EMAIL}`, + { cwd: path } + ); + }; + + const performTestCommit = (path, env = process.env) => { + try { + execSync( + 'git add . && git commit --allow-empty -m "Test commit" && git commit --allow-empty -am "Change commit msg"', + { + cwd: path, + env: env, + } + ); + return false; + } catch (e) { + return true; + } + }; + + beforeEach(() => { + initializeGitRepository(projectWithConfigurationInPackageJsonPath); + createGitHooksFolder(projectWithConfigurationInPackageJsonPath); + spc.setHooksFromConfig(projectWithConfigurationInPackageJsonPath); + }); + + afterEach(() => { + delete process.env.SKIP_SIMPLE_GIT_HOOKS; + }); + + const expectCommitToSucceed = (path) => { + const errorOccurred = performTestCommit(path); + expect(errorOccurred).toBe(false); + }; + + const expectCommitToFail = (path) => { + const errorOccurred = performTestCommit(path); + expect(errorOccurred).toBe(true); + }; + + it('commits successfully when SKIP_SIMPLE_GIT_HOOKS is set to "1"', () => { + process.env.SKIP_SIMPLE_GIT_HOOKS = "1"; + expectCommitToSucceed(projectWithConfigurationInPackageJsonPath); + }); + + it("fails to commit when SKIP_SIMPLE_GIT_HOOKS is not set", () => { + expectCommitToFail(projectWithConfigurationInPackageJsonPath); + }); + + it('fails to commit when SKIP_SIMPLE_GIT_HOOKS is set to "0"', () => { + process.env.SKIP_SIMPLE_GIT_HOOKS = "0"; + expectCommitToFail(projectWithConfigurationInPackageJsonPath); + }); + + it("fails to commit when SKIP_SIMPLE_GIT_HOOKS is set to a random string", () => { + process.env.SKIP_SIMPLE_GIT_HOOKS = "simple-git-hooks"; + expectCommitToFail(projectWithConfigurationInPackageJsonPath); + }); +});