diff --git a/script/todo/proxmox_menu.py b/script/todo/proxmox_menu.py index 65ea910..2c01844 100644 --- a/script/todo/proxmox_menu.py +++ b/script/todo/proxmox_menu.py @@ -1483,6 +1483,13 @@ class ProxmoxMenuMixin: f" ERPLibre : {spec['install'].get('label') or ''}" f" ({spec['install'].get('branch')})" ) + # La VM CLONE la branche depuis le dépôt distant : tout ce qui + # tourne dedans — install_proxmox.sh compris — vient de là, pas + # d'ici. Un correctif non poussé est invisible pour elle. + for ligne in self._qemu_branch_gap_lines( + spec["install"].get("branch") or "" + ): + print(f" {ligne}") return self._is_yes_default_yes( input(f"\n{t('Deploy this VM now? (Y/n): ')}") ) diff --git a/script/todo/qemu_deploy.py b/script/todo/qemu_deploy.py index 4d8c95a..9ea06f1 100644 --- a/script/todo/qemu_deploy.py +++ b/script/todo/qemu_deploy.py @@ -885,6 +885,12 @@ class QemuDeployMixin: f" {t('Install:')} {t('branch')} {br_txt}, " f"{t('profile')} {lb_txt}, {env}" ) + # La VM ne reçoit pas CE checkout : elle CLONE la branche depuis + # le dépôt distant. Un correctif commité ici et non poussé n'y est + # donc pas, et le défaut « revient » alors qu'il est corrigé — + # vécu deux fois de suite sur install_proxmox.sh. + for ligne in self._qemu_branch_gap_lines(br_txt): + print(f" {ligne}") else: print(f" {t('Install:')} {t('no')}") flavour = spec.get("desktop") diff --git a/script/todo/qemu_manage.py b/script/todo/qemu_manage.py index 50f32b0..04f4694 100644 --- a/script/todo/qemu_manage.py +++ b/script/todo/qemu_manage.py @@ -2537,6 +2537,67 @@ class QemuManageMixin: nom = (res.stdout or "").strip() return "" if res.returncode or nom == "HEAD" else nom + @staticmethod + def _qemu_branch_gap(branche): + """Combien de commits LOCAUX manquent à origin/, et lesquels. + + Rend (nombre, [sujets]) — (0, []) quand il n'y a rien à dire, ou quand + la question ne se pose pas (pas de dépôt, pas de distant). + + Pourquoi le déploiement s'en soucie : la VM ne reçoit PAS le checkout + d'ici, elle CLONE la branche depuis le dépôt distant. Tout ce qui + tourne dans la VM — install_proxmox.sh, les scripts d'installation, le + Makefile — vient donc de là. + + Vécu deux fois de suite. Un correctif de install_proxmox.sh, commité + ici, absent du distant : chaque VM déployée ensuite recevait l'ancien + script, et le défaut « revenait » alors qu'il était corrigé. Rien ne + le disait ; il a fallu comparer les deux versions à la main. + """ + if not branche: + return 0, [] + try: + res = subprocess.run( + [ + "git", + "log", + "--oneline", + "--no-decorate", + f"origin/{branche}..HEAD", + ], + capture_output=True, + text=True, + timeout=15, + ) + except (OSError, subprocess.SubprocessError): + return 0, [] + # Une branche inconnue du distant, ou aucun distant : ce n'est pas un + # écart à signaler, c'est une question qui ne se pose pas. + if res.returncode: + return 0, [] + sujets = [ + ligne.strip() + for ligne in (res.stdout or "").splitlines() + if ligne.strip() + ] + return len(sujets), sujets + + def _qemu_branch_gap_lines(self, branche, limite=3): + """Les lignes à dire avant de déployer, ou [].""" + nombre, sujets = self._qemu_branch_gap(branche) + if not nombre: + return [] + lignes = [ + f"⚠ {t('The VM clones')} origin/{branche}, " + f"{t('not this checkout.')}", + f" {nombre} {t('local commit(s) are missing there:')}", + ] + lignes += [f" {s}" for s in sujets[:limite]] + if nombre > limite: + lignes.append(f" … {nombre - limite} {t('more')}") + lignes.append(f" → git push {t('to deploy your own work.')}") + return lignes + def _qemu_branch_list(self): """Branches distantes d'ERPLibre, triées. Vide si le réseau manque. diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 20f5471..73c72b5 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -3382,6 +3382,22 @@ TRANSLATIONS = { "fr": "rejouer install_proxmox.sh sur l'hôte : il corrige /etc/hosts et empêche cloud-init de le défaire.", "en": "replay install_proxmox.sh on the host: it fixes /etc/hosts and stops cloud-init undoing it.", }, + "The VM clones": { + "fr": "La VM clone", + "en": "The VM clones", + }, + "not this checkout.": { + "fr": "et non ce checkout.", + "en": "not this checkout.", + }, + "local commit(s) are missing there:": { + "fr": "commit(s) local(aux) y manquent :", + "en": "local commit(s) are missing there:", + }, + "to deploy your own work.": { + "fr": "pour déployer votre propre travail.", + "en": "to deploy your own work.", + }, "No storage able to hold a VM disk.": { "fr": "Aucun stockage capable d'héberger un disque de VM.", "en": "No storage able to hold a VM disk.", diff --git a/test/test_proxmox_form.py b/test/test_proxmox_form.py index bccd158..adbc7fc 100644 --- a/test/test_proxmox_form.py +++ b/test/test_proxmox_form.py @@ -686,6 +686,82 @@ class TestUnParcMixte(unittest.TestCase): self.assertEqual(vu["kw"]["desktop"], "") +class TestLaVmCloneLeDepotDistant(unittest.TestCase): + """« Le problème est revenu » — alors qu'il était corrigé. + + La VM ne reçoit pas le checkout d'ici : elle CLONE la branche depuis le + dépôt DISTANT. Tout ce qui tourne dedans — install_proxmox.sh, les + scripts d'installation, le Makefile — vient donc de là. Un correctif + commité ici et non poussé lui est invisible. + + Vécu deux fois de suite : la correction de /etc/hosts était dans le + checkout depuis la veille, absente du distant, et chaque VM déployée + ensuite recevait l'ancien script. Il a fallu comparer les deux versions à + la main pour le voir. Rien ne le disait.""" + + def _todo(self, sortie, code=0): + import sys + + sys.argv = ["todo.py"] + from script.todo.todo import TODO + + todo = TODO.__new__(TODO) + faux = mock.Mock(returncode=code, stdout=sortie) + return todo, faux + + def test_the_gap_is_counted_and_named(self): + todo, faux = self._todo( + "abc1234 [FIX] un correctif\ndef5678 [ADD] autre chose\n" + ) + with mock.patch("subprocess.run", return_value=faux): + nombre, sujets = todo._qemu_branch_gap("develop") + self.assertEqual(nombre, 2) + self.assertIn("[FIX] un correctif", sujets[0]) + + def test_nothing_to_say_when_the_remote_is_up_to_date(self): + todo, faux = self._todo("") + with mock.patch("subprocess.run", return_value=faux): + self.assertEqual(todo._qemu_branch_gap("develop"), (0, [])) + self.assertEqual(todo._qemu_branch_gap_lines("develop"), []) + + def test_an_unknown_remote_branch_is_not_a_gap(self): + # « origin/xyz » inconnu fait échouer git : ce n'est pas un écart à + # signaler, c'est une question qui ne se pose pas. Le dire quand même + # serait un avertissement à chaque déploiement d'une branche neuve. + todo, faux = self._todo("", code=128) + with mock.patch("subprocess.run", return_value=faux): + self.assertEqual(todo._qemu_branch_gap("nouvelle"), (0, [])) + + def test_no_branch_asks_nothing(self): + todo, _faux = self._todo("") + self.assertEqual(todo._qemu_branch_gap(""), (0, [])) + + def test_the_long_list_is_trimmed_but_counted(self): + todo, faux = self._todo( + "\n".join(f"c{i} sujet {i}" for i in range(10)) + ) + with mock.patch("subprocess.run", return_value=faux): + lignes = todo._qemu_branch_gap_lines("develop", limite=2) + texte = " ".join(lignes) + self.assertIn("10", texte, "le nombre TOTAL doit rester lisible") + self.assertIn("8", texte, "et ce qui n'est pas montré, dit") + self.assertIn("git push", texte) + + def test_both_screens_say_it_before_deploying(self): + # L'avertissement ne vaut que là où on peut encore renoncer. + import inspect + + from script.todo.proxmox_menu import ProxmoxMenuMixin + from script.todo.qemu_deploy import QemuDeployMixin + + for fn in ( + ProxmoxMenuMixin._pve_confirm_spec, + QemuDeployMixin._qemu_print_recap, + ): + with self.subTest(fonction=fn.__name__): + self.assertIn("_qemu_branch_gap_lines", inspect.getsource(fn)) + + class TestLePontQuiNeMeneraitNullePart(unittest.TestCase): """Le pont NAT était écrit AVANT qu'on sache si le NAT existe.