[IMP] migration: offer the COW checker at the error prompt
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 <db> », « --database <db> », 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) <noreply@anthropic.com>
This commit is contained in:
parent
188dad9960
commit
2997dfeb26
2 changed files with 90 additions and 9 deletions
|
|
@ -1609,6 +1609,23 @@ TRANSLATIONS = {
|
||||||
"fr": "Rien de désinstallé.",
|
"fr": "Rien de désinstallé.",
|
||||||
"en": "Nothing uninstalled.",
|
"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)": {
|
"Migration statistics (read-only)": {
|
||||||
"fr": "📊 Statistiques de la migration (lecture seule)",
|
"fr": "📊 Statistiques de la migration (lecture seule)",
|
||||||
"en": "📊 Migration statistics (read-only)",
|
"en": "📊 Migration statistics (read-only)",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
@ -312,6 +313,52 @@ class TodoUpgrade:
|
||||||
answer = input(t("Choice (0-3, default 1): ")).strip()
|
answer = input(t("Choice (0-3, default 1): ")).strip()
|
||||||
return {"0": None, "2": "cli", "3": "stats"}.get(answer, "tui")
|
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 <base> », « --database
|
||||||
|
<base> », et l'argument positionnel des scripts addons
|
||||||
|
(`update_addons_all.sh <base>`, `install_addons*.sh <base> <modules>`).
|
||||||
|
"""
|
||||||
|
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 <key> --apply"
|
||||||
|
)
|
||||||
|
print(f" {warn}")
|
||||||
|
|
||||||
def show_stats(self):
|
def show_stats(self):
|
||||||
"""Écran de statistiques, en lecture seule : rien n'est écrit, ni
|
"""Écran de statistiques, en lecture seule : rien n'est écrit, ni
|
||||||
dans la base ni dans le journal de migration."""
|
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
|
# failure, never as a success (defence in depth: exec_command_live now
|
||||||
# always sets one, but a silent None must not skip this prompt).
|
# always sets one, but a silent None must not skip this prompt).
|
||||||
if (status is None or status) and wait_at_error:
|
if (status is None or status) and wait_at_error:
|
||||||
print("[1] to redo the command")
|
database_name = self.database_from_command(cmd)
|
||||||
wait_status = (
|
while True:
|
||||||
input(
|
print("[1] to redo the command")
|
||||||
"💬 Error detected, press to continue or ctrl+c to stop : "
|
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
|
# psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist
|
||||||
# LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (...
|
# 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
|
||||||
|
# <xpath …> 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":
|
if wait_status == "1":
|
||||||
return self.todo_upgrade_execute(
|
return self.todo_upgrade_execute(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue