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
38 changes: 28 additions & 10 deletions simple-git-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,27 +190,35 @@ async function setHooksFromConfig(projectRootPath=process.cwd(), argv=process.ar
* 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
* @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,
encoding: 'utf8'
}).trim()

if (!customHooksDirPath) {
return defaultHooksDirPath
return getDefaultHooksDirPath(projectRoot)
}

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

Expand All @@ -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`

Choose a reason for hiding this comment

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

looking into it, i think the failing test comes from this and the test needs to be adapted? The test expects this command without changes in the resulting hook file, but finds it with this

other than that, the fix looks good to me

}
const hookCommand = PREPEND_SCRIPT + finalCommand

const hookPath = path.join(hookDirectory, hook)

const normalizedHookDirectory = path.normalize(hookDirectory)
Expand Down Expand Up @@ -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)) {
Expand Down
44 changes: 42 additions & 2 deletions simple-git-hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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(() => {
Expand Down