[UPD] commit rule: English subject first, French title under --- FR ---
git log --oneline shows only the subject, and it read in whichever language the author was thinking in. The subject and the body under it are now in English, then --- FR --- opens the French section, which starts with the subject translated under the same tag. The hook refuses a --- EN --- marker and a French section without that title, and checks the title like a subject; it does not count against the body budget. Whether the subject is really English is not checked. /commit and /git_prepare_merge follow. Checked: 55 hook tests, 7 of them new; i18n tests pass. --- FR --- [UPD] règle de commit : sujet anglais d'abord, titre FR sous --- FR --- git log --oneline ne montre que le sujet, et il se lisait dans la langue où l'auteur pensait. Le sujet et le corps qui le suit sont désormais en anglais, puis --- FR --- ouvre la section française, qui commence par le sujet traduit sous le même tag. Le hook refuse un marqueur --- EN --- et une section française sans ce titre, et juge ce titre comme un sujet ; il ne compte pas dans le budget du corps. Que le sujet soit vraiment en anglais ne se vérifie pas. /commit et /git_prepare_merge suivent. Vérifié : 55 tests du hook, dont 7 nouveaux ; tests i18n au vert. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
aa3f99e746
commit
76c548a781
6 changed files with 186 additions and 40 deletions
|
|
@ -90,9 +90,9 @@ liste, et elle est vraie demain.
|
|||
- Branches : `develop` (développement), `master` (production)
|
||||
- Pas de submodules Git — utilise **Google Repo** pour les addons
|
||||
- Manifests XML dans `manifest/` pour chaque version Odoo
|
||||
- Format de commit : `[TYPE] portée : sujet`, sujet à l'impératif, 72
|
||||
caractères au plus. Tags réellement utilisés : `[UPD]`, `[FIX]`, `[ADD]`,
|
||||
`[IMP]`, `[REF]`.
|
||||
- Format de commit : `[TYPE] scope: subject`, sujet **en anglais**, à
|
||||
l'impératif, 72 caractères au plus. Tags réellement utilisés : `[UPD]`,
|
||||
`[FIX]`, `[ADD]`, `[IMP]`, `[REF]`.
|
||||
- **Nommer les fichiers à l'indexation, jamais `git add -A`** : `private/` et
|
||||
`tasks/` ne sont pas suivis EXPRÈS, et un ratissage les commit. Il emporte
|
||||
aussi ce qui est en cours ailleurs dans le checkout, sous un sujet qui ne le
|
||||
|
|
@ -117,14 +117,17 @@ Le sujet résume le commit ENTIER, pas sa plus grosse pièce. S'il lui faut un
|
|||
|
||||
Si le travail n'entre décidément pas dans une phrase de 72 caractères, ne pas
|
||||
en écrire une amputée : des **mots-clés qui résument**, séparés par des
|
||||
virgules, en disent plus dans la même place — `[FIX] proxmox : pmxcfs à terre,
|
||||
pvesm muet, diagnostic à la source`. C'est un repli, pas un défaut : la phrase
|
||||
reste préférable quand elle tient.
|
||||
virgules, en disent plus dans la même place — `[FIX] proxmox: pmxcfs down,
|
||||
pvesm silent, diagnosis at the source`. C'est un repli, pas un défaut : la
|
||||
phrase reste préférable quand elle tient.
|
||||
|
||||
Un garde-fou refuse le mécanique. Sur le sujet : tag absent, plus de 72
|
||||
caractères, ouverture sur une citation. Sur le corps : plus de 10 lignes pour
|
||||
une langue, une adresse IP, un courriel, un chemin de compte. Ce qui reste un
|
||||
jugement — « ce corps raconte-t-il l'enquête » — n'est vérifié par personne.
|
||||
une langue, une adresse IP, un courriel, un chemin de compte. Sur l'ordre des
|
||||
langues : un marqueur `--- EN ---`, une section française qui ne s'ouvre pas
|
||||
sur le sujet traduit sous le même tag. Que le sujet soit bien en anglais
|
||||
reste à l'auteur. Ce qui reste un jugement — « ce corps raconte-t-il
|
||||
l'enquête » — n'est vérifié par personne.
|
||||
|
||||
```bash
|
||||
git config core.hooksPath script/git/hooks # une fois par clone
|
||||
|
|
@ -141,8 +144,9 @@ Trois exigences, sans exception — `AI_POLICY.md` en donne la raison :
|
|||
- Trailer `Assisted-by: <modèle>`, une ligne par modèle. C'est **binaire** :
|
||||
il y a eu IA ou non, aucun seuil à apprécier.
|
||||
- **Jamais** d'IA dans `Co-authored-by:` — ce champ est réservé aux humains.
|
||||
- Corps **bilingue** : le corps, puis `--- FR ---` (ou `--- EN ---`, le
|
||||
marqueur nomme la langue de ce qui SUIT), puis la traduction.
|
||||
- Message **bilingue, l'anglais d'abord** : le sujet et le corps en anglais,
|
||||
puis `--- FR ---`, puis le sujet traduit en français sous le même tag, puis
|
||||
le corps traduit. `--- EN ---` n'existe plus : l'ordre ne varie jamais.
|
||||
|
||||
Court et direct : **8 lignes par langue**, 10 est un plafond. Le corps dit
|
||||
pourquoi c'était nécessaire, puis s'arrête. Rien de ce que le diff montre
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ The first five cover every one of the last 400 commits. Reach for `[REM]`,
|
|||
### Format
|
||||
|
||||
```
|
||||
[TAG] scope: short description in imperative mood
|
||||
[TAG] scope: short description in imperative mood, in English
|
||||
|
||||
Explain WHY the change was made — the diff already shows what. Name the
|
||||
failure mode, and what was measured rather than assumed, in words a
|
||||
|
|
@ -87,11 +87,18 @@ Wrap at 80 characters.
|
|||
|
||||
--- FR ---
|
||||
|
||||
[TAG] portée : le sujet, traduit en français
|
||||
|
||||
The same body, translated.
|
||||
|
||||
Assisted-by: {MODEL}
|
||||
```
|
||||
|
||||
The subject line is in ENGLISH, and the English body follows it: together
|
||||
they are the English section. `--- FR ---` then opens the French section,
|
||||
which starts with the subject translated, under the same tag, and goes on
|
||||
with the translated body.
|
||||
|
||||
### The subject line
|
||||
|
||||
The subject is read a hundred times for every time the body is read: in
|
||||
|
|
@ -108,9 +115,9 @@ built on them reads well and tells the next reader nothing:
|
|||
|
||||
| Instead of | Write |
|
||||
|-----------|-------|
|
||||
| `[FIX] nettoyage : les enfants s'en vont avec leur rebond` | `[FIX] nettoyage : les entrées ssh qui rebondissent par une VM effacée` |
|
||||
| `[FIX] proxmox : « il manque le stockage » était le symptôme, pas la cause` | `[FIX] proxmox : signaler pmxcfs à terre, et non « aucun stockage »` |
|
||||
| `[FIX] migration: un module fautif n'emporte plus tout le lot` | `[FIX] migration: isoler l'échec d'un module dans la désinstallation` |
|
||||
| `[FIX] cleanup: the children leave with their bounce` | `[FIX] cleanup: ssh entries that bounce through a deleted VM` |
|
||||
| `[FIX] proxmox: "storage is missing" was the symptom, not the cause` | `[FIX] proxmox: report pmxcfs down, not "no storage"` |
|
||||
| `[FIX] migration: a faulty module no longer takes the whole batch` | `[FIX] migration: isolate a module's failure during uninstall` |
|
||||
|
||||
The scope is not the subject. `proxmox` says WHERE; the words after the colon
|
||||
must say WHAT. A subject that works with its scope removed is usually the
|
||||
|
|
@ -130,8 +137,8 @@ one — write **keywords that summarise**. A comma-separated list of the nouns
|
|||
that matter says more in the space than half a sentence does:
|
||||
|
||||
```
|
||||
[FIX] proxmox : pmxcfs à terre, pvesm muet, diagnostic à la source
|
||||
[ADD] migration : copies de site, index doublés, réglages perdus
|
||||
[FIX] proxmox: pmxcfs down, pvesm silent, diagnosis at the source
|
||||
[ADD] migration: site copies, doubled indexes, lost settings
|
||||
```
|
||||
|
||||
That form is a fallback, not a default. Prefer the sentence when it fits.
|
||||
|
|
@ -139,7 +146,10 @@ That form is a fallback, not a default. Prefer the sentence when it fits.
|
|||
**The guard rail.** `script/git/hooks/commit-msg` refuses a subject with no
|
||||
tag, one over 72 characters, and one opening on a quotation. It reads the body
|
||||
too: over ten lines for one language, an IP address, an e-mail, and a
|
||||
`/home/<account>/` path. Install the hook with `git config core.hooksPath script/git/hooks`; `git commit
|
||||
`/home/<account>/` path. And the order of the languages: a `--- EN ---`
|
||||
marker, or a French section that does not open on the translated subject
|
||||
under the same tag. Whether the subject is really in English stays yours to
|
||||
check. Install the hook with `git config core.hooksPath script/git/hooks`; `git commit
|
||||
--no-verify` passes a legitimate exception. It checks only what is mechanical
|
||||
— whether the subject says what the code is about, and whether the body tells
|
||||
the story instead of the mechanism, stay judgements, and the tests above are
|
||||
|
|
@ -187,12 +197,15 @@ goes.
|
|||
|
||||
### Bilingual body
|
||||
|
||||
Every AI-assisted commit carries its body twice. Write it first in whichever
|
||||
language you were thinking in, then the marker, then the translation.
|
||||
Every AI-assisted commit carries its subject and its body twice, English
|
||||
first: the subject and the body under it are in English, then `--- FR ---`,
|
||||
then the subject translated into French under the same tag, then the body
|
||||
translated. The order never varies — `--- EN ---` is refused — so a reader of
|
||||
`git log --oneline` always gets English, and a French reader finds the French
|
||||
title where the French section starts.
|
||||
|
||||
The marker names the language of what FOLLOWS it: `--- FR ---` after an
|
||||
English body, `--- EN ---` after a French one. One marker per commit, never
|
||||
both.
|
||||
The translated subject obeys the same rules as the subject: 72 characters,
|
||||
no opening quotation. It does not count against the body's line budget.
|
||||
|
||||
Translate, do not re-summarise: a reader of either language must get the same
|
||||
reasoning, the same measured figures and the same caveats.
|
||||
|
|
@ -259,12 +272,14 @@ the subject you just wrote, and no others. When one file carries two subjects,
|
|||
git status --porcelain
|
||||
git add script/module/thing.py test/test_thing.py
|
||||
git -c user.name="Your Name" -c user.email="your@email.com" commit -F - <<'MSG'
|
||||
[TAG] scope: description
|
||||
[TAG] scope: description in English
|
||||
|
||||
Explain WHY here.
|
||||
|
||||
--- FR ---
|
||||
|
||||
[TAG] portée : la description en français
|
||||
|
||||
The same body, translated.
|
||||
|
||||
Assisted-by: {MODEL}
|
||||
|
|
|
|||
|
|
@ -117,12 +117,14 @@ The shape, as this repository writes it:
|
|||
```
|
||||
Merge branch '<branch>'
|
||||
|
||||
[TAG] scope: what the branch delivers, imperative, 72 characters maximum
|
||||
[TAG] scope: what the branch delivers, in English, 72 characters maximum
|
||||
|
||||
<N> commits. Why the branch existed: the failure mode it removes, the
|
||||
figure that bounds it, what was verified and how. Wrap at 80 characters.
|
||||
|
||||
--- EN ---
|
||||
--- FR ---
|
||||
|
||||
[TAG] portée : ce que livre la branche, traduit en français
|
||||
|
||||
The same body, translated.
|
||||
|
||||
|
|
@ -143,14 +145,15 @@ commits the branch carries, then say what they add up to. No bullet list, no
|
|||
per-commit rundown: `git log <base>..HEAD` already gives that, and a body
|
||||
repeating it teaches nothing.
|
||||
|
||||
The bilingual rule holds — body, then the marker naming the language of what
|
||||
FOLLOWS, then the translation — as does the ban on naming an AI in
|
||||
The bilingual rule holds as for a commit — the tagged line and the body in
|
||||
English, then `--- FR ---`, then the tagged line translated under the same
|
||||
tag, then the translated body — as does the ban on naming an AI in
|
||||
`Co-authored-by:`.
|
||||
|
||||
**The hook does not check this one.** `script/git/hooks/commit-msg` skips any
|
||||
message beginning with `Merge `, along with `Revert `, `fixup!` and `squash!`.
|
||||
Length, addresses and account paths pass unchallenged here, so the discipline
|
||||
is entirely yours.
|
||||
Length, addresses, account paths and the order of the languages pass
|
||||
unchallenged here, so the discipline is entirely yours.
|
||||
|
||||
### 3. Hand it over
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ d'emploi dans `conf/template_claude_commands_commit.md`. Ce module en vérifie
|
|||
la part MÉCANIQUE, sur deux plans :
|
||||
|
||||
- le sujet : le tag, la longueur, l'ouverture sur une citation ;
|
||||
- le corps : sa longueur par langue, et la donnée identifiante.
|
||||
- le corps : sa longueur par langue, et la donnée identifiante ;
|
||||
- l'ordre des langues : le sujet et le corps qui le suit sont en anglais,
|
||||
« --- FR --- » ouvre la traduction, et celle-ci commence par le sujet
|
||||
traduit, sous le même tag.
|
||||
|
||||
Que le sujet soit VRAIMENT en anglais ne se vérifie pas : seul l'ordre des
|
||||
moitiés et la présence du titre traduit le sont.
|
||||
|
||||
Le reste — « ce sujet dit-il sur quoi porte le code », « ce corps raconte-t-il
|
||||
l'enquête plutôt que le fonctionnement » — est un jugement, et aucun hook ne
|
||||
|
|
@ -64,6 +70,8 @@ GENERATED = ("Merge ", "Revert ", "fixup!", "squash!", "amend!")
|
|||
QUOTES = ("«", '"', "'", "`", "“", "‘")
|
||||
|
||||
# Le marqueur nomme la langue de ce qui SUIT : il sépare les deux moitiés.
|
||||
# Seul « --- FR --- » est admis, l'anglais venant toujours en premier ; « EN »
|
||||
# est reconnu pour être refusé avec un message, et non ignoré.
|
||||
MARKER = re.compile(r"^---\s*(FR|EN)\s*---\s*$", re.MULTILINE)
|
||||
|
||||
# `git commit --cleanup=scissors` laisse le diff en clair sous cette ligne :
|
||||
|
|
@ -133,14 +141,26 @@ def _moities(body: str) -> list:
|
|||
return [part for part in MARKER.split(body) if part not in ("FR", "EN")]
|
||||
|
||||
|
||||
def _tag_of(subject: str):
|
||||
"""Le tag qui ouvre le sujet, sans crochets ; None s'il n'y en a pas."""
|
||||
for candidate in TAGS:
|
||||
if subject.startswith(f"[{candidate}]"):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _titre_traduit(moitie: str) -> str:
|
||||
"""La première ligne non vide d'une moitié traduite : son titre."""
|
||||
for ligne in moitie.split("\n"):
|
||||
if ligne.strip():
|
||||
return ligne.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _check_subject(subject: str) -> list:
|
||||
problems = []
|
||||
|
||||
tag = None
|
||||
for candidate in TAGS:
|
||||
if subject.startswith(f"[{candidate}]"):
|
||||
tag = candidate
|
||||
break
|
||||
tag = _tag_of(subject)
|
||||
if tag is None:
|
||||
problems.append(
|
||||
t("the subject must start with a tag: %s")
|
||||
|
|
@ -179,8 +199,12 @@ def _check_body(sans_trailers: str, avec_trailers: str) -> list:
|
|||
"""
|
||||
problems = []
|
||||
|
||||
for moitie in _moities(sans_trailers):
|
||||
for rang, moitie in enumerate(_moities(sans_trailers)):
|
||||
pleines = [ligne for ligne in moitie.split("\n") if ligne.strip()]
|
||||
# Le titre traduit est le sujet de sa langue : il ne pèse pas sur le
|
||||
# budget du corps, pas plus que le sujet anglais.
|
||||
if rang and pleines and _tag_of(pleines[0].strip()):
|
||||
pleines = pleines[1:]
|
||||
if len(pleines) > MAX_BODY:
|
||||
problems.append(
|
||||
t(
|
||||
|
|
@ -243,11 +267,45 @@ def _check_body(sans_trailers: str, avec_trailers: str) -> list:
|
|||
return problems
|
||||
|
||||
|
||||
def _check_langues(subject: str, sans_trailers: str) -> list:
|
||||
"""L'anglais d'abord, puis « --- FR --- » et le sujet traduit.
|
||||
|
||||
Un message sans marqueur n'est pas bilingue et n'est pas jugé ici.
|
||||
"""
|
||||
marqueurs = MARKER.findall(sans_trailers)
|
||||
if not marqueurs:
|
||||
return []
|
||||
if "EN" in marqueurs:
|
||||
return [
|
||||
t(
|
||||
"the marker is « --- EN --- ». The subject and the body under it\n"
|
||||
" are in English; « --- FR --- » opens the French translation."
|
||||
)
|
||||
]
|
||||
tag = _tag_of(subject)
|
||||
titre = _titre_traduit(_moities(sans_trailers)[-1])
|
||||
if tag is None or not titre.startswith(f"[{tag}]"):
|
||||
return [
|
||||
t(
|
||||
"the French section must open on the translated subject,\n"
|
||||
" under the same tag: %s"
|
||||
)
|
||||
% ("[%s] …" % (tag or "TAG"))
|
||||
]
|
||||
return [
|
||||
t("French subject: %s") % probleme
|
||||
for probleme in _check_subject(titre)
|
||||
]
|
||||
|
||||
|
||||
def check(message: str) -> list:
|
||||
"""Rend la liste des problèmes. Vide si le message passe."""
|
||||
subject = subject_of(message)
|
||||
if not subject or subject.startswith(GENERATED):
|
||||
return []
|
||||
return _check_subject(subject) + _check_body(
|
||||
body_of(message), body_of(message, trailers=True)
|
||||
sans_trailers = body_of(message)
|
||||
return (
|
||||
_check_subject(subject)
|
||||
+ _check_langues(subject, sans_trailers)
|
||||
+ _check_body(sans_trailers, body_of(message, trailers=True))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9446,6 +9446,24 @@ TRANSLATIONS = {
|
|||
"en": "the subject opens on a quotation. A screen message is\n"
|
||||
" evidence: it belongs in the body. The subject names the cause.",
|
||||
},
|
||||
"the marker is « --- EN --- ». The subject and the body under it\n"
|
||||
" are in English; « --- FR --- » opens the French translation.": {
|
||||
"fr": "le marqueur est « --- EN --- ». Le sujet et le corps qui le suit\n"
|
||||
" sont en anglais ; « --- FR --- » ouvre la traduction française.",
|
||||
"en": "the marker is « --- EN --- ». The subject and the body under it\n"
|
||||
" are in English; « --- FR --- » opens the French translation.",
|
||||
},
|
||||
"the French section must open on the translated subject,\n"
|
||||
" under the same tag: %s": {
|
||||
"fr": "la section française doit s'ouvrir sur le sujet traduit,\n"
|
||||
" sous le même tag : %s",
|
||||
"en": "the French section must open on the translated subject,\n"
|
||||
" under the same tag: %s",
|
||||
},
|
||||
"French subject: %s": {
|
||||
"fr": "sujet français : %s",
|
||||
"en": "French subject: %s",
|
||||
},
|
||||
"the body is %s lines for one language, %s at most.\n"
|
||||
" The body says why it was necessary, then stops.\n"
|
||||
" The investigation, the dated measurements and the dead ends go\n"
|
||||
|
|
|
|||
|
|
@ -151,7 +151,55 @@ class TestCeQuiEstRefuse(unittest.TestCase):
|
|||
|
||||
|
||||
def _message(corps):
|
||||
return "[FIX] portée : quelque chose\n\n" + corps + "\n"
|
||||
return "[FIX] scope: something\n\n" + corps + "\n"
|
||||
|
||||
|
||||
# Le sujet de _message, traduit : ce qui ouvre la section française.
|
||||
TITRE_FR = "[FIX] portée : quelque chose"
|
||||
|
||||
|
||||
class TestLesLangues(unittest.TestCase):
|
||||
"""L'anglais d'abord, puis « --- FR --- » et le sujet traduit."""
|
||||
|
||||
def test_anglais_puis_francais_avec_titre_traduit(self):
|
||||
corps = f"Why, in English.\n\n--- FR ---\n\n{TITRE_FR}\n\nPourquoi."
|
||||
self.assertEqual([], check(_message(corps)))
|
||||
|
||||
def test_un_message_sans_marqueur_nest_pas_juge(self):
|
||||
self.assertEqual([], check(_message("Why, in one language.")))
|
||||
|
||||
@EN_FRANCAIS
|
||||
def test_le_marqueur_en_est_refuse(self):
|
||||
corps = "Pourquoi, en français.\n\n--- EN ---\n\nWhy, in English."
|
||||
problemes = check(_message(corps))
|
||||
self.assertEqual(1, len(problemes))
|
||||
self.assertIn("--- EN ---", problemes[0])
|
||||
|
||||
@EN_FRANCAIS
|
||||
def test_la_section_francaise_sans_titre(self):
|
||||
problemes = check(_message("Why.\n\n--- FR ---\n\nPourquoi."))
|
||||
self.assertEqual(1, len(problemes))
|
||||
self.assertIn("sujet traduit", problemes[0])
|
||||
self.assertIn("[FIX] …", problemes[0])
|
||||
|
||||
def test_le_titre_traduit_porte_le_meme_tag(self):
|
||||
corps = (
|
||||
"Why.\n\n--- FR ---\n\n[ADD] portée : autre chose\n\nPourquoi."
|
||||
)
|
||||
self.assertEqual(1, len(check(_message(corps))))
|
||||
|
||||
@EN_FRANCAIS
|
||||
def test_le_titre_traduit_suit_les_regles_du_sujet(self):
|
||||
titre = "[FIX] portée : " + "é" * MAX
|
||||
corps = f"Why.\n\n--- FR ---\n\n{titre}\n\nPourquoi."
|
||||
problemes = check(_message(corps))
|
||||
self.assertEqual(1, len(problemes))
|
||||
self.assertTrue(problemes[0].startswith("sujet français :"), problemes[0])
|
||||
|
||||
def test_le_titre_traduit_ne_compte_pas_dans_le_corps(self):
|
||||
moitie = "\n".join(f"ligne {n}" for n in range(MAX_BODY))
|
||||
corps = f"{moitie}\n\n--- FR ---\n\n{TITRE_FR}\n{moitie}"
|
||||
self.assertEqual([], check(_message(corps)))
|
||||
|
||||
|
||||
class TestLeCorps(unittest.TestCase):
|
||||
|
|
@ -166,7 +214,7 @@ class TestLeCorps(unittest.TestCase):
|
|||
|
||||
def test_dix_lignes_par_langue_passent(self):
|
||||
moitie = "\n".join(f"ligne {n}" for n in range(MAX_BODY))
|
||||
corps = f"{moitie}\n\n--- FR ---\n\n{moitie}"
|
||||
corps = f"{moitie}\n\n--- FR ---\n\n{TITRE_FR}\n\n{moitie}"
|
||||
self.assertEqual([], check(_message(corps)))
|
||||
|
||||
def test_onze_lignes_pour_une_langue_sont_refusees(self):
|
||||
|
|
|
|||
Loading…
Reference in a new issue