From 2997dfeb26fa3273003498884398ea7da5b6c046 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sun, 2 Aug 2026 04:36:25 -0400 Subject: [PATCH] [IMP] migration: offer the COW checker at the error prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failed update_addons_all left one option, « [1] to redo the command » — useless against the failure that actually causes it, since redoing an upgrade whose COW copy has drifted fails again identically. The prompt now offers to run reset_stale_cow_views on the database concerned: [1] to redo the command [2] Check the COW views that drifted (technolibre_…_upgrade_15) Choosing [2] runs the checker and comes back to the same prompt, so the usual sequence — look, repair, redo — happens without leaving the migration. The database is read from the failed command itself, since the executor is not told which one it is. Three forms are recognised: « -d », « --database », and the positional argument of the addons scripts. No database found, no [2] offered — « make format » gets the old prompt unchanged. The option only ever REPORTS. Resetting a copy can erase a real customisation, so it stays a separate deliberate command, printed with the exact line to run and a reminder to read the diff first. Verified: the six command forms parsed correctly (and two that hold no database at all), [2] invoking the tool then re-offering the choice, no [2] on a command without a database, and the whole chain against the real _upgrade_15, where it prints the two drifted copies followed by the reset line. Co-Authored-By: Claude Opus 4.8 (1M context) --- script/todo/todo_i18n.py | 17 ++++++++ script/todo/todo_upgrade.py | 82 +++++++++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 630e8b7..ab78bcc 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1609,6 +1609,23 @@ TRANSLATIONS = { "fr": "Rien de désinstallé.", "en": "Nothing uninstalled.", }, + "Check the COW views that drifted": { + "fr": "Vérifier les vues COW en retard sur leur vue module", + "en": "Check the COW views that drifted", + }, + "Tool not found": { + "fr": "Outil introuvable", + "en": "Tool not found", + }, + "To reset one of them onto its module view:": { + "fr": "Pour réinitialiser l'une d'elles sur sa vue module :", + "en": "To reset one of them onto its module view:", + }, + "Read the diff first: a copy can hold a customisation.": { + "fr": "Lire le diff d'abord : une copie peut porter une" + " personnalisation.", + "en": "Read the diff first: a copy can hold a customisation.", + }, "Migration statistics (read-only)": { "fr": "📊 Statistiques de la migration (lecture seule)", "en": "📊 Migration statistics (read-only)", diff --git a/script/todo/todo_upgrade.py b/script/todo/todo_upgrade.py index 3fd616a..7565aa7 100755 --- a/script/todo/todo_upgrade.py +++ b/script/todo/todo_upgrade.py @@ -6,6 +6,7 @@ import datetime import json import logging import os +import re import shutil import sys import zipfile @@ -312,6 +313,52 @@ class TodoUpgrade: answer = input(t("Choice (0-3, default 1): ")).strip() return {"0": None, "2": "cli", "3": "stats"}.get(answer, "tui") + @staticmethod + def database_from_command(cmd): + """Nom de base visé par une commande, ou "" si indécelable. + + Sert à proposer le bon outil au bon moment quand une commande échoue. + On reconnaît les trois formes du dépôt : « -d », « --database + », et l'argument positionnel des scripts addons + (`update_addons_all.sh `, `install_addons*.sh `). + """ + if not cmd: + return "" + match = re.search(r"(?:^|\s)(?:-d|--database)[=\s]+([\w.-]+)", cmd) + if match: + return match.group(1) + match = re.search( + r"\./script/addons/\w+\.sh\s+([\w.-]+)", + cmd, + ) + return match.group(1) if match else "" + + def check_stale_cow_views(self, database_name): + """Lance le détecteur de copies COW en retard sur leur vue module. + + Purement consultatif : il n'écrit rien sans « --reset … --apply », que + l'on propose seulement après avoir montré le diff — réinitialiser une + copie peut effacer une personnalisation réelle.""" + script_path = os.path.join( + PATH_MIGRATION_GLOBAL, "reset_stale_cow_views.py" + ) + if not os.path.exists(script_path): + print(f"⚠️ {t('Tool not found')}: {script_path}") + return + status, _cmd = self.todo_upgrade_execute( + f"{PYTHON_BIN} ./{script_path} -d {database_name}", + wait_at_error=False, + ) + if status: + # Sortie 1 = des écarts ont été trouvés (le script les a listés). + warn = t("Read the diff first: a copy can hold a customisation.") + print(f"\n💡 {t('To reset one of them onto its module view:')}") + print( + f" {PYTHON_BIN} ./{script_path} -d {database_name}" + f" --reset --apply" + ) + print(f" {warn}") + def show_stats(self): """Écran de statistiques, en lecture seule : rien n'est écrit, ni dans la base ni dans le journal de migration.""" @@ -2650,17 +2697,34 @@ class TodoUpgrade: # failure, never as a success (defence in depth: exec_command_live now # always sets one, but a silent None must not skip this prompt). if (status is None or status) and wait_at_error: - print("[1] to redo the command") - wait_status = ( - input( - "💬 Error detected, press to continue or ctrl+c to stop : " + database_name = self.database_from_command(cmd) + while True: + print("[1] to redo the command") + if database_name: + print( + f"[2] {t('Check the COW views that drifted')}" + f" ({database_name})" + ) + wait_status = ( + input( + "💬 Error detected, press to continue or ctrl+c to" + " stop : " + ) + .strip() + .lower() ) - .strip() - .lower() - ) - # psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist - # LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (... + # psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist + # LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (... + + if wait_status == "2" and database_name: + # Le motif d'échec le plus fréquent ici est « Element + # cannot be located in parent view » : une copie + # COW en retard sur sa vue module. On propose l'outil sur + # place, puis on repose le choix pour rejouer. + self.check_stale_cow_views(database_name) + continue + break if wait_status == "1": return self.todo_upgrade_execute(