Un répertoire vidé à la main passe `os.path.isdir` puis fait lever l'ouverture du dépôt, ce qui emportait tout le reste du lot. Ces états — clone interrompu, `.git` effacé — sont courants sur un checkout de développement, et sans rapport avec les dépôts qui attendent leur remote. Les écarts sont rendus plutôt qu'affichés au fil de l'eau : noyés dans la trace d'un lot de plus de cent dépôts, ils ne se voient plus. La sortie reste nulle, comme déjà pour un répertoire absent. Vérifié dans des dépôts jetables ; 6 tests neufs, dont 2 échouent si le garde-fou est retiré. --- EN --- A directory emptied by hand passes `os.path.isdir` then makes opening the repository raise, which took down the whole rest of the batch. Such states — an interrupted clone, a deleted `.git` — are common on a development checkout, and unrelated to the repositories awaiting their remote. Deviations are returned rather than printed as they happen: drowned in the trace of a batch of over a hundred repositories, they can no longer be seen. The exit code stays zero, as it already was for a missing directory. Checked in throwaway repositories; 6 new tests, 2 of which fail if the guard is removed. Assisted-by: Claude Opus 5
120 lines
4.4 KiB
Python
120 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
"""Un dépôt fautif n'arrête pas le lot, et le bilan le nomme à la fin.
|
|
|
|
Un checkout de développement porte des répertoires vidés à la main, des
|
|
clones interrompus, des `.git` effacés. Ils n'ont rien à voir avec les cent
|
|
quarante autres dépôts qui attendent leur nouveau remote, et une trace de
|
|
cette longueur noie l'avertissement qui passe au moment où il survient.
|
|
"""
|
|
|
|
import io
|
|
import os
|
|
import subprocess
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
from tempfile import TemporaryDirectory
|
|
|
|
from script.git.git_change_remote_https_to_git import (
|
|
change_all_remotes,
|
|
print_report,
|
|
)
|
|
from script.git.git_tool import GitTool
|
|
|
|
HTTPS = "https://github.com/OCA/web.git"
|
|
GIT = "git@github.com:OCA/web.git"
|
|
|
|
|
|
def _depot(chemin, url=HTTPS):
|
|
"""Un vrai dépôt git. Rien n'est bouchonné : c'est la lecture et
|
|
l'écriture des remotes par git qu'on vérifie."""
|
|
os.makedirs(chemin, exist_ok=True)
|
|
subprocess.run(["git", "init", "-q", chemin], check=True)
|
|
subprocess.run(
|
|
["git", "-C", chemin, "remote", "add", "origin", url], check=True
|
|
)
|
|
return chemin
|
|
|
|
|
|
def _url(chemin):
|
|
return subprocess.run(
|
|
["git", "-C", chemin, "remote", "get-url", "origin"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout.strip()
|
|
|
|
|
|
class UnDepotFautifNArretePasLeLot(unittest.TestCase):
|
|
def _lot(self, racine, noms):
|
|
with redirect_stdout(io.StringIO()) as sortie:
|
|
changed, skipped = change_all_remotes(
|
|
GitTool(), [{"name": n} for n in noms], racine
|
|
)
|
|
return changed, skipped, sortie.getvalue()
|
|
|
|
def test_un_repertoire_sans_depot_git_est_ignore(self):
|
|
"""Le répertoire existe et ne porte plus de dépôt git :
|
|
`os.path.isdir` passe, l'ouverture du dépôt lève."""
|
|
with TemporaryDirectory() as racine:
|
|
_depot(os.path.join(racine, "addons/Bon"))
|
|
os.makedirs(os.path.join(racine, "addons/Vide"))
|
|
changed, skipped, _ = self._lot(
|
|
racine, ["addons/Bon", "addons/Vide"]
|
|
)
|
|
self.assertEqual(_url(os.path.join(racine, "addons/Bon")), GIT)
|
|
self.assertEqual(changed, 1)
|
|
self.assertEqual(len(skipped), 1)
|
|
self.assertIn("addons/Vide", skipped[0][0])
|
|
self.assertIn("no git repository", skipped[0][1])
|
|
|
|
def test_le_lot_continue_apres_le_fautif(self):
|
|
"""Le fautif est au MILIEU : ce qui le suit doit être servi."""
|
|
with TemporaryDirectory() as racine:
|
|
os.makedirs(os.path.join(racine, "addons/Vide"))
|
|
_depot(os.path.join(racine, "addons/Apres"))
|
|
changed, skipped, _ = self._lot(
|
|
racine, ["addons/Vide", "addons/Apres"]
|
|
)
|
|
self.assertEqual(_url(os.path.join(racine, "addons/Apres")), GIT)
|
|
self.assertEqual(changed, 1)
|
|
self.assertEqual(len(skipped), 1)
|
|
|
|
def test_un_repertoire_absent_est_ignore_aussi(self):
|
|
with TemporaryDirectory() as racine:
|
|
changed, skipped, _ = self._lot(racine, ["addons/Absent"])
|
|
self.assertEqual(changed, 0)
|
|
self.assertEqual(skipped[0][1], "directory is missing")
|
|
|
|
def test_rien_a_signaler_ne_signale_rien(self):
|
|
with TemporaryDirectory() as racine:
|
|
_depot(os.path.join(racine, "addons/Bon"))
|
|
changed, skipped, _ = self._lot(racine, ["addons/Bon"])
|
|
self.assertEqual((changed, skipped), (1, []))
|
|
|
|
|
|
class LeBilanSeLitALaFin(unittest.TestCase):
|
|
"""Ce que l'humain lit d'un lot long, c'est sa fin."""
|
|
|
|
def _bilan(self, total, changed, skipped):
|
|
with redirect_stdout(io.StringIO()) as sortie:
|
|
print_report(total, changed, skipped)
|
|
return sortie.getvalue()
|
|
|
|
def test_il_nomme_chaque_depot_ignore_et_sa_raison(self):
|
|
texte = self._bilan(
|
|
3, 1, [("addons/Vide", "no git repository"), ("addons/X", "gone")]
|
|
)
|
|
self.assertIn("3 repo, 1 remote updated, 2 skipped", texte)
|
|
self.assertIn("addons/Vide — no git repository", texte)
|
|
self.assertIn("addons/X — gone", texte)
|
|
|
|
def test_sans_incident_il_ne_liste_rien(self):
|
|
texte = self._bilan(2, 2, [])
|
|
self.assertIn("2 repo, 2 remote updated, 0 skipped", texte)
|
|
self.assertNotIn("Skipped", texte)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|