Los Git hooks son scripts que Git ejecuta automáticamente en momentos concretos: antes de crear un commit, al escribir su mensaje, antes de un push o cuando un repositorio remoto recibe cambios. En este tutorial usarás los hooks del lado del cliente para bloquear commits con errores y los del lado del servidor para desplegar un sitio web con un simple git push a un servidor con Ubuntu 24.04. Es un flujo sencillo, sin sistema de CI, adecuado para sitios estáticos y proyectos pequeños.

Requisitos previos

Para seguir esta guía necesitas:

  • Git instalado en tu equipo local y un repositorio con el código que quieres desplegar.
  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios sudo al que te conectes por SSH con clave.
  • Nginx instalado en el servidor y sirviendo un sitio desde /var/www/your_domain, si quieres comprobar el despliegue en el navegador.

A lo largo de la guía, sustituye your_user por tu usuario del servidor, your_server_ip por su IP y your_domain por tu dominio.

Paso 1: Entender cómo funcionan los hooks

Cada repositorio tiene un directorio .git/hooks con ejemplos que terminan en .sample. Para activar un hook basta con crear un archivo ejecutable con el nombre exacto del evento. Git le pasa información por argumentos o por la entrada estándar y, en los hooks que empiezan por pre-, un código de salida distinto de cero cancela la operación.

Estos son los hooks que usarás:

HookDónde se ejecutaCuándoPuede cancelar
pre-commitLocalAntes de crear el commitSí
commit-msgLocalTras escribir el mensaje, recibe su ruta en $1Sí
pre-pushLocalAntes de enviar al remotoSí
post-receiveServidorDespués de aceptar un push, recibe las referencias por stdinNo

El directorio .git/hooks no se versiona, así que los hooks que pongas ahí no llegan al resto del equipo. En el siguiente paso usarás un directorio versionado.

Paso 2: Crear un hook pre-commit compartido

En tu equipo local, dentro del repositorio, crea un directorio .githooks e indica a Git que lo use en lugar de .git/hooks:

mkdir -p .githooks
git config core.hooksPath .githooks

core.hooksPath es una opción local de cada clon: cada miembro del equipo debe ejecutar el comando git config una vez tras clonar.

Crea el hook pre-commit:

nano .githooks/pre-commit
#!/usr/bin/env bash
set -euo pipefail

# Rechaza espacios en blanco erróneos y marcadores de conflicto sin resolver
git diff --cached --check

# Revisa con ShellCheck los scripts de shell incluidos en el commit
mapfile -t scripts < <(git diff --cached --name-only --diff-filter=ACM -- '*.sh')
if (( ${#scripts[@]} > 0 )); then
    if command -v shellcheck > /dev/null; then
        shellcheck "${scripts[@]}"
    else
        echo "pre-commit: shellcheck no está instalado, se omite la revisión" >&2
    fi
fi

git diff --cached --check es una comprobación integrada de Git que falla si el contenido preparado tiene espacios al final de línea o restos como <<<<<<< de un conflicto. Después, el hook revisa los scripts .sh añadidos o modificados. Ten en cuenta que ShellCheck analiza la versión del archivo en disco, que coincide con la preparada salvo que hayas hecho cambios después de git add.

Hazlo ejecutable:

chmod +x .githooks/pre-commit

Pruébalo con un archivo que tenga un espacio al final de una línea:

printf 'hola \n' > prueba.txt
git add prueba.txt
git commit -m "test: probar el hook"
prueba.txt:1: trailing whitespace.
+hola 

El commit no se ha creado. Corrige el archivo o elimínalo del índice con git rm --cached prueba.txt y borra prueba.txt. Si en algún momento necesitas saltarte los hooks locales, git commit --no-verify lo permite; por eso los hooks locales ayudan, pero no sustituyen a una validación en el servidor o en CI.

Paso 3: Validar el formato de los mensajes con commit-msg

Un formato de mensaje consistente facilita leer el historial y generar changelogs. Este hook exige el estilo Conventional Commits (tipo(ámbito): descripción) en la primera línea del mensaje. Crea el archivo:

nano .githooks/commit-msg
#!/usr/bin/env bash
set -euo pipefail

first_line="$(head -n 1 "$1")"
pattern='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._-]+\))?!?: .+'

# Permite los mensajes que genera Git para merges y reverts
if [[ "$first_line" =~ ^(Merge|Revert) ]]; then
    exit 0
fi

if ! [[ "$first_line" =~ $pattern ]]; then
    echo "commit-msg: el mensaje no sigue el formato 'tipo(ámbito): descripción'" >&2
    echo "  Ejemplo: fix(nginx): corregir la redirección a HTTPS" >&2
    exit 1
fi

Hazlo ejecutable y compruébalo con un mensaje incorrecto y otro correcto:

chmod +x .githooks/commit-msg
git commit --allow-empty -m "cambios varios"
commit-msg: el mensaje no sigue el formato 'tipo(ámbito): descripción'
  Ejemplo: fix(nginx): corregir la redirección a HTTPS
git commit --allow-empty -m "chore: activar hooks del repositorio"
[main 3f2a1c9] chore: activar hooks del repositorio

Añade los hooks al repositorio para que el resto del equipo los reciba:

git add .githooks
git commit -m "chore: añadir git hooks compartidos"

Paso 4: Crear un repositorio bare en el servidor

Para desplegar con git push necesitas en el servidor un repositorio bare, es decir, sin directorio de trabajo, que solo almacena el historial. El hook post-receive copiará después los archivos al directorio que sirve Nginx.

Conéctate al servidor:

ssh your_user@your_server_ip

Instala Git si no lo está y crea el repositorio bare con main como rama inicial:

sudo apt update
sudo apt install git
git init --bare --initial-branch=main ~/site.git
Initialized empty Git repository in /home/your_user/site.git/

Da a tu usuario la propiedad del directorio de destino para que el hook pueda escribir en él sin sudo:

sudo mkdir -p /var/www/your_domain
sudo chown your_user:your_user /var/www/your_domain

Paso 5: Escribir el hook post-receive

Git ejecuta post-receive una vez por push y le pasa por la entrada estándar una línea por cada referencia actualizada, con el formato revisión_antigua revisión_nueva nombre_referencia. El hook solo desplegará cuando se actualice la rama main. Créalo en el servidor:

nano ~/site.git/hooks/post-receive
#!/usr/bin/env bash
set -euo pipefail

readonly TARGET="/var/www/your_domain"
readonly REPO="$HOME/site.git"
readonly BRANCH="main"

while read -r _ newrev ref; do
    if [[ "$ref" != "refs/heads/$BRANCH" ]]; then
        echo "Recibida $ref: no se despliega (solo $BRANCH)"
        continue
    fi

    echo "Desplegando $BRANCH (${newrev:0:7}) en $TARGET"
    git --work-tree="$TARGET" --git-dir="$REPO" checkout --force "$BRANCH"
    echo "Despliegue completado"
done

git checkout --force con --work-tree extrae los archivos de la rama en TARGET. El repositorio bare recuerda qué archivos extrajo la última vez, así que los que borres del repositorio también desaparecen del destino. La revisión antigua no se necesita, por eso se lee en la variable descartable _.

Hazlo ejecutable:

chmod +x ~/site.git/hooks/post-receive

Si tu proyecto necesita un paso de compilación (por ejemplo npm ci && npm run build), ejecútalo en el hook después del checkout, con cd "$TARGET" delante, y apunta Nginx al directorio de salida.

Paso 6: Desplegar con git push

Vuelve a tu equipo local y añade el servidor como remoto con el nombre production:

git remote add production your_user@your_server_ip:site.git

Envía la rama main:

git push production main

Las líneas que empiezan por remote: son la salida del hook en el servidor:

Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Writing objects: 100% (12/12), 3.41 KiB | 3.41 MiB/s, done.
Total 12 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Desplegando main (8c41e02) en /var/www/your_domain
remote: Already on 'main'
remote: Despliegue completado
To your_server_ip:site.git
 * [new branch]      main -> main

Comprueba en el servidor que los archivos están en su sitio y que Nginx los sirve:

ssh your_user@your_server_ip ls /var/www/your_domain
curl -I http://your_domain
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: text/html

A partir de ahora, cada git push production main publica la última versión. Ten en cuenta que se publica todo el contenido del repositorio, incluido .githooks: no guardes en él secretos ni archivos que no deban ser accesibles desde la web, o configura Nginx para servir solo un subdirectorio como public/. Para volver a una versión anterior, revierte el commit problemático con git revert y vuelve a hacer push.

Paso 7: Reiniciar un servicio tras el despliegue (opcional)

Si despliegas una aplicación que corre como servicio de systemd, el hook debe reiniciarla. En lugar de dar a tu usuario sudo sin contraseña para todo, permítele solo ese comando. Crea una regla con visudo, que valida la sintaxis antes de guardar:

sudo visudo -f /etc/sudoers.d/deploy-your_app
your_user ALL=(root) NOPASSWD: /usr/bin/systemctl restart your_app.service

Añade esta línea en el hook post-receive, justo después del checkout:

sudo /usr/bin/systemctl restart your_app.service

Comprueba que la regla funciona sin pedir contraseña:

sudo -n /usr/bin/systemctl restart your_app.service && echo "reinicio permitido"

Solución de problemas

hint: The '.githooks/pre-commit' hook was ignored because it's not set as executable.: falta el permiso de ejecución. Aplica chmod +x al hook. En Windows, marca el bit en Git con git update-index --chmod=+x .githooks/pre-commit.

El hook no se ejecuta después de clonar: core.hooksPath no se clona. Ejecuta git config core.hooksPath .githooks en el nuevo clon.

/usr/bin/env: 'bash\r': No such file or directory: el hook tiene finales de línea CRLF. Conviértelo con sed -i 's/\r$//' ruta/del/hook.

remote: fatal: this operation must be run in a work tree o el push se rechaza con refusing to update checked out branch: el remoto no es un repositorio bare o el hook no pasa --work-tree. Crea el repositorio con git init --bare y revisa el comando checkout del hook.

Permission denied al escribir en /var/www/your_domain: el directorio no pertenece al usuario con el que haces push. Revisa el chown del paso 4.

Conclusión

Has configurado hooks locales compartidos que bloquean commits con errores y mensajes sin formato, y un repositorio bare con un hook post-receive que despliega tu sitio en Ubuntu 24.04 con cada git push.

Como siguientes pasos puedes:

  • Desplegar en un directorio nuevo por versión y cambiar un enlace simbólico al final, para que el cambio sea atómico y el rollback inmediato.
  • Añadir un hook pre-receive en el servidor que rechace pushes que no pasen las pruebas, ya que los hooks locales se pueden saltar.
  • Pasar a un sistema de CI/CD como GitHub Actions o GitLab CI cuando el proyecto necesite compilación, pruebas y varios entornos.