Conforme nuestro repositorio va creciendo vamos a necesitar tener más control sobre qué código se sube y cómo. Por supuesto, para facilitar la vida del equipo de desarrollo, vamos a necesitar automatizarlo.
Ahí es donde entran los hooks de Git, una forma de automatizar ciertas tareas que deben ejecutarse en ciertos momentos del proceso de subida de código.
¿Qué es un hook?
Un hook, o punto de enganche, es la posibilidad de ejecutar una acción o script cada vez que ocurre un evento determinado de Git.
Estos scripts se colocan dentro de la carpeta.git/hookscon el nombre del evento que escuchará para ser ejecutado. En esa carpeta, por defecto, ya existen unos pocos scripts de ejemplo:
applypatch-msg.sample pre-push.sample
commit-msg.sample pre-rebase.sample
post-update.sample prepare-commit-msg.sample
pre-applypatch.sample update.sample
pre-commit.sample
Este listado contiene los hooks más usados pero no son los únicos.
Entra a cualquier repositorio que tengas inicializado con git y luego lista los hooks que hay en la carpeta.git/hooks. ¿Qué hay dentro? Revisa el contenido de los ficheros.
¿Qué hooks hay disponibles?
Existen puntos de enganche para eventos en el lado del cliente y en el lado del servidor. En este manual vamos a revisar los más útiles de ambos lados aunque, en realidad, los que normalmente vas a usar son los disponibles en el cliente.
En este diagrama puedes ver todos los hooks del lado del cliente y del servidor
Hooks del lado del cliente (repositorio local)
Los hooks del lado del cliente sólo afectan al repositorio local que los contiene. Esto significa que puedes tener el mismo repositorio clonado de forma local varias veces y, sin embargo, ejecutar diferentes hooks. Esto es muy importante que lo tengas en cuenta:los hooks son sólo parte del repositorioqueloscontieneynoseguardannisesincronizanconotrasréplicas del repositorio automáticamente. Más adelante veremos cómo podemos hacer que los hooks se instalen cada vez que un usuario quiera trabajar en un repositorio. Los hooks más importantes son:
- pre-commit
- prepare-commit-msg
- commit-msg
- post-commit
- pre-push
Otros hooks interesantes a destacar:
- post-checkout
- post-merge
pre-commit
Se ejecuta inmediatamente después del comando git commitpero antes de que Git te pida un mensaje para el commit y que genere el objeto de commit. Desde el script puedes salir con un código diferente a 0 para abortar el commit.
¿Para qué lo puedes usar?: Puede ser un buen sitio para ejecutar el linter sobre los archivos que han sido modificados. También podrías comprobar si se está intentando hacer un commit de demasiados archivos o líneas de código cambiadas.
prepare-commit-msg
Se ejecuta cuando Git va a crear el mensaje que usará en el commit. Igual que pre-commit, también puedes abortar el commit si es necesario.
¿Para qué lo puedes usar?: Puedes usarlo para modificar el mensaje del commit o añadir cualquier información extra. Ten en cuenta que, después de preparado, el usuario todavía podría modificar el mensaje por lo que no es un buen sitio para comprobar que el mensaje de commit sigue un patrón estándar.
commit-msg
Se ejecuta una vez que se ha preparado el mensaje de commit y el usuario ha hecho todas las modificaciones sobre el mismo. Desde este script todavía puedes abortar que el commit se realice si es necesario.
¿Para qué lo puedes usar?: Es el sitio perfecto para hacer todas las comprobaciones pertinentes al mensaje. Por ejemplo, si usas una convención de mensajes de commit comoconventional-commit, aquí puedes usar una herramienta comocommitlint para asegurate que el mensaje sigue la convención y abortar el commit si no lo hace.
¿Sabías que al hacer un commit puedes saltar la ejecución de los commits pre-commit y commit-msg? Para ello, puedes usar el comando git commit —no-verify. Aunque no es recomendable, puedes usarlo si quieres que no se ejecuten los hooks.
post-commit
Una vez que el commit ha sido grabado, se ejecuta este hook que te permite ser notificado que la operación se ha realizado con éxito. Esto quiere decir que desde este script ya no puedes evitar que el commit se realice y no importa el código de salida del script.
¿Para qué lo puedes usar?: Su uso principal es la de notificar. Podrías usarlo para enviar un mensaje a Slack a tu equipo cada vez que haces un commit en tu repositorio local, para que vean que estás trabajando en algo. Un poco raro… pero puede ser útil.
pre-push
Este hook se ejecuta tras llamar al comando git pushy ocurre antes de que se haya transferido ningún objeto al repositorio remoto. Desde este script puedes evitar que el push se realice si devuelves un código de error.
¿Para qué lo puedes usar?: Para ejecutar una batería de tests de forma que evites que llegue a un repositorio remoto cambios que no pasan las pruebas.
post-checkoutypost-merge
Estos dos hooks son ejecutados tras realizar un git checkouty git mergerespectivamente.
¿Para qué lo puedes usar?: A diferencia delpost-commitque sólo sirve para posibles notificaciones, estos dos hooks pueden tener otras utilidades. La más interesante es la de limpiar el directorio de trabajo, tras realizar un checkout, o el de limpiar las ramas que ya no se usan tras realizar un merge.
Hooks del lado del servidor (repositorio remoto)
En el lado del servidor, en elrepositorio remoto, tienes tres puntos de enganche:
- pre-receive
- update
- post-receive
Es muy probable que nunca necesites realizar ningún tipo de script que se ejecute en uno de estos puntos de enganche. Sin embargo, es interesante conocerlos ya que páginas como GitHub o GitLab los usan intensivamente a la hora de construir.
pre-receive
Este hook se ejecuta una vez antes de que las referencias sean actualizadas en el repositorio remoto. Al estar en el lado del servidor, es capaz de rechazar cualquier cambio si es necesario.
¿Para qué lo puedes usar?: Si fuese posible, puedes tener en el lado del servidor un script para comprobar que los commits que se quieren guardar están bien formados. O que el usuario que intenta grabar los commits tiene los permisos necesarios para hacerlo. También puedes evitarcommitsque tengan conflictos o intentos de rebase.
update
Muy similar apre-receive, pero este hook se ejecuta por cada referencia que quiere ser actualizada. Si, por ejemplo, en un sólopushintentas enviar 3 ramas distintas… entonces este hook será ejecutado 3 veces.
¿Para qué lo puedes usar?: Similar a los usos depre-receive, con la diferencia que puedes evitar de una forma granular cada actualización (de forma que algunas referencias sí puedan ser grabadas y otras no).
post-receive
Tras grabar los cambios en el repositorio remoto, este hook se ejecuta con la información de cada cambio. Ya no puedes evitar que los cambios sean grabados y sólo sirve para notificar.
¿Para qué lo puedes usar?: En este caso para notificaciones. Por ejemplo, enviar un correo a todos los usuarios del repositorio que se han grabado nuevos cambios en el repositorio remoto. O reflejar en una UI las nuevas referencias, ramas o commits disponibles.
¿Cómo puedo crear mi propio hook?
Para crear un propio hook sólo tienes que crear un archivonombre-del-hooken la carpeta.git/hooksy en él poner el código que quieras que se ejecute. Puedes usar todo tipo de intérpretes de lenguaje de programación comobash, node, python, perl, etc.
Verificar archivos con un _linter_ antes de hacer commit
Vamos a crear un hook que verifica los archivos con unlinterantes de hacer un commit. Para ello, vamos a crear un script que se ejecute antes de hacer un commit.
Primero creamos el ficheropre-commiten la carpeta.git/hooks. En un sistema Unix, puedes ejecutar el siguiente comando.
$ touch .git/hooks/pre-commit
Además, necesitamos hacer que el script sea ejecutable. Lo podemos conseguir con este comando:
$ chmod +x .git/hooks/pre-commit
Ahora, con tu editor favorito, lo primero que vamos a hacer es indicar que el script que queremos ejecutar debe usar el shell disponible en tu sistema.
# !/bin/sh
Ahora vamos a escribir nuestro pequeño script. Es posible que no conozcas este lenguaje, ya que es un script de Shell, pero es muy sencillo y te voy a dejar unos pocos comentarios para que no te pierdas.
# creamos una variable para guardar el número de salida del proceso
linter_exit_code= 1
# ejecutamos un comando para recuperar sólo los archivos .js y .jsx
staged_js_files= $( git diff —cached —diff-filter=d —name-only | grep ** -E’.(js|jsx)$’ )
# ejecutamos nuestro linter contra los archivos .js y .jsx modificados
./node_modules/.bin/standard $staged_js_files--quiet --fix
# con esta línea, recuperamos el código de salida del
linter_exit_code=$?
# volvemos a añadir los archivos modificados a la lista de commits
git add -f $staged_js_files
# si el código de salida es distinto de cero, es que hubo algún error
if [ $linter_exit_code -ne 0 ]
then
echo "[error] Linter errors have occurred"
exit 1
else
echo "[ok] Linter did not find any errors"
exit 0
fi
Ahora que ya lo tenemos, podemos usar colores en la salida de la consola usando unas secuencias de ANSI definida. Vamos a crear estas variables para usarlas en los mensajes:
# variable para hacer el texto de color rojo
RED="\033[1;31m"
# variable para hacer el texto de color verde
GREEN="\033[1;32m"
# variable para quitar el color del texto
NC="\033[0m"
Estas variables, ahora se pueden interpolar en el string para hacer que los mensajes sean de un solo color. Es importante al finalizar el mensaje limpiar el color de la cadena de texto.
if [ $linter_exit_code -ne 0 ] then echo” ${ RED } [error] Linter errors have occurred ${ NC } ” exit 1 else echo” ${ GREEN } [ok] Linter did not find any errors ${ NC } ” exit 0 fi
Y al final quedaría de esta forma:
RED="\033[1;31m"
GREEN="\033[1;32m"
NC="\033[0m"
linter_exit_code= 1 staged_js_files= $( git diff —cached —diff-filter=d —name-only | grep ** -E’.(js|jsx)$’ )
./node_modules/.bin/standard $staged_js_files--quiet --fix
linter_exit_code=$?
git add -f $staged_js_files
if [ $linter_exit_code -ne 0 ] then echo” ${ RED } [error] Linter errors have occurred ${ NC } ” exit 1 else echo” ${ GREEN } [ok] Linter did not find any errors ${ NC } ” exit 0 fi
Limpiar ramas locales tras merge
Script compatible con git version 2.28.0 o superior.
Vamos a crear un hook que nos permita limpiar las ramas locales y remotas tras hacer un merge. Para ello, vamos a crear un script que se ejecute después de hacer un merge. He añadido pequeños comentarios para que no te pierdas.
# !/bin/bash
exec< /dev/tty
# Recuperamos el nombre de la rama actual
branch_name= $( git branch | grep "*" | sed "s/\* //" )
# Recuperamos el nombre de la rama recién fusionada
reflog_message= $( git reflog -1 )
merged_branch_name= $( echo$reflog_message| cut -d" " -f 4 | sed "s/://" )
main_branch= $( git config --get init.defaultBranch )
# Si la rama recién fusionada es la principal, salimos
if [[ $merged_branch_name= $main_branch ]]; then
exit 0
fi
# Escribimos en la consola información
echo" "
echo "Fusionada \"$merged_branch_name\" en \"$branch_name\". "
# Preguntamos al usuario la acción
read-p "¿Quieres borrar la rama \"$merged_branch_name\"? (y/N)" answer
# Revisamos que la respuesta sea "y"
if [[ "$answer" == "y" ]]; then
# Borramos la rama local
echo "Borrando la rama local: \"$merged_branch_name\""
git branch -d$merged_branch_name
# Borramos la rama remota
echo "Borrando la rama remota"
git push origin --delete$merged_branch_name
exit 1
else
echo "No he podido borrar la rama \"$merged_branch_name\""
fi
Ahora, para lograr que se ejecute este pequeño script tras cada merge, vamos a crear un hook en la carpeta.git/hooksy le vamos a dar permisos de ejecución.
chmod +x .git/hooks/post-merge
Anímate a modificar el script para dejarlo a tu gusto y que se adapte a tus necesidades.