Suivre les changements dans vos dépôts

Si vous êtes un utilisateur de Linux (ou UNIX) et que vous utilisez des gestionnaires de versions (en anglais SCM pour Source Control Management) comme Fossil ou Git, peut-être que comme moi vous ne voulez pas immédiatement faire de commit de vos changements. Vous ne voulez pas que vos brouillons soient suivis. Mais de temps en temps vous oubliez ces changements et vous risquez de perdre votre travail.

Voudriez-vous voir une liste de vos dépôts avec des changements non suivis à chaque fois que vous ouvrez une session shell ou un terminal? Avoir quelque chose comme ça?

Changements non suivis présents dans ces dossiers:
/home/nales/git-project
/home/nales/fossil-project
/home/nales/website
~ $

Cet article est sur des scripts que j'utilise au quotidien pour suivre les changements dans mes dépôts. Vous pouvez trouver mes scripts (et d'autres) dans mon dépôt de scripts. Ces scripts fonctionnent sur Linux, ils fonctionnent peut-être sur des systèmes BSD ou MacOS. Le but de cet article n'est pas d'expliquer ligne par ligne mes scripts, mais de vous donnez l'idée d'utiliser une solution similaire et de vous montrer comment adapter ma solution à la vôtre.

Le terme SCM est utilisé ici pour désigner les gestionnaires de versions (Source Control Management software) comme Fossil et Git. Si vous ne savez pas ce qu'est un SCM, je vous conseille fortement de voir Git qui est le plus populaire en ce moment en 2020 (même si je préfère Fossil). En résumé, un SCM est un logiciel pour organiser les changements dans vos fichiers (souvent du code source). Vous pouvez diverger de l'état actuel pour faire des changements et les appliquer plus tard, vous pouvez collaborer avec d'autres personnes et appliquer leurs changements aux vôtres (merge), vous pouvez aussi voir l'historique des changements pour voir comment les fichiers ont évolué et pour revenir en arrière.

Planifier la solution

Le but est de vérifier si il y a des changements non suivis (uncommit) de notre côté dans les dépôts suivis par la solution. Le but n'est pas de vérifier les changements distants d'autres personnes. Nous utilisons Fossil et Git.

Nous faisons deux scripts pour ça. Le premier script, which-scm, sert à trouver quel SCM est utilisé dans un dossier et quel est le chemin racine du dépôt. Le second script, follow-scm, sert à ajouter les dépôts que l'on veut suivre et à vérifier lesquels ont des changements non suivis. Quand nous ouvrons un nouveau shell dans un terminal, les dépôts avec des changements non suivis sont affichés.

Trouver le SCM avec which-scm

Fossil et Git créent tous les deux un fichier caché dans le chemin racine de leurs dépôts. Fossil crée une base de donnée SQLite nommé .fslckout. (Fossil a été créé par les développeurs de SQLite.) Git créé un dossier nommé .git.

Nous voulons que which-scm affiche dans la sortie standard le nom du SCM et le chemin racine du dépôts, le tout séparé par une tabulation pour simplifier l'intégration par d'autres scripts. Si un SCM est trouvé, le script retourne 0. Si aucun SCM n'est trouvé, parce que le dossier vérifié n'est pas un dépôt ou que c'est un dépôt d'un SCM non géré comme SVN, alors rien n'est affiché et le script retourne 1.

which-scm
#!/bin/sh

print_info_and_exit() {
    printf "%s\t%s\n" "$1" "$(pwd)"
    exit 0
}

check_local() {
    if [ -d ".git" ]; then
        print_info_and_exit "git"
    elif [ -f ".fslckout" ]; then
        print_info_and_exit "fossil"
    fi;
}

# Si aucun argument, utilise le dossier courant comme argument.
path="${1:-$(pwd)}"

if [ ! -d "$path" ]; then
    printf "%s is a not a directory path.\n" "$path" 1>&2
    exit 2
fi

# Vérifie le dossier et ses parents jusqu'à abandonner à "/" (racine système)
cd "$path"
while [ "$(pwd)" != "/" ]; do
    check_local
    cd ..
done

exit 1

Utiliser le script dans un shell nous donne ces résultats.

$ which-scm /home/nales/git-project/src/
git	/home/nales/git-project
$ which-scm /home/nales/fossil-project/src/
fossil	/home/nales/fossil-project
$ which-scm /home/nales/not-a-repository/
$ echo $?
1

Écrire follow-scm

follow-scm est utilisé pour choisir quels dépôts à suivre dans le système. Il utilise which-scm pour garder en mémoire quel SCM est utilisé et le chemin racine du dépôt. Écrivons le script ensemble en partie.

Définir les fichiers et les commandes du script

Nous devons trouver où stocker la liste des dépôts que follow-scm suit. Pour laisser l'utilisateur organiser où les fichiers se trouvent dans son système, le script suit les spécifications XDG (XDG Base Directory specification). Nous écrivons nos données dans le dossier défini par XDG_DATA_HOME et dans le fichier follow-scm.

Il y a plusieurs commandes à définir. Je préfère utiliser des variables pour stocker les noms de commandes, c'est plus facile à refactoriser. Je ne vais pas implémenter dans cet article toutes les commandes car ce n'est pas le but. Jetez un coup d'œil sur mon dépôt de scripts pour une implémentation complète. Voici ici le squelette du code.

follow-scm
#!/bin/sh

COMMAND_HELP="help"
COMMAND_ADD="add"
COMMAND_REMOVE="remove"
COMMAND_AUTOREMOVE="autoremove"
COMMAND_CHECK="check"
COMMAND_LIST="list"
COMMAND_EDIT="edit"

DATA_FILE="${XDG_DATA_HOME:-$HOME/.local/share}/follow-scm"
if [ ! -e "$DATA_FILE" ]; then
    touch "$DATA_FILE"
fi

# Nous avons besoin d'un fichier temporaire pour remove et autoremove
# ATTENTION: Linux utilise /tmp, pas toujours le cas ailleurs. À adapter!
TEMP_DIR="${TMPDIR:-/tmp}"
TEMP_FILE="$TEMP_DIR/follow-scm-$$"

is_known_scm() {
    [ "$1" = "fossil" ] ||
    [ "$1" = "git" ]
}

# Nous définissons les commandes ici.

if [ $# -eq 0 ]; then
    help
else
    case "$1" in
        "$COMMAND_HELP")
            shift; help "$@"
            ;;
        "$COMMAND_ADD")
            shift; add "$@"
            ;;
        "$COMMAND_REMOVE")
            shift; remove "$@"
            ;;
        "$COMMAND_AUTOREMOVE")
            shift; autoremove
            ;;
        "$COMMAND_LIST")
            shift; list
            ;;
        "$COMMAND_EDIT")
            shift; edit
            ;;
        "$COMMAND_CHECK")
            shift; check
            ;;
        *)
            print_unknown_command "$1"
    esac
fi

Ajouter les dépôts à suivre

La commande add prend un chemin en argument. Si il n'y en a pas, follow-scm utilise le dossier courant. Nous stockons simplement le résultat de which-scm dans le fichier de données défini plus haut.

follow-scm
add() {
    path="${1:-$(pwd)}"
    which_output="$(which-scm "$path")"
    if [ -z "$which_output" ]; then
        printf "%s is not in a SCM repository.\n" "$path"
        exit 1
    fi
    scm="$(printf %s "$which_output" | cut -f1)"
    if ! is_known_scm "$scm"; then
        # Devrait afficher les erreurs et instructions ici
        exit 1
    fi

    # Pas besoin d'ajouter de nouveau une entrée déjà présente
    if grep --quiet "^$which_output\$" "$DATA_FILE"; then
        return
    fi
    printf "%s\n" "$which_output" >> "$DATA_FILE"
}

Puis il n'y a qu'à ajouter les dépôts que vous voulez suivre.

$ follow-scm add /home/nales/git-project/src/
$ follow-scm add /home/nales/fossil-project/src/

Vérifier les changement dans Fossil et Git

Avec Fossil, il y a besoin de deux commandes pour vérifier les changements. La commande fossil changes affiche les changements pour les fichiers déjà suivis, la commande fossil extras afficher les nouveaux fichiers qui ne sont pas encore suivi. Si aucun changement n'est présent, rien n'est affiché.

Avec Git, nous n'avons besoin que de git status -s pour afficher les changements. Si il n'y en a pas, rien n'est affiché.

follow-scm
get_scm_from_entry() {
    printf "%s" "$1" | cut -f1
}

get_path_from_entry() {
    printf "%s" "$1" | cut -f2
}

check() {
    line_number=0
    while read -r line; do
        line_number=$((line_number + 1))

        # Ici il devrait y avoir des tests pour vérifier l'intégrité des
        # entrées stockées dans le fichier de données. Je les passe pour être
        # concis.

        path="$(get_path_from_entry "$line")"
        scm="$(get_scm_from_entry "$line")"

        cd "$path" || continue
        if [ "$scm" = "git" ] && [ -n "$(git status -s)" ]; then
            printf "%s\n" "$path"
        elif [ "$scm" = "fossil" ] && [ -n "$(fossil changes && fossil extras)" ]; then
            printf "%s\n" "$path"
        fi
    done < "$DATA_FILE"
}

Maintenant nous pouvons vérifier les dépôts avec des changements non suivis.

$ follow-scm check
/home/nales/fossil-project

Supprimer des entrées

Sans aller dans les détails ici, utilisez quelque chose comme ça pour supprimer des dépôts à suivre dans le fichier de donnée:

follow-scm
remove() {
    path="${1:-$(pwd)}"
    which_output="$(which-scm "$path")"
    if [ -z "$which_output" ]; then
        # Afficher ici les erreurs et proposer d'utiliser autoremove
        exit 1
    fi
    grep -v "^$which_output\$" "$DATA_FILE" > "$TEMP_FILE"
    mv -f "$TEMP_FILE" "$DATA_FILE"
}

autoremove() {
    printf "" > "$TEMP_FILE"
    while read -r line; do
        # ...
        # Tester ici chaque ligne, passer les entrées incorrectes.
        # ...

        # Écrire uniquement les entrées correctes
        printf "%s\n" "$line" >> "$TEMP_FILE"
    done < "$DATA_FILE"
    mv -f "$TEMP_FILE" "$DATA_FILE"
}

Pour supprimer le dépôt Git actuellement suivi de la liste des dépôts à suivre, faire:

$ follow-scm remove /home/nales/git-project/src/

Je ne vais pas montrer les autres commandes comme help ou list parce que ce n'est pas le but ici.

Afficher la liste dans une nouvelle session shell

Nous voulons afficher les dépôts avec des changements non suivis quand nous ouvrons une nouvelle session shell (quand vous ouvrez un terminal). Pour ce faire, ajoutez dans votre .bashrc ou .zshrc (ou l'équivalent pour votre système) ces lignes.

# Affiche en rouge la liste des dépôts suivis avec changements non suivis.
echo -en "\033[1;31m"
follow-scm check
echo -en "\033[0m"

Cela fonctionne, mais ce n'est pas très efficace. À chaque fois que vous ouvrez une nouvelle session shell, vous devez attendre la fin de la commande avant de pouvoir faire quoique ce soit. Améliorons ça.

La première chose à faire est de créer un script nommé follow-scm-check qui affiche le résultat de follow-scm check dans un fichier cache, ou supprime ce fichier si il n'y a rien à afficher.

follow-scm-check
#!/bin/sh

# Si appelé dans une tâche cron, ne pas oublier d'exporter la variable
# d'environnement PATH avec les chemins pour ce script et ses dépendances
# (follow-scm, git, fossil...). Si vous ne le faites pas, peut-être que rien ne
# fonctionnera parce que le shell ne saura pas trouver ces scripts.

CACHE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/follow-scm-check"

result="$(follow-scm check)"

if [ -n "$result" ]; then
    echo "Uncommitted changes present in these directories:" > "$CACHE_FILE"
    printf "%s\n" "$result" >> "$CACHE_FILE"
elif [ -f "$CACHE_FILE" ]; then
    rm "$CACHE_FILE"
fi

Maintenant nous pouvons appeler ce script toutes les 10 minutes en utilisant une tâche cron. Si vous ne savez pas ce qu'est une tâche cron, c'est une tâche dont l'exécution est planifiée selon des règles que vous choisissez. Ici se trouve la tâche cron dans mon système, utilisez la commande crontab -e pour l'ajouter:

*/10 * * * * export PATH=$PATH":$HOME/bin:$HOME/.local/bin:/usr/local/bin/"; follow-scm-check

C'est vraiment pénible de travailler avec des tâches cron. Elles ne s'exécutent pas dans votre shell habituel donc même si votre script fonctionne quand vous l'exécutez manuellement, peut-être ne fonctionnera-t-il pas en tant que tâche cron. Vous devez bien vérifier la configuration de la variable d'environnement PATH. Vous voulez voir le journal d'erreurs en cas de problème? Que vous êtes drôle! Pensez-vous vraiment que sur Linux il y a un moyen standard entre toutes les différents versions pour ce genre de choses simples? Il n'y a vraiment pas à chercher plus loin pourquoi $CURRENT_YEAR n'est pas the year of the Linux desktop...

Explication de la tâche cron.

  1. Nous configurons la tâche pour être lancée toutes les 10 minutes avec */10 * * * *.
  2. Le reste de la ligne est sur le script à exécuter. La configuration de la variable d'environnement PATH correspond à mes besoins sur mon système.
    • J'ai sauvegardé follow-scm-check dans $HOME/bin/ parce que c'est un script personnel.
    • J'ai mon exécutable fossil dans $HOME/.local/bin/ qui est un dossier standard pour ça.
    • Et les scripts venant de mon dépôt de scripts qui inclut fossil-scm sont installés dans /usr/local/bin/ comme le font les gens de suckless pour leurs logiciels. (Vous devriez vérifier le Makefile de mon dépôt au cas où l'emplacement a changé depuis que j'ai écrit cet article.)
  3. Enfin on appelle follow-scm-check.

Maintenant nous pouvons mettre à jour le code dans .bashrc ou .zshrc (ou équivalent) pour afficher la liste des dépôts avec des changements non suivis.

# Affiche en rouge la liste des dépôts suivis avec changements non suivis.
if [ -f ~/.cache/follow-scm-check ]; then
    echo -en "\033[1;31m"
    cat ~/.cache/follow-scm-check
    echo -en "\033[0m"
fi

Astuce: suivre tous les dépôts dans votre système

Plutôt que de manuellement chercher et ajouter tous les dépôts à suivre dans votre système, nous pouvons utiliser find pour automatiser ça.

Pour trouver les dépôts, nous pouvons chercher les dossiers .git et les fichiers .fslckout. Pour plus de détails, regardez le manuel de find à la partie OPERATORS. Nous supposons que les dépôts sont dans le dossier Projects.

$ find Projects \( -type d -a -name '.git' -o -type f -a -name '.fslckout' \)
Projects/Website/.fslckout
Projects/Scripts/.git
Projects/G-Code-Viewer/.git

find a une commande pour exécuter un script pour chaque chose qu'il trouve. Nous ne pouvons pas directement utiliser follow-scm add parce que cette commande ne fonctionne qu'avec des dossiers mais ici find retourne également les chemins pour les fichiers .fslckout. La solution la plus simple est de créer un script temporaire.

add-scm
#!/bin/sh
# ${1%/*} enlève le plus petit suffixe dans "$1" avec le pattern "/*". Ici cela
# enlève de l'argument "/.git" et "/.fslckout", donc retourne le chemin du
# dossier.
# Exemple: "/home/nales/project/.git" devient "/home/nales/project"
follow-scm add "${1%/*}"

Nous pouvons maintenant mélanger notre commande find avec notre script temporaire.

$ find Projects \( -type d -a -name '.git' -o -type f -a -name '.fslckout' \) -exec ./add-scm {} \;

Tous les dépôts sont maintenant suivis. Si vous voulez en supprimer certains, vous pouvez utiliser follow-scm edit qui ouvre le fichier data follow-scm dans un éditeur de texte. Ici le code de la commande si cela vous intéresse:

follow-scm
edit() {
    # Vérifie les variables d'environnement VISUAL puis EDITOR sinon choisit vi
    editor="${VISUAL:-${EDITOR:-vi}}"
    "$editor" "$DATA_FILE"
}

En résumé

Pour afficher tous les dépôts dans votre système avec des changements non suivis, vous pouvez utiliser follow-scm présent dans mon dépôt de scripts ou créer votre propre version. Après ça, créez simplement une tâche cron pour faire une vérification régulièrement. Affichez ensuite le résultat de cette vérification quand vous ouvrez une session shell.

Avec toutes les explications que je vous ai données, vous êtes plus que prêt pour faire votre propre version. Amusez-vous bien!