#!/bin/sh
#
# Hook PRE-PUSH — Lintorn controle avant d'envoyer quoi que ce soit sur GitHub.
#
# POURQUOI PRE-PUSH ET PAS PRE-COMMIT ?
#   Un commit est local et se rattrape (`git commit --amend`, `git reset`).
#   Un push est PUBLIC : une fois parti, c'est dans l'historique de tout le
#   monde. Et ça n'alourdit pas chaque commit — on peut commiter dix fois et
#   ne payer Lintorn qu'une seule fois.
#
# CE QU'IL FAIT
#   1. calcule la liste des fichiers réellement poussés
#   2. lance Lintorn, avec un focus sur CES fichiers
#   3. si un contrôle bloquant est rouge → demande confirmation
#
# INSTALLATION (sur chaque clone, et après chaque mise à jour de Lintorn) :
#   lintorn --installer-hook
#
#   ⚠️ Sans cette commande, git regarde dans .git/hooks/, n'y trouve rien, et
#   le push part sans contrôle — sans un message. Ni cette copie ni le réglage
#   `core.hooksPath` ne suivent un clone, et `pip install -U lintorn` ne
#   touche pas à la copie : le contrôle « Hook pre-push » dit quand relancer.
#
# POUR TOUT COURT-CIRCUITER : git push --no-verify

# ─────────────────────────────────────────────────────────────────────────────
# 0. SE PLACER DANS LE PROJET AUDITÉ
# ─────────────────────────────────────────────────────────────────────────────
# git lance ses hooks depuis le SOMMET du dépôt. Sur un monorepo, le
# sous-projet où ce hook a été installé n'est donc pas le dossier courant :
# Lintorn auditait la racine, n'y trouvait aucune configuration, et laissait
# passer un push que les règles du sous-projet interdisaient.
#
# Le hook vit dans <projet>/.lintorn/hooks/ : deux niveaux au-dessus de lui,
# c'est le projet. Vrai aussi quand projet et dépôt ne font qu'un.
cd "$(dirname "$0")/../.." || exit 1

# Se placer dans le projet NE SUFFIT PAS : sans consigne, Lintorn remonte
# jusqu'au `.git`, donc jusqu'au sommet du dépôt. On lui nomme la racine à
# auditer — celle où ce hook a été installé. Calculée à l'exécution : aucun
# chemin de machine n'est écrit dans ce fichier.
LINTORN_RACINE="$(pwd)"
export LINTORN_RACINE

# COMMENT ON APPELLE LINTORN
#
# Il est installé par pip, donc normalement dans le PATH. Mais un hook git
# tourne dans l'environnement du shell qui a lancé `git push` : si le venv
# n'est pas activé (bouton de VS Code, IDE, tâche planifiée), la commande est
# introuvable. On essaie les trois formes, puis on abandonne PROPREMENT —
# un hook qui plante sur « command not found » bloquerait tous les push sans
# jamais expliquer pourquoi.
if command -v lintorn >/dev/null 2>&1; then
    LINTORN="lintorn"
elif python -c "import lintorn" >/dev/null 2>&1; then
    LINTORN="python -m lintorn"
elif python3 -c "import lintorn" >/dev/null 2>&1; then
    LINTORN="python3 -m lintorn"
else
    echo ""
    echo "── Lintorn introuvable — le push passe ──────────────────"
    echo "   Le hook est branche, mais Lintorn n'est pas installe"
    echo "   dans cet environnement :   pip install lintorn"
    echo ""
    exit 0
fi

# ─────────────────────────────────────────────────────────────────────────────
# 1. QUELS FICHIERS PARTENT DANS CE PUSH ?
# ─────────────────────────────────────────────────────────────────────────────
# git envoie au hook, sur son entrée standard, une ligne par branche poussée :
#   <ref locale> <sha local> <ref distante> <sha distant>
# Le sha distant vaut 000...0 quand la branche n'existe pas encore là-bas.
FICHIERS=""
ZERO="0000000000000000000000000000000000000000"

while read -r _local_ref local_sha _remote_ref remote_sha; do
    [ -z "$local_sha" ] && continue
    if [ "$remote_sha" = "$ZERO" ]; then
        # Branche neuve : on compare à la branche principale plutôt qu'à rien,
        # sinon on listerait l'histoire entière du dépôt.
        BASE=$(git merge-base "$local_sha" origin/main 2>/dev/null || echo "")
    else
        BASE="$remote_sha"
    fi
    if [ -n "$BASE" ]; then
        # `--relative` et `-- .` : les chemins sortent relatifs au projet, et
        # ceux des autres sous-projets d'un monorepo ne s'y mêlent pas.
        #
        # `core.quotePath=false` : sans lui, git rend un nom accentué sous la
        # forme "src/fichier_\303\251xotique.py" — un nom qui ne désigne aucun
        # fichier. Le focus ne le trouvait pas et annonçait « aucun defaut
        # dedans » sur un fichier fautif.
        NOUVEAUX=$(git -c core.quotePath=false diff --name-only --relative \
                       "$BASE" "$local_sha" -- . 2>/dev/null)
        # Séparés par des SAUTS DE LIGNE, jamais par des espaces : un espace
        # dans un nom de fichier le coupait en deux, et aucune des deux
        # moitiés ne désignait quoi que ce soit.
        FICHIERS="$FICHIERS
$NOUVEAUX"
    fi
done

# Un nom par ligne → les arguments positionnels, qui savent porter un nom
# contenant une espace. (`sh` n'a pas de tableaux ; c'est la façon POSIX.)
ANCIEN_IFS=$IFS
IFS='
'
set -f                      # pas de glob : un nom contenant `*` reste ce nom
# shellcheck disable=SC2086
set -- $FICHIERS
set +f
IFS=$ANCIEN_IFS

echo ""
echo "── Lintorn controle avant push ─────────────────────────────"

# --rapide : on saute pip-audit et vulture (lents, et non bloquants de toute
# façon). Un push ne doit pas se transformer en pause café.
if [ "$#" -gt 0 ]; then
    # Les noms passent par l'ENTRÉE STANDARD, un par ligne — jamais en
    # arguments. Sous Windows, une ligne de commande tient en 32 767
    # caractères : un push de mille fichiers (le premier d'un projet, un venv
    # commité par erreur) la dépassait, Lintorn ne démarrait même pas, et le
    # push était refusé sans un mot d'explication. `printf` est interne au
    # shell : lui n'a pas de limite. Et un fichier nommé `--help` ne peut plus
    # être pris pour une option.
    #
    # `$LINTORN` reste NON protégé : il peut valoir `python -m lintorn`.
    # shellcheck disable=SC2086
    printf '%s\n' "$@" | $LINTORN --rapide --fichiers-stdin
else
    $LINTORN --rapide
fi
CODE=$?

if [ $CODE -eq 0 ]; then
    echo "── Lintorn OK, push autorise ─────────────────────────────"
    echo ""
    exit 0
fi

# ─────────────────────────────────────────────────────────────────────────────
# 2. ÇA COINCE — on demande, on n'impose pas
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo "╔════════════════════════════════════════════════════════╗"
echo "║  Des controles BLOQUANTS sont en alerte                ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
echo "  Detail complet : .lintorn/rapport.md"
echo ""

# git a déjà consommé l'entrée standard (la liste des refs) : pour poser une
# question, il faut lire directement le terminal via /dev/tty.
#
# On ESSAIE de l'ouvrir plutôt que de tester son existence : dans un script
# automatisé, le fichier /dev/tty existe mais ne s'ouvre pas.
#
# Le test se fait dans un SOUS-SHELL `( : < /dev/tty )` : une redirection qui
# échoue affiche son erreur au moment où elle est appliquée, donc un
# `2>/dev/null` posé après arriverait trop tard. En enfermant le tout dans un
# sous-shell, le 2>/dev/null couvre bien l'ensemble.
if ! ( : < /dev/tty ) 2>/dev/null; then
    echo "  ────────────────────────────────────────────────────"
    echo "  PUSH ANNULE : pas de terminal pour repondre."
    echo ""
    echo "  Tu pousses probablement depuis le bouton de VS Code."
    echo "  VS Code lance git SANS terminal : impossible de poser"
    echo "  la question, donc on annule par securite."
    echo ""
    echo "  POUR VOIR CE MESSAGE DANS VS CODE :"
    echo "     Affichage > Sortie (Ctrl+Shift+U) > liste 'Git'"
    echo ""
    echo "  POUR DECIDER TOI-MEME, pousse depuis un terminal :"
    echo "     git push"
    echo ""
    echo "  POUR PASSER OUTRE SANS DISCUTER :"
    echo "     git push --no-verify"
    echo "  ────────────────────────────────────────────────────"
    echo ""
    exit 1
fi

printf "  Pousser quand meme ? [o/N] "
read -r REPONSE < /dev/tty

case "$REPONSE" in
    o|O|y|Y|oui|OUI)
        echo "  -> Push force par l'utilisateur."
        echo ""
        exit 0
        ;;
    *)
        echo "  -> Push ANNULE."
        echo ""
        exit 1
        ;;
esac
