Skip to content

feat: optimize the migration experience from husky #127

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

Merged
merged 9 commits into from
Apr 7, 2025
33 changes: 0 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,6 @@ If you need multiple verbose commands per git hook, flexible configuration or au
3. Run the CLI script to update 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 Down Expand Up @@ -211,34 +206,6 @@ If you have the option to set arguments or environment variables, you can use th

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

**Why is this happening?**

Husky might change the `core.gitHooks` value to `.husky`, this way, git hooks would search `.husky` directory instead of `.git/hooks/`.

Read more on git configuration in [Git book](https://git-scm.com/docs/githooks)

You can check it by running this command inside of your repo:

`git config core.hooksPath`

If it outputs `.husky` then this is your case

**How to fix?**

you need to point `core.gitHooks` value to `your-awesome-project/.git/hooks`. You can use this command:

`git config core.hooksPath .git/hooks/`

validate the value is set:

`git config core.hooksPath`

should output: `.git/hooks/`

Then remove the `.husky` folder that are generated previously by `husky`.

### I am getting "npx: command not found" error in a GUI git client

This happens when using a node version manager such as `nodenv`, `nvm`, `mise` which require
Expand Down
44 changes: 37 additions & 7 deletions simple-git-hooks.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const fs = require('fs')
const path = require('path')
const url = require('url')
const { execSync } = require('child_process');

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'

Expand Down Expand Up @@ -185,7 +186,36 @@ async function setHooksFromConfig(projectRootPath=process.cwd(), argv=process.ar
}

/**
* Creates or replaces an existing executable script in .git/hooks/<hook> with provided command
* Returns the absolute path to the Git hooks directory.
* Respects user-defined core.hooksPath from Git config if present;
* otherwise defaults to <gitRoot>/.git/hooks.
*
* @param {string} gitRoot - The absolute path to the Git project root
* @returns {string} - The resolved absolute path to the hooks directory
* @private
*/
function _getHooksDirPath(projectRoot) {
const defaultHooksDirPath = path.join(projectRoot, '.git', 'hooks')
try {
const customHooksDirPath = execSync('git config core.hooksPath', {
cwd: projectRoot,
encoding: 'utf8'
}).trim()

if (!customHooksDirPath) {
return defaultHooksDirPath
}

return path.isAbsolute(customHooksDirPath)
? customHooksDirPath
: path.resolve(projectRoot, customHooksDirPath)
} catch {
return defaultHooksDirPath
}
}

/**
* Creates or replaces an existing executable script in the git hooks directory with provided command
* @param {string} hook
* @param {string} command
* @param {string} projectRoot
Expand All @@ -200,12 +230,12 @@ function _setHook(hook, command, projectRoot=process.cwd()) {
}

const hookCommand = PREPEND_SCRIPT + command
const hookDirectory = gitRoot + '/hooks/'
const hookPath = path.normalize(hookDirectory + hook)
const hookDirectory = _getHooksDirPath(projectRoot)
const hookPath = path.join(hookDirectory, hook)

const normalizedHookDirectory = path.normalize(hookDirectory)
if (!fs.existsSync(normalizedHookDirectory)) {
fs.mkdirSync(normalizedHookDirectory)
fs.mkdirSync(normalizedHookDirectory, { recursive: true })
}

fs.writeFileSync(hookPath, hookCommand)
Expand Down Expand Up @@ -235,14 +265,14 @@ async function removeHooks(projectRoot = process.cwd()) {
}

/**
* Removes the pre-commit hook from .git/hooks
* Removes the pre-commit hook
* @param {string} hook
* @param {string} projectRoot
* @private
*/
function _removeHook(hook, projectRoot=process.cwd()) {
const gitRoot = getGitProjectRoot(projectRoot)
const hookPath = path.normalize(gitRoot + '/hooks/' + hook)
const hookDirectory = _getHooksDirPath(projectRoot)
const hookPath = path.join(hookDirectory, hook)

if (fs.existsSync(hookPath)) {
fs.unlinkSync(hookPath)
Expand Down
32 changes: 32 additions & 0 deletions simple-git-hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,38 @@ describe("Simple Git Hooks tests", () => {
});
});

describe("Custom core.hooksPath", () => {
beforeAll(() => {
execSync('git config core.hooksPath .husky');
});

afterAll(() => {
execSync('git config --unset core.hooksPath');
});

const TEST_HUSKY_PROJECT = PROJECT_WITH_CONF_IN_SEPARATE_JS_ALT;

it("creates git hooks in .husky if core.hooksPath is set to .husky", async () => {
const huskyDir = path.join(TEST_HUSKY_PROJECT, ".husky");

if (!fs.existsSync(huskyDir)) {
fs.mkdirSync(huskyDir);
}

await simpleGitHooks.setHooksFromConfig(TEST_HUSKY_PROJECT);

const installedHooks = getInstalledGitHooks(huskyDir);
expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true);
})

it("remove git hooks in .husky if core.hooksPath is set to .husky", async () => {
await simpleGitHooks.removeHooks(TEST_HUSKY_PROJECT);
const huskyDir = path.join(TEST_HUSKY_PROJECT, ".husky");
const installedHooks = getInstalledGitHooks(huskyDir);
expect(isEqual(installedHooks, {})).toBe(true);
})
})

describe("CLI tests", () => {
const testCases = [
["npx", "simple-git-hooks", "./git-hooks.js"],
Expand Down