[ADD] script todo : installer les hooks git depuis le menu Git
Un commit-msg non installé, c'est le garde-fou de convention qui ne tourne jamais, et la pose restait une commande à recopier. git saute sans rien dire un hook privé du bit d'exécution : l'installation le remet. exec_command_live ne passe aucun cwd et ce dépôt porte 128 dépôts imbriqués : sans « git -C racine », un lancement depuis un addon y écrivait core.hooksPath et laissait la racine sans garde-fou. Et comme cette fonction retourne le code sans jamais lever, un « fatal: not in a git directory » annonçait « hooks installés ». Vérifié : 4114 tests verts ; le nouveau garde tombe sur un dispatch échangé comme sur une entrée sans branche. --- EN --- An uninstalled commit-msg is the convention guard never running, and installing it stayed a command to copy by hand. git skips a hook without the execution bit and says nothing: installing restores it. exec_command_live passes no cwd and this checkout holds 128 nested repositories: without the "git -C root", a launch from an addon wrote core.hooksPath there and left the root unguarded. And since that function returns the exit code without ever raising, a "fatal: not in a git directory" still announced "hooks installed". Checked: 4114 tests green; the new guard fails on a swapped dispatch as well as on an entry with no branch. Assisted-by: Claude Opus 5
This commit is contained in:
parent
dfd6cd1fb6
commit
0b8f25fb4b
3 changed files with 110 additions and 1 deletions
|
|
@ -2524,11 +2524,21 @@ class TODO(
|
|||
if cmd_no_found:
|
||||
print(t("Command not found !"))
|
||||
|
||||
# Les hooks que le dépôt fournit. git saute silencieusement un hook qui
|
||||
# ne porte pas le bit d'exécution, d'où la vérification à l'installation.
|
||||
_GIT_HOOKS = ("commit-msg", "pre-commit")
|
||||
_GIT_HOOKS_PATH = os.path.join("script", "git", "hooks")
|
||||
|
||||
def prompt_execute_git(self):
|
||||
print(f"🤖 {t('Git management tools!')}")
|
||||
choices = [
|
||||
{"prompt_description": t("Local git server")},
|
||||
{"prompt_description": t("Add a remote to a local repository")},
|
||||
{
|
||||
"prompt_description": t(
|
||||
"Install git hooks (commit-msg, pre-commit)"
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
# Append config-driven entries
|
||||
|
|
@ -2547,6 +2557,8 @@ class TODO(
|
|||
self.prompt_execute_git_local_server()
|
||||
elif status == "2":
|
||||
self._git_add_remote()
|
||||
elif status == "3":
|
||||
self._git_install_hooks()
|
||||
else:
|
||||
cmd_no_found = True
|
||||
try:
|
||||
|
|
@ -2579,6 +2591,51 @@ class TODO(
|
|||
except Exception as e:
|
||||
print(f"{t('Error adding remote: ')}{e}")
|
||||
|
||||
def _git_install_hooks(self):
|
||||
"""Pointer core.hooksPath sur les hooks du dépôt.
|
||||
|
||||
Le bit d'exécution fait partie de l'installation : sans lui git
|
||||
ignore le hook sans rien dire, et le garde-fou du message de commit
|
||||
passe inaperçu.
|
||||
"""
|
||||
racine = self._claude_context_root()
|
||||
absolu = os.path.join(racine, self._GIT_HOOKS_PATH)
|
||||
if not os.path.isdir(absolu):
|
||||
print(f"{t('Hooks directory is missing: ')}{absolu}")
|
||||
return
|
||||
actuel = self._git_hooks_path(racine)
|
||||
if actuel and actuel != self._GIT_HOOKS_PATH:
|
||||
print(f"{t('Another hooks path is already set: ')}{actuel}")
|
||||
if not self._is_yes(input(t("Replace it? (y/Y): "))):
|
||||
print(t("Nothing to do."))
|
||||
return
|
||||
for hook in self._GIT_HOOKS:
|
||||
chemin = os.path.join(absolu, hook)
|
||||
if os.path.isfile(chemin) and not os.access(chemin, os.X_OK):
|
||||
os.chmod(chemin, os.stat(chemin).st_mode | 0o111)
|
||||
print(f"{t('Execution bit added: ')}{hook}")
|
||||
# « -C racine » et non le cwd : lancé depuis un dépôt imbriqué
|
||||
# (odoo18.0/addons/…), git écrirait core.hooksPath là-bas et la
|
||||
# racine resterait sans garde-fou, sans le moindre message.
|
||||
cmd = (
|
||||
f"git -C {shlex.quote(racine)} config"
|
||||
f" core.hooksPath {self._GIT_HOOKS_PATH}"
|
||||
)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
# exec_command_live RETOURNE le code de sortie, il ne lève rien : sans
|
||||
# ce test, un « fatal: not in a git directory » annonçait quand même
|
||||
# « Hooks git installés! ». Le rapport qui suit ne rattrape pas, il
|
||||
# relit le bit d'exécution et non core.hooksPath.
|
||||
status = self.execute.exec_command_live(cmd, source_erplibre=False)
|
||||
if status:
|
||||
print(f"{t('Error installing hooks: ')}{status}")
|
||||
return
|
||||
print(t("Git hooks installed!"))
|
||||
for hook in self._GIT_HOOKS:
|
||||
pose = os.access(os.path.join(absolu, hook), os.X_OK)
|
||||
marque = t("hook installed") if pose else t("hook not installed")
|
||||
print(f" {hook:<26} {marque}")
|
||||
|
||||
def prompt_execute_git_local_server(self):
|
||||
print(f"🤖 {t('Manage local git repository server!')}")
|
||||
choices = [
|
||||
|
|
@ -2879,7 +2936,7 @@ class TODO(
|
|||
)
|
||||
if chemin_hooks:
|
||||
absolu = os.path.join(racine, chemin_hooks)
|
||||
for hook in ("commit-msg", "pre-commit"):
|
||||
for hook in self._GIT_HOOKS:
|
||||
pose = os.access(os.path.join(absolu, hook), os.X_OK)
|
||||
marque = (
|
||||
t("hook installed") if pose else t("hook not installed")
|
||||
|
|
|
|||
|
|
@ -883,6 +883,35 @@ TRANSLATIONS = {
|
|||
"fr": "Erreur lors de l'ajout du remote : ",
|
||||
"en": "Error adding remote: ",
|
||||
},
|
||||
# Git hooks
|
||||
"Install git hooks (commit-msg, pre-commit)": {
|
||||
"fr": "Installer les hooks git (commit-msg, pre-commit)",
|
||||
"en": "Install git hooks (commit-msg, pre-commit)",
|
||||
},
|
||||
"Hooks directory is missing: ": {
|
||||
"fr": "Le répertoire des hooks est absent : ",
|
||||
"en": "Hooks directory is missing: ",
|
||||
},
|
||||
"Another hooks path is already set: ": {
|
||||
"fr": "Un autre chemin de hooks est déjà configuré : ",
|
||||
"en": "Another hooks path is already set: ",
|
||||
},
|
||||
"Replace it? (y/Y): ": {
|
||||
"fr": "Le remplacer? (y/Y) : ",
|
||||
"en": "Replace it? (y/Y): ",
|
||||
},
|
||||
"Execution bit added: ": {
|
||||
"fr": "Bit d'exécution ajouté : ",
|
||||
"en": "Execution bit added: ",
|
||||
},
|
||||
"Git hooks installed!": {
|
||||
"fr": "Hooks git installés!",
|
||||
"en": "Git hooks installed!",
|
||||
},
|
||||
"Error installing hooks: ": {
|
||||
"fr": "Erreur lors de l'installation des hooks : ",
|
||||
"en": "Error installing hooks: ",
|
||||
},
|
||||
# Git config vim
|
||||
"Configure git local editor to vim": {
|
||||
"fr": "Configuration git local par vim",
|
||||
|
|
|
|||
|
|
@ -448,6 +448,29 @@ class TestProxmoxMenuNumbering(MenuCoherence, unittest.TestCase):
|
|||
}
|
||||
|
||||
|
||||
class TestGitMenuNumbering(MenuCoherence, unittest.TestCase):
|
||||
"""Le menu Git, le seul dont todo.json suit des entrées codées en dur.
|
||||
|
||||
Ses trois premières entrées sont écrites à la main, les suivantes viennent
|
||||
de `git_from_makefile` et le repli générique les renumérote tout seul :
|
||||
ajouter « Installer les hooks git » en [3] a poussé les deux entrées de
|
||||
todo.json en [4] et [5] sans que rien ne le dise. Une quatrième entrée
|
||||
codée en dur, oubliée dans le dispatch, ferait lancer à [4] la commande
|
||||
du voisin sous le libellé attendu.
|
||||
"""
|
||||
|
||||
SOURCE = TODO_DIR / "todo.py"
|
||||
ENTRY = "def prompt_execute_git(self):"
|
||||
END = "def _git_install_hooks(self):"
|
||||
MINIMUM = 2
|
||||
|
||||
EXPECTED = {
|
||||
"Local git server": "prompt_execute_git_local_server",
|
||||
"Add a remote to a local repository": "_git_add_remote",
|
||||
"Install git hooks": "_git_install_hooks",
|
||||
}
|
||||
|
||||
|
||||
class TestMenuLabels(unittest.TestCase):
|
||||
"""Toute méthode de menu doit avoir son étiquette de fil d'Ariane.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue