Django: migrer automatiquement entre les checkouts git
Posté le Wed 14 February 2024 dans développement
Le problème
Lorsque l'on travaille en équipe et que l'on relit le code, il arrive souvent que l'on navigue entre les branches, et avec la façon dont Django gère l'état de la base de données et son système de migrations, vous pouvez facilement oublier de désappliquer les migrations avant de changer de branche. Voyons comment nous pouvons améliorer cela.
Détection des différences dans les migrations
Le bon moment pour vérifier l'état des migrations est lorsque le code change en lançant un git checkout
, nous pouvons donc utiliser les hooks git pour cela.
Maintenant, dans ce hook, nous devons avoir le contexte de configuration de django (paramètres, base de données, et ainsi de suite), donc nous allons travailler avec une commande personnalisée pour avoir facilement le contexte de Django.
Pour vérifier les différences dans les migrations, j'ai d'abord regardé l'option --prune
de la commande migrate
car elle détecte la différence entre les migrations appliquées (existant dans la base de données) et celles déclarées dans le code.
Cette partie utilise le MigrationExecutor
pour vérifier les migrations manquantes appliquées et les supprimer :
set(executor.loader.applied_migrations) - set(executor.loader.disk_migrations)
Nous pouvons utiliser ce bout de code pour simplement vérifier les migrations manquantes appliquées, et avertir l'utilisateur à ce sujet :
from collections import defaultdict
from django.core.management import BaseCommand
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
class Command(BaseCommand):
def handle(self, *args, **options):
executor = MigrationExecutor(connection)
applied_missing_migrations = set(executor.loader.applied_migrations) - set(
executor.loader.disk_migrations
)
if applied_missing_migrations:
self.stderr.write(
"Warning: you have applied migrations that no longer exist:"
)
for app_label, migration_name in applied_missing_migrations:
self.stderr.write(
f" - {app_label}: {migration_name}", style_func=self.style.WARNING
)
# Determine which migration number to revert to
revert_commands = defaultdict(lambda: "9999")
for app_label, migration_name in applied_missing_migrations:
[migration_number, *_] = migration_name.split("_")
if int(migration_number) - 1 < int(revert_commands[app_label]):
revert_commands[app_label] = (
"zero"
if int(migration_number) - 1 == 0
else f"{int(migration_number) - 1:0>4}"
)
# Write the result to stdout for further use
for app_label, migration_number in revert_commands.items():
self.stdout.write(f"./manage.py migrate {app_label} {migration_number}")
Notez qu'en plus de l'avertissement, nous retournons également la commande à exécuter dans la sortie standard pour ramener la base de données à un état commun (entre l'état de la base de données et l'état du fichier), cela sera utile dans notre hook git.
Revenir automatiquement sur les migrations lors du checkout
Maintenant que nous pouvons détecter les différences, allons un peu plus loin et effectuons les migrations lors du checkout des branches. Cela peut être fait via un script bash qui sera utilisé pour le hook git :
#!/bin/bash
if [ "$3" -eq 1 ] && [ -z ${GIT_CHECKOUTING+x} ]
then
export GIT_CHECKOUTING=1
# Get our commands to execute
result=$(python src/manage.py check_applied_missing_migrations 2> /dev/null)
if [ -n "$result" ]
then
echo "Migrations must be reverted:"
echo ""
echo "$result"
git checkout - > /dev/null
(cd src && eval "$result")
git checkout - > /dev/null
(cd src && ./manage.py migrate)
fi
unset GIT_CHECKOUTING
fi
GIT_CHECKOUTING
est une variable d'environnement que nous définissons pour éviter les appels récursifs- on utilise
git checkout -
pour revenir à la branche précédente et migrer vers l'état commun, puis pour revenir à la branche orignalement demandée
Quelques limitations
Cela ne fonctionne que si :
- vos migrations ont des opérations inverses (paramètre
reverse
) - l'état de votre base de données est sain
- le répertoire de travail
src
dans ce hook git est adapté à votre projet