Conociendo los hooks de Git

En la edición anterior ya vimos cómo podemos crear comandos automatizados Usando ficheros Makefile para trabajar de manera homogénea en equipo. Ahora veremos cómo garantizar unos mínimos de calidad en nuestos desarrollos mediante los hooks de Git.

Introducción

¿Qué es un hook de Git?

Git hooks son shell scripts que Git ejecuta antes o después de eventos como: commit, push, and receive.

Los hooks de Git son una funcionalidad que se instala por defecto con Git, por lo que no se necesita de descarga alguna. Además se ejecutan en entorno local.

Cómo funcionan los hooks

Todo repositorio Git tiene una carpeta .git/hooks con un shell script por cada hook que podemos configurar.

Estos shell scripts pueden ser desarrollados en el lenguaje que se prefiera siempre y cuando puedan ser ejecutados por consola.

Para activar un determinado hook basta con sobreescribir o reemplazar un determinado hook por un shell script válido y hace que dicho script sea ejecutable.

Hooks disponibles

  • applypatch-msg
  • pre-applypatch
  • post-applypatch
  • pre-commit
  • prepare-commit-msg
  • commit-msg
  • post-commit
  • pre-rebase
  • post-checkout
  • post-merge
  • pre-receive
  • update
  • post-receive
  • post-update
  • pre-auto-gc
  • post-rewrite
  • pre-push

Creando nuestros propios hooks

Para ejemplificar el proceso haremos uso de los dos hooks más relevantes:

  • pre-commit, que siempre se ejecuta antes de cada commit. Aquí aplicaremos las comprobaciones relativas al estilo, sintaxis, detección de dependencias no utilizadas...
  • pre-push, que siempre se ejecuta antes de hacer push al repositorio remoto. Aquí aplicaremos las comprobaciones de aplicación relativas a los tests unitarios, funcionales, etc.

De este modo garantizamos que los ficheros en el commit no tienen errores y, además, siempre comprobamos antes de cada push si los cambios no causan un impacto negativo en la lógica de nuestra aplicación, minimizando así la inclusión de errores en nuestro proyecto.

Herramientas necesarias

Para poder realizar las comprobaciones, debemos instalar algunas dependencias como herramientas de desarrollo, ya que estas herramientas no deben ser instaladas en producción por razones obvias de seguridad y rendimiento:

pre-commit

#!/bin/bash

# Run '$(chmod +x pre-commit; ln -s .git/hooks/pre-commit pre-commit)' to install

RESET='\e[0m'
RED='\e[31m'
GREEN='\e[32m'
YELLOW='\e[33m'

SERVICE_NAME='api'

#----------------------------------------------------------------------------------------------------------------------
# FUNCTIONS
#----------------------------------------------------------------------------------------------------------------------

function __displayBox() #(emoji, color, message)
{
    caption=$(printf "%-72s" "${3}")

    echo -e "${2}"
    echo -e "┌─────────────────────────────────────────────────────────────────────────────┐"
    echo -e "│ ${1} ${RESET}${caption}${2} │"
    echo -e "└─────────────────────────────────────────────────────────────────────────────┘"
    echo -e "${RESET}"
}

#----------------------------------------------------------------------------------------------------------------------

function __checkPhpLint() #(file)
{
    task=$(printf "%-40s" "PHP Linter")

    output=$(docker-compose exec ${SERVICE_NAME} php -l ${1})
    RETVAL=$?

    if [[ $RETVAL != 0 ]]
    then
        hasErrors=1

        echo -e "    🔴 ${task}"
        echo -e ""
        echo -e "${output}"
    else
        echo -e "    🟢 ${task}"
    fi

    if [[ $hasErrors == 1 ]]
    then
        __displayBox 👹 ${RED} 'COMMIT IS NOT ALLOWED!'
        exit 1
    fi
}

#----------------------------------------------------------------------------------------------------------------------

function __checkPhpCodeBeautifierAndFixer() #(file)
{
    task=$(printf "%-40s" "PHPCBF - PHP Code Beautifier and Fixer")

    output=$(docker-compose exec ${SERVICE_NAME} ./vendor/bin/phpcbf --standard=PSR12 ${1})
    RETVAL=$?

    if [[ $RETVAL != 0 ]]
    then
        hasErrors=1

        echo -e "    🔴 ${task}"
        echo -e "${output}";
    else
        echo -e "    🟢 ${task}"
    fi

    if [[ $hasErrors == 1 ]]
    then
        __displayBox 👹 ${RED} 'COMMIT IS NOT ALLOWED!'
        exit 1
    fi
}

#----------------------------------------------------------------------------------------------------------------------

function __checkPhpStan() #(file)
{
    task=$(printf "%-40s" "PHPSTAN - PHP Static Analyzer")

    output=$(docker-compose exec ${SERVICE_NAME} ./vendor/bin/phpstan analyse --memory-limit=2G ${1})
    RETVAL=$?

    if [[ $RETVAL != 0 ]]
    then
        hasErrors=1

        echo -e "    🔴 ${task}"
        echo -e "";
        echo -e "${output}";
    else
        echo -e "    🟢 ${task}"
    fi

    if [[ $hasErrors == 1 ]]
    then
        __displayBox 👹 ${RED} 'COMMIT IS NOT ALLOWED!'
        exit 1
    fi
}

#----------------------------------------------------------------------------------------------------------------------
# MAIN LOGIC
#----------------------------------------------------------------------------------------------------------------------

phpFiles=$(git diff --cached --name-only --diff-filter=ACMR HEAD | grep ".php$");

for file in $phpFiles
do
    echo -e "- Checking [ ${YELLOW}$file${RESET} ]"

    __checkPhpLint $file
    __checkPhpCodeBeautifierAndFixer $file

    if [ "$1" != "--no-verify" ]; then
        __checkPhpStan $file
    fi
done

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# At this point all checkpoints were successfully passed

__displayBox 🚀 ${GREEN} 'GIT COMMIT IS ALLOWED!'
exit 0

pre-push

Como ya hemos estado Usando ficheros Makefile tenemos un target test que permite ejecutar las baterías de pruebas de nuestra aplicación bajo demanda. Para simplificar el script, haremos uso de dicho comando:

#!/bin/bash

# Run '$(chmod +x pre-push; ln -s .git/hooks/pre-push pre-push)' to install

RESET='\e[0m'
RED='\e[31m'
GREEN='\e[32m'
YELLOW='\e[33m'

#----------------------------------------------------------------------------------------------------------------------
# FUNCTIONS
#----------------------------------------------------------------------------------------------------------------------

function __displayBox() #(emoji, color, message)
{
    caption=$(printf "%-72s" "${3}")

    echo -e "${2}"
    echo -e "┌─────────────────────────────────────────────────────────────────────────────┐"
    echo -e "│ ${1} ${RESET}${caption}${2} │"
    echo -e "└─────────────────────────────────────────────────────────────────────────────┘"
    echo -e "${RESET}"
}

#----------------------------------------------------------------------------------------------------------------------
# MAIN LOGIC
#----------------------------------------------------------------------------------------------------------------------

if ! make tests; then
    __displayBox 👹 ${RED} 'GIT PUSH IS NOT ALLOWED!'
    exit 1
fi

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# At this point all checkpoints were successfully passed

__displayBox 🚀 ${GREEN} 'GIT PUSH IS ALLOWED!'
exit 0

Descarga

Puedes descargar el fichero de ejemplo de pre-commit aquí y el fichero de ejemplo de pre-push aquí.

Versión del documento

[^v1.0]: Última Modificación: 12/03/2021