diff --git a/simple-git-hooks.js b/simple-git-hooks.js index 122eb4b..e9d68c4 100644 --- a/simple-git-hooks.js +++ b/simple-git-hooks.js @@ -190,12 +190,20 @@ async function setHooksFromConfig(projectRootPath=process.cwd(), argv=process.ar * Respects user-defined core.hooksPath from Git config if present; * otherwise defaults to /.git/hooks. * - * @param {string} gitRoot - The absolute path to the Git project root + * @param {string} projectRoot - The absolute path to the project root * @returns {string} - The resolved absolute path to the hooks directory * @private */ function _getHooksDirPath(projectRoot) { - const defaultHooksDirPath = path.join(projectRoot, '.git', 'hooks') + const getDefaultHooksDirPath = (projectRootPath) => { + const gitRoot = getGitProjectRoot(projectRootPath) + if (!gitRoot) { + console.info('[INFO] No `.git` root folder found, skipping') + return + } + return path.join(gitRoot, 'hooks') + } + try { const customHooksDirPath = execSync('git config core.hooksPath', { cwd: projectRoot, @@ -203,14 +211,14 @@ function _getHooksDirPath(projectRoot) { }).trim() if (!customHooksDirPath) { - return defaultHooksDirPath + return getDefaultHooksDirPath(projectRoot) } return path.isAbsolute(customHooksDirPath) ? customHooksDirPath : path.resolve(projectRoot, customHooksDirPath) } catch { - return defaultHooksDirPath + return getDefaultHooksDirPath(projectRoot) } } @@ -222,15 +230,19 @@ function _getHooksDirPath(projectRoot) { * @private */ function _setHook(hook, command, projectRoot=process.cwd()) { - const gitRoot = getGitProjectRoot(projectRoot) - - if (!gitRoot) { - console.info('[INFO] No `.git` root folder found, skipping') + const hookDirectory = _getHooksDirPath(projectRoot) + + if (!hookDirectory) { + console.info('[INFO] No hooks folder found, skipping') return } - const hookCommand = PREPEND_SCRIPT + command - const hookDirectory = _getHooksDirPath(projectRoot) + let finalCommand = command; + if(hookDirectory !== path.join(projectRoot, '.git', 'hooks')) { + finalCommand = `pushd . && cd ${projectRoot} && ${command} && popd` + } + const hookCommand = PREPEND_SCRIPT + finalCommand + const hookPath = path.join(hookDirectory, hook) const normalizedHookDirectory = path.normalize(hookDirectory) @@ -272,6 +284,12 @@ async function removeHooks(projectRoot = process.cwd()) { */ function _removeHook(hook, projectRoot=process.cwd()) { const hookDirectory = _getHooksDirPath(projectRoot) + + if (!hookDirectory) { + console.info('[INFO] No hooks folder found, skipping') + return + } + const hookPath = path.join(hookDirectory, hook) if (fs.existsSync(hookPath)) { diff --git a/simple-git-hooks.test.js b/simple-git-hooks.test.js index f126c8b..d363ddb 100644 --- a/simple-git-hooks.test.js +++ b/simple-git-hooks.test.js @@ -132,6 +132,13 @@ describe("Simple Git Hooks tests", () => { const TEST_SCRIPT = `${simpleGitHooks.PREPEND_SCRIPT}exit 1`; const COMMON_GIT_HOOKS = { "pre-commit": TEST_SCRIPT, "pre-push": TEST_SCRIPT }; + + + // This function is used to wrap commands with directory change + // It is used to ensure that commands are executed in the correct directory + const wrapCommandWithDirectoryChange = (dir, command) => { + return `pushd . && cd ${dir} && ${command} && popd`; + }; // To test this package, we often need to create and manage files. // Best to use real file system and real files under _tests folder @@ -518,9 +525,13 @@ describe("Simple Git Hooks tests", () => { } await simpleGitHooks.setHooksFromConfig(TEST_HUSKY_PROJECT); - + const installedHooks = getInstalledGitHooks(huskyDir); - expect(isEqual(installedHooks, COMMON_GIT_HOOKS)).toBe(true); + const expectedHook = wrapCommandWithDirectoryChange( + TEST_HUSKY_PROJECT, "exit 1" + ); + expect(installedHooks["pre-commit"]).toContain(expectedHook); + expect(installedHooks["pre-push"]).toContain(expectedHook); }) it("remove git hooks in .husky if core.hooksPath is set to .husky", async () => { @@ -730,6 +741,35 @@ describe("Simple Git Hooks tests", () => { expectCommitToFail(PROJECT_WITH_CONF_IN_PACKAGE_JSON); }); }) + + describe("Monorepo scenarios", () => { + it("installs hooks in git root when executed from project subdirectory", async () => { + const MONOREPO_ROOT = path.join(testsFolder, "monorepo_scenario"); + const PROJECT_SUBDIR = path.join(MONOREPO_ROOT, "npm_project"); + + // Setup: Git root with npm project in subdirectory + fs.mkdirSync(MONOREPO_ROOT, { recursive: true }); + fs.mkdirSync(path.join(MONOREPO_ROOT, ".git", "hooks"), { recursive: true }); + fs.mkdirSync(PROJECT_SUBDIR, { recursive: true }); + + fs.writeFileSync(path.join(PROJECT_SUBDIR, "package.json"), JSON.stringify({ + "simple-git-hooks": { "pre-commit": "exit 1" } + })); + + // Execute from subdirectory + await simpleGitHooks.setHooksFromConfig(PROJECT_SUBDIR); + + // Verify hooks installed in git root, not project subdirectory + const gitRootHooks = getInstalledGitHooks(path.join(MONOREPO_ROOT, ".git", "hooks")); + expect(Object.keys(gitRootHooks)).toContain("pre-commit"); + + // Verify no .git created in subdirectory + expect(fs.existsSync(path.join(PROJECT_SUBDIR, ".git"))).toBe(false); + + // Cleanup + fs.rmSync(MONOREPO_ROOT, { recursive: true, force: true }); + }); + }); }); afterEach(() => {