Files
Jared ScottandGitHub f1b9867c97 fix #1366 Update how-to.md (#1384)
Fix #1366 by adding documentation to `how-to.md`
2024-02-14 12:02:19 +01:00

6.5 KiB

How To

Adding a New Hook

Adding a hook is as simple as creating a file. This can be accomplished using your favorite editor, a script or a basic echo command. For example, on Linux/macOS:

echo "npm test" > .husky/pre-commit

Startup files

Husky allows you to execute local commands before running hooks. It reads commands from these files:

  • $XDG_CONFIG_HOME/husky/init.sh
  • ~/.config/husky/init.sh
  • ~/.huskyrc (deprecated)

On Windows: C:\Users\yourusername\.config\husky\init.sh

Skipping Git Hooks

For a Single Command

Most Git commands include a -n/--no-verify option to skip hooks:

git commit -m "..." -n # Skips Git hooks

For commands without this flag, disable hooks temporarily with HUSKY=0:

HUSKY=0 git ... # Temporarily disables all Git hooks
git ... # Hooks will run again

For multiple commands

To disable hooks for an extended period (e.g., during rebase/merge):

export HUSKY=0 # Disables all Git hooks
git ...
git ...
unset HUSKY # Re-enables hooks

For a GUI or Globally

To disable Git hooks in a GUI client or globally, modify the husky config:

# ~/.config/husky/init.sh
export HUSKY=0 # Husky won't install and won't run hooks on your machine

CI server and Docker

To avoid installing Git Hooks on CI servers or in Docker, use HUSKY=0. For instance, in GitHub Actions:

# https://docs.github.com/en/actions/learn-github-actions/variables
env:
  HUSKY: 0

If installing only dependencies (not devDependencies), the "prepare": "husky" script may fail because Husky won't be installed.

You have multiple solutions.

Modify the prepare script to never fail:

// package.json
"prepare": "husky || true"

You'll still get a command not found error message in your output which may be confusing. To make it silent, create .husky/install.mjs:

// Skip Husky install in production and CI
if (process.env.NODE_ENV === 'production' || process.env.CI === 'true') {
  process.exit(0)
}
const husky = (await import('husky')).default
console.log(husky())

Then, use it in prepare:

"prepare": "node .husky/install.mjs"

Testing Hooks Without Committing

To test a hook, add exit 1 to the hook script to abort the Git command:

# .husky/pre-commit

# Your WIP script
# ...

exit 1
git commit -m "testing pre-commit code"
# A commit will not be created

Project Not in Git Root Directory

Husky doesn't install in parent directories (../) for security reasons. However, you can change the directory in the prepare script.

Consider this project structure:

.
├── .git/
├── backend/  # No package.json
└── frontend/ # Package.json with husky

Set your prepare script like this:

"prepare": "cd .. && husky frontend/.husky"

In your hook script, change the directory back to the relevant subdirectory:

# frontend/.husky/pre-commit
cd frontend
npm test

Non-shell hooks

In order to run scripts that require the use of a scripting language, use the following pattern for each applicable hook:

(Example using hook pre-commit and NodeJS)

  1. Create an entrypoint for the hook:
    .husky/pre-commit
    
  2. In the file add the following
    node .husky/pre-commit.js
    
  3. in .husky/pre-commit.js
    // Your NodeJS code
    // ...
    

Node Version Managers and GUIs

If you're using Git hooks in GUIs with Node installed via a version manager (like nvm, n, fnm, asdf, volta, etc...), you might face a command not found error due to PATH environment variable issues.

Understanding PATH and Version Managers

PATH is an environment variable containing a list of directories. Your shell searches these directories for commands. If it doesn't find a command, you get a command not found message.

Run echo $PATH in a shell to view its contents.

Version managers work by:

  1. Adding initialization code to your shell startup file (.zshrc, .bashrc, etc.), which runs each time you open a terminal.
  2. Downloading Node versions to a directory in your home folder.

For example, if you have two Node versions:

~/version-manager/Node-X/node
~/version-manager/Node-Y/node

Opening a terminal initializes the version manager, which picks a version (say Node-Y) and prepends its path to PATH:

echo $PATH
# Output
~/version-manager/Node-Y/:...

Now, node refers to Node-Y. Switching to Node-X changes PATH accordingly:

echo $PATH
# Output
~/version-manager/Node-X/:...

The issue arises because GUIs, launched outside a terminal, don't initialize the version manager, leaving PATH without the Node install path. Thus, Git hooks from GUIs often fail.

Solution

Husky sources ~/.config/husky/init.sh before each Git hook. Copy your version manager initialization code here to ensure it runs in GUIs.

Example with nvm:

# ~/.config/husky/init.sh
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

Alternatively, if your shell startup file is fast and lightweight, source it directly:

# ~/.config/husky/init.sh
. ~/.zshrc

Manual setup

Git needs to be configured and husky needs to setup files in .husky/.

Run the husky command once in your repo. Ideally, include it in the prepare script in package.json for automatic execution after each install (recommended).

::: code-group

{
  "scripts": {
    "prepare": "husky" // [!code hl]
  }
}
{
  "scripts": {
    "prepare": "husky" // [!code hl]
  }
}
{
  "scripts": {
    // Yarn doesn't support prepare script
    "postinstall": "husky",
    // Include this if publishing to npmjs.com
    "prepack": "pinst --disable",
    "postpack": "pinst --enable"
  }
}
{
  "scripts": {
    "prepare": "husky" // [!code hl]
  }
}

:::

Run prepare once:

::: code-group

npm run prepare
pnpm run prepare
# Yarn doesn't support `prepare`
yarn run postinstall
bun run prepare

:::

Create a pre-commit file in the .husky/ directory:

::: code-group

# .husky/pre-commit
npm test
# .husky/pre-commit
pnpm test
# .husky/pre-commit
yarn test
# .husky/pre-commit
bun test

:::