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
sudoal 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:
| Hook | Dónde se ejecuta | Cuándo | Puede cancelar |
|---|---|---|---|
pre-commit | Local | Antes de crear el commit | Sí |
commit-msg | Local | Tras escribir el mensaje, recibe su ruta en $1 | Sí |
pre-push | Local | Antes de enviar al remoto | Sí |
post-receive | Servidor | Después de aceptar un push, recibe las referencias por stdin | No |
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-receiveen 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.
