Saltar al contenido

Hooks de Git para automatizar el desarrollo

Cómo usar hooks de Git para automatizar calidad, validar mensajes de commit, ejecutar pruebas pre-push y nombrar ramas, con Husky y scripts propios.

4 min de lectura
Diagrama de flujo de hooks de Git mostrando en secuencia los hooks pre-commit, commit-msg y pre-push

Los hooks de Git son scripts que se ejecutan automáticamente en momentos específicos del flujo de trabajo de Git: antes de un commit, antes de un push, después de un merge. Son la capa de cumplimiento entre "quedó en que íbamos a lintear el código" y "el código realmente está linteado". Sin hooks, las revisiones de calidad dependen de la disciplina de cada desarrollador. Con hooks, son automáticas.

Los hooks más valiosos son pre-commit (valida el código antes de commitear), commit-msg (fuerza el formato del mensaje de commit) y pre-push (ejecuta pruebas antes de hacer push).

Configurar hooks con Husky

Los hooks de Git viven en .git/hooks/, que no se rastrea en el control de versiones. Esto significa que no puedes compartirlos a través del repositorio. Husky resuelve esto al colocar los hooks en un directorio .husky/ que sí se commitea.

shbash
# Install Husky
npm install --save-dev husky
 
# Initialize Husky (creates .husky/ directory)
npx husky init
 
# This creates .husky/pre-commit with a default script
# and adds "prepare": "husky" to package.json
jsonjson
// package.json
{
  "scripts": {
    "prepare": "husky",
    "lint": "eslint . --ext .ts,.tsx",
    "format:check": "prettier --check .",
    "typecheck": "tsc --noEmit",
    "test": "vitest run"
  },
  "devDependencies": {
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0"
  }
}

Hook pre-commit: lintear solo los archivos en staging

El hook pre-commit se ejecuta antes de cada commit. La optimización clave es lint-staged: en lugar de lintear toda la base de código, solo se lintean los archivos que se van a commitear.

shbash
# .husky/pre-commit
npx lint-staged
tstypescript
// lint-staged.config.ts
const config = {
  // TypeScript/JavaScript files: lint and format
  '*.{ts,tsx}': [
    'eslint --fix --max-warnings 0',
    'prettier --write',
  ],
 
  // CSS/SCSS files: format only
  '*.{css,scss}': [
    'prettier --write',
  ],
 
  // JSON/YAML files: format
  '*.{json,yaml,yml}': [
    'prettier --write',
  ],
 
  // Markdown files: format
  '*.md': [
    'prettier --write',
  ],
};
 
export default config;
shbash
# ❌ Linting the entire codebase on every commit
# .husky/pre-commit
npm run lint              # Takes 30 seconds on a large codebase
npm run format:check      # Another 15 seconds
npm run typecheck         # Another 20 seconds
# Developers disable the hook because it's too slow
 
# ✅ Only check the files being committed
# .husky/pre-commit
npx lint-staged           # Takes 2-3 seconds — only changed files
# Fast enough that developers never disable it

Hook commit-msg: forzar Conventional Commits

El hook commit-msg valida el formato del mensaje de commit. Los Conventional Commits (feat:, fix:, chore:, etc.) permiten generar changelogs automáticamente y hacer versionado semántico.

shbash
# .husky/commit-msg
npx --no -- commitlint --edit $1
tstypescript
// commitlint.config.ts
const config = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    // Type must be one of these
    'type-enum': [
      2,  // Error level
      'always',
      [
        'feat',     // New feature
        'fix',      // Bug fix
        'docs',     // Documentation only
        'style',    // Formatting (no code change)
        'refactor', // Code change that neither fixes nor adds
        'perf',     // Performance improvement
        'test',     // Adding or updating tests
        'chore',    // Build process or auxiliary tools
        'ci',       // CI configuration
        'revert',   // Revert a previous commit
      ],
    ],
 
    // Subject must be lowercase
    'subject-case': [2, 'always', 'lower-case'],
 
    // Subject must not be empty
    'subject-empty': [2, 'never'],
 
    // Subject must not end with period
    'subject-full-stop': [2, 'never', '.'],
 
    // Max line length for the header
    'header-max-length': [2, 'always', 100],
  },
};
 
export default config;
shbash
# ❌ Bad commit messages that commit-msg hook rejects
git commit -m "fixed stuff"              # No type prefix
git commit -m "feat: Updated the login." # Period at end, uppercase
git commit -m "wip"                      # No type, not descriptive
 
# ✅ Good commit messages that pass validation
git commit -m "fix: resolve race condition in payment webhook handler"
git commit -m "feat: add email notification preferences to user settings"
git commit -m "refactor: extract validation logic into shared utility"
git commit -m "docs: add API authentication examples to README"

Hook pre-push: ejecutar pruebas antes de hacer push

El hook pre-push se ejecuta antes de que el código se envíe al remoto. Aquí van las comprobaciones más pesadas que serían demasiado lentas para pre-commit: verificación de tipos y suites de pruebas.

shbash
# .husky/pre-push
echo "Running type check..."
npx tsc --noEmit
 
echo "Running tests..."
npx vitest run --reporter=dot
 
echo "All checks passed. Pushing..."
tstypescript
// For monorepos: only run tests for packages that changed
// .husky/pre-push (advanced)
 
import { execSync } from 'child_process';
 
function getChangedPackages(): string[] {
  // Compare local HEAD with remote
  const diff = execSync(
    'git diff --name-only @{upstream}...HEAD',
    { encoding: 'utf-8' }
  );
 
  const changedFiles = diff.trim().split('\n');
  const packages = new Set<string>();
 
  for (const file of changedFiles) {
    // Extract package name from path like "packages/auth/src/..."
    const match = file.match(/^packages\/([^/]+)\//);
    if (match) {
      packages.add(match[1]);
    }
  }
 
  return [...packages];
}
 
const changed = getChangedPackages();
if (changed.length > 0) {
  console.log(`Testing changed packages: ${changed.join(', ')}`);
  for (const pkg of changed) {
    execSync(`npm run test --workspace=packages/${pkg}`, { stdio: 'inherit' });
  }
}

Hooks personalizados: nombres de ramas

Más allá de las comprobaciones estándar, puedes hacer cumplir convenciones del equipo como patrones de nomenclatura de ramas.

shbash
#!/bin/bash
# .husky/pre-push — branch naming validation
 
BRANCH=$(git rev-parse --abbrev-ref HEAD)
 
# Allowed patterns: feat/*, fix/*, chore/*, hotfix/*, release/*
PATTERN="^(feat|fix|chore|hotfix|release|docs|test)\/[a-z0-9._-]+$"
 
if [[ "$BRANCH" == "main" || "$BRANCH" == "develop" ]]; then
  exit 0  # Main branches are always allowed
fi
 
if [[ ! "$BRANCH" =~ $PATTERN ]]; then
  echo "❌ Branch name '$BRANCH' does not match the required pattern."
  echo "   Use: feat/description, fix/description, chore/description"
  echo "   Example: feat/add-user-notifications"
  exit 1
fi
tstypescript
// ❌ Without branch naming enforcement
// Branches in the repo: wip, johns-branch, test123, fix, asdf
// Nobody knows what these branches are for
 
// ✅ With branch naming enforcement
// Branches: feat/user-notifications, fix/payment-race-condition, chore/update-deps
// Every branch name tells you what it contains and why it exists

Cómo omitir los hooks (cuando sea necesario)

A veces necesitas saltarte los hooks: durante un hotfix a las 2 AM, o al commitear archivos generados. Git ofrece salidas de emergencia, pero deberían ser raras.

shbash
# Skip pre-commit and commit-msg hooks
git commit --no-verify -m "hotfix: emergency patch for production outage"
 
# Skip pre-push hooks
git push --no-verify
 
# ⚠️ IMPORTANT: Hooks are a safety net, not a prison.
# If you're using --no-verify regularly, your hooks are too slow or too strict.
# Fix the hooks instead of bypassing them.

Conclusiones clave

  1. Usa lint-staged en pre-commit — lintea solo los archivos cambiados para mantener el hook lo suficientemente rápido como para que nadie lo desactive
  2. Fuerza Conventional Commits — los mensajes de commit estructurados permiten generar changelogs automáticos y hacer versionado semántico
  3. Ejecuta pruebas en pre-push, no en pre-commit — las comprobaciones más pesadas van antes del push, no antes de cada commit
  4. Husky hace que los hooks sean compartibles — .husky/ se commite en el repositorio, así que cada desarrollador obtiene los mismos hooks
  5. Haz cumplir los nombres de ramas — los patrones consistentes hacen que el propósito de una rama sea inmediatamente claro
  6. Mantén los hooks rápidos — si un hook tarda más de 5 segundos, los desarrolladores lo omitirán; optímizalo o muévelo a CI
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX