Skip to content

Conversation

MKlingner
Copy link

@MKlingner MKlingner commented Jul 28, 2025

Fix the issue 132

There is a bug in the install script when the gitRoot is not the same as the projectRoot. My Project looks like:

  • .git
  • npm_project_1
    • package.json
  • npm_project_2
    • package.json
    • <- executing npx simple-git-hooks here

result:

  • .git
  • npm_project_1
    • package.json
  • npm_project_2
    • .git
      • hooks
        • pre-commit
    • package.json

expectation:

  • .git
    • hooks
      • pre-commit
  • npm_project_1
    • package.json
  • npm_project_2
    • package.json

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling for projects without a Git root or hooks directory, preventing errors and providing informative messages.
    • Ensured hook commands execute from the correct directory when hooks are stored outside the default location.
  • Tests
    • Added coverage for monorepo setups to verify hooks install in the Git root directory.
    • Enhanced tests to confirm correct command execution context when custom hooks paths are used.

Copy link

changeset-bot bot commented Jul 28, 2025

⚠️ No Changeset found

Latest commit: f76ec8a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

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

Copy link
Owner

@toplenboren toplenboren left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! 🚀

Please feel free to adapt tests and add some more to cover your cases

@toplenboren
Copy link
Owner

@coderabbitai wanna make a review?

Copy link

coderabbitai bot commented Jul 31, 2025

@toplenboren I'll review the changes to see how this PR fixes the gitRoot vs projectRoot issue.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

coderabbitai bot commented Jul 31, 2025

Walkthrough

The changes update internal logic for resolving the Git hooks directory in the script. The hooks directory is now determined relative to the Git project root, with early exits and info logs if no Git root or hooks directory is found. Hook command execution context is adjusted when hooks are outside the default location. Tests were added to cover monorepo scenarios and verify command wrapping for non-default hooks paths.

Changes

Cohort / File(s) Change Summary
Internal Git Hook Logic
simple-git-hooks.js
Refactored internal functions to resolve hooks directory from Git root, added early exits with info logs, and adjusted hook command execution context for non-default hook locations. No changes to exported or public APIs.
Test Enhancements
simple-git-hooks.test.js
Added helper to wrap commands with directory changes, updated tests to verify wrapped commands, and introduced monorepo scenario tests to ensure hooks install correctly in Git root when run from subdirectories.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A rabbit hopped through Git’s domain,
Seeking hooks with logic plain.
If no root’s found, it logs and waits,
No more errors at unknown gates.
With pushd/popd, commands now run—
In every burrow, work is done!
🐇✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

coderabbitai bot commented Jul 31, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll take a look at this PR! Let me examine the changes to understand how you've fixed the gitRoot vs projectRoot issue.


🏁 Script executed:

#!/bin/bash
# Get the PR details and changed files
gh pr view 133 --json files,additions,deletions,title,body

# Get the diff to see what changes were made
gh pr diff 133

Length of output: 3218


@toplenboren Great work on fixing this gitRoot vs projectRoot issue! I've reviewed the changes and the approach looks solid. Here's my analysis:

What's Working Well:

  1. Correct Git Root Detection: The fix properly uses getGitProjectRoot(projectRoot) to find the actual git repository root instead of assuming it's the same as the project root.

  2. Proper Hook Path Construction: Now correctly constructs the hooks path as path.join(gitRoot, 'hooks') instead of path.join(projectRoot, '.git', 'hooks').

  3. Working Directory Handling: The smart use of pushd . && cd ${projectRoot} && ${command} && popd ensures hook commands execute in the correct context (the npm project directory) even when hooks are installed in a different location.

  4. Error Handling: Added proper null checks and informative messages when git root or hooks directory can't be found.

🤔 Potential Considerations:

  1. Cross-Platform Compatibility: The pushd/popd commands are shell-specific. While they work on most Unix-like systems and Windows with appropriate shells, you might want to consider a more portable approach:
// Instead of shell commands, consider:
const originalCwd = process.cwd();
try {
    process.chdir(projectRoot);
    // Execute command here
} finally {
    process.chdir(originalCwd);
}
  1. Path Handling: In _getHooksDirPath, you're now using path.join(gitRoot, 'hooks') directly instead of path.join(gitRoot, '.git', 'hooks'). This assumes the hooks are always in the git root's hooks directory, but git repositories can have their hooks in .git/hooks. Is this intentional?

Let me check how getGitProjectRoot works to understand this better:


🏁 Script executed:

#!/bin/bash
# Let's see the getGitProjectRoot function implementation
ast-grep --pattern 'function getGitProjectRoot($_) {
  $$$
}'

Length of output: 2051


Now I understand the issue! After examining the getGitProjectRoot function, I found a problem with the current implementation:

🚨 Critical Issue Found:

The getGitProjectRoot function returns the path to the .git directory itself, not the repository root directory. However, in your fix, you're constructing the hooks path as:

const defaultHooksDirPath = path.join(gitRoot, 'hooks')  // This would be `.git/hooks` 

But since gitRoot already points to the .git directory, this is correct! Let me double-check this by looking at the original implementation:

Actually, looking at the diff again, I can see that your implementation is correct! The getGitProjectRoot function returns the .git directory path, so path.join(gitRoot, 'hooks') correctly gives us .git/hooks.

Correction - The Implementation is Actually Correct:

Your fix properly handles the git directory structure. The logic flow is:

  1. getGitProjectRoot(projectRoot) finds and returns the .git directory path
  2. path.join(gitRoot, 'hooks') correctly constructs .git/hooks

🔍 Let me verify with a test scenario:

The implementation looks solid overall! The fix correctly addresses the issue where hooks were being created in the wrong location when the git root and project root differ.

Summary:

  • ✅ Correctly finds the actual git root directory
  • ✅ Properly constructs hook paths relative to git root
  • ✅ Handles working directory context for command execution
  • ✅ Includes proper error handling

The only minor suggestion I have is about the cross-platform compatibility of pushd/popd, but for a Node.js tool, this should work fine in most environments.

Great job fixing this issue! 🐰✨


🏁 Script executed:

#!/bin/bash
# Let's see what the original _getHooksDirPath looked like before this PR
git show HEAD~1:simple-git-hooks.js | grep -A 20 "_getHooksDirPath"

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Check if there are any existing tests for this functionality
fd -t f -e js . | grep -i test

Length of output: 180


Let me examine the tests to understand the current coverage and see if any tests need to be updated for this fix:


🏁 Script executed:

#!/bin/bash
# Check the main test file to understand current test coverage
cat simple-git-hooks.test.js

Length of output: 28112


After examining the test suite, I can see that your fix addresses a real gap in both functionality and test coverage!

Current Test Analysis:

All existing tests assume that projectRoot === gitRoot, which is why this bug wasn't caught earlier. The tests:

  • Create .git folders directly in project directories with createGitHooksFolder(root)
  • Expect hooks to be installed at ${projectRoot}/.git/hooks
  • Never test the scenario where git root is in a parent directory

🧪 Missing Test Coverage:

Your fix addresses a critical scenario that isn't tested at all. I strongly recommend adding a test case like this:

Based on the test structure, here's what I'd recommend adding:

describe("Git root vs Project root scenarios", () => {
  it("creates git hooks in correct location when git root differs from project root", async () => {
    // Create a scenario like issue #132:
    // - .git (git root)
    // - npm_project (project root with package.json)
    
    const GIT_ROOT = path.join(testsFolder, "monorepo_git_root");
    const PROJECT_ROOT = path.join(GIT_ROOT, "npm_project");
    
    // Setup git root
    if (!fs.existsSync(GIT_ROOT)) fs.mkdirSync(GIT_ROOT);
    if (!fs.existsSync(GIT_ROOT + "/.git")) fs.mkdirSync(GIT_ROOT + "/.git");
    if (!fs.existsSync(GIT_ROOT + "/.git/hooks")) fs.mkdirSync(GIT_ROOT + "/.git/hooks");
    
    // Setup npm project in subdirectory
    if (!fs.existsSync(PROJECT_ROOT)) fs.mkdirSync(PROJECT_ROOT);
    fs.writeFileSync(path.join(PROJECT_ROOT, "package.json"), JSON.stringify({
      "simple-git-hooks": { "pre-commit": "exit 1" }
    }));
    
    // Run from project root (subdirectory)
    await simpleGitHooks.setHooksFromConfig(PROJECT_ROOT);
    
    // Hooks should be installed in git root, not project root
    const gitRootHooks = getInstalledGitHooks(path.join(GIT_ROOT, ".git", "hooks"));
    const projectRootHooks = fs.existsSync(path.join(PROJECT_ROOT, ".git")) ? 
      getInstalledGitHooks(path.join(PROJECT_ROOT, ".git", "hooks")) : {};
    
    expect(Object.keys(gitRootHooks)).toEqual(["pre-commit"]);
    expect(Object.keys(projectRootHooks)).toEqual([]);
    
    // Cleanup
    fs.rmSync(GIT_ROOT, { recursive: true, force: true });
  });
});

🏃‍♂️ Next Steps:

  1. Run existing tests to ensure your fix doesn't break anything:

  2. Add the missing test case to prevent future regressions

  3. Consider edge cases like:

    • What happens with git worktrees?
    • What about when core.hooksPath is set to a relative path?

Your fix is solid and addresses a real user pain point. The only thing missing is test coverage for this scenario!

🐰💭


🏁 Script executed:

#!/bin/bash
# Let's check if there are any test project setups that might simulate this scenario
find _tests -name "*.json" -exec echo "=== {} ===" \; -exec cat {} \;

Length of output: 4060


🏁 Script executed:

npm test

Length of output: 840


Perfect! The test project structures confirm my analysis - all current tests assume the project root contains the .git directory directly.

Since I can't run the tests in this sandbox environment (jest isn't available), but based on my code analysis, here's my final assessment:

Overall Assessment: LGTM!

Your fix correctly addresses the gitRoot vs projectRoot issue:

  1. Root Cause Fixed: _getHooksDirPath now properly finds the actual git root using getGitProjectRoot()
  2. Command Context Handled: The smart pushd/popd solution ensures hook commands run in the correct working directory
  3. Error Handling: Added proper null checks and informative messages
  4. Backward Compatible: Existing behavior preserved when projectRoot === gitRoot

🧪 Testing Recommendations:

For you to run locally:

npm test  # Ensure no regressions

Missing test case to add:

// Add this to simple-git-hooks.test.js
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 });
  });
});

🚀 Ready to Merge:

This fix solves a real user pain point and the implementation is solid. Once you've:

  1. ✅ Confirmed existing tests pass locally
  2. ✅ Added the missing test case for the monorepo scenario

This PR will be ready to merge! Great work on identifying and fixing this issue!

🐰✨

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
simple-git-hooks.test.js (1)

744-772: Excellent test coverage for the core monorepo scenario.

This test directly validates the main fix described in the PR objectives - ensuring hooks are installed in the git root's .git/hooks directory when executed from a project subdirectory. The test setup, execution, and verification logic are all sound.

Consider these minor enhancements:

-        it("installs hooks in git root when executed from project subdirectory", async () => {
+        it("installs hooks in git root when executed from project subdirectory and wraps commands with directory change", async () => {

And add verification of the command wrapping:

          // 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 hook contains directory change wrapper
+         const expectedWrappedCommand = wrapCommandWithDirectoryChange(PROJECT_SUBDIR, "exit 1");
+         expect(gitRootHooks["pre-commit"]).toContain(expectedWrappedCommand);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1dd3f51 and f76ec8a.

📒 Files selected for processing (2)
  • simple-git-hooks.js (3 hunks)
  • simple-git-hooks.test.js (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • simple-git-hooks.js
🧰 Additional context used
🧬 Code Graph Analysis (1)
simple-git-hooks.test.js (1)
simple-git-hooks.js (3)
  • dir (74-74)
  • path (2-2)
  • fs (1-1)
🔇 Additional comments (2)
simple-git-hooks.test.js (2)

137-141: LGTM! Helper function accurately reflects the main application logic.

This helper function correctly implements the directory change wrapping logic that mirrors the behavior in the main simple-git-hooks.js when hooks are installed outside the default .git/hooks path. The use of pushd, cd, and popd ensures commands execute in the correct directory context.


528-534: Correctly updated test to verify command wrapping for custom hooks paths.

The test modification appropriately changes from exact equality to containment checking, which is necessary now that hook scripts include directory change wrapper commands when installed outside the default .git/hooks path. This ensures the test validates the core functionality while being flexible about the exact script format.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants