[ADD] script todo : déployer /git_prepare_merge depuis le menu Claude

Une fusion apporte plusieurs commits d'un coup, et rien ne guidait les
deux écrits qu'elle demande : l'entrée de changelog et le message de
merge. La commande déployée impose la source — CHANGELOG.base.md, les
fichiers générés étant perdus au prochain doc_markdown — et le corps
bilingue avec son trailer, que le garde-fou ne verra jamais, commit-msg
ignorant tout message ouvrant sur « Merge ». Elle prépare, elle ne
fusionne pas : le message part dans tasks/, non versionné.

Vérifié : 45 tests du menu, et un déploiement dans un HOME jetable que
l'écran de contexte relit « à jour » face à son gabarit.

--- EN ---

A merge lands several commits at once, and nothing guided the two pieces
of writing it needs: the changelog entry and the merge message. The
deployed command imposes the source — CHANGELOG.base.md, the generated
files being lost at the next doc_markdown — and the bilingual body with
its trailer, which the guard rail never sees, commit-msg skipping any
message opening on « Merge ». It prepares, it does not merge: the
message goes to tasks/, which is not versioned.

Checked: 45 menu tests, and a deployment into a throwaway HOME that the
context screen reads back as up to date against its template.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-09-02 06:15:43 -04:00
parent 8bdbf42a31
commit c32e7d87c9
3 changed files with 188 additions and 1 deletions

View file

@ -0,0 +1,170 @@
---
name: git_prepare_merge
description: "ERPLibre merge preparation: changelog entry, then the merge message for the current branch."
disable-model-invocation: true
allowed-tools:
- Bash(git status:*)
- Bash(git branch:*)
- Bash(git log:*)
- Bash(git diff:*)
- Bash(git merge-base:*)
- Bash(sed:*)
- Bash(make doc_markdown:*)
- Bash(python3:*)
- Read
- Edit
- Write
---
## Context
- Current branch: !`git branch --show-current`
- Branch commits: !`git log --oneline $(git merge-base HEAD master)..HEAD`
- Files touched: !`git diff --stat $(git merge-base HEAD master)..HEAD`
- Working tree: !`git status --porcelain`
- Changelog head: !`sed -n '24,45p' CHANGELOG.base.md`
## Task
Prepare the merge of the CURRENT branch into its integration branch. Two
deliverables, in this order: the changelog entry, then the merge message.
Nothing is merged here — `/git_prepare_merge` prepares, the human merges.
### 0. Read the branch
`master` is production, `develop` is where the work lands. Take the target
from where the branch forked: `git merge-base HEAD develop` and
`git merge-base HEAD master`, the closer of the two names the target.
Read the WHOLE branch before writing a word — `git log -p <base>..HEAD` for
the commits, `git diff <base>..HEAD` for the net result. A merge message
summarises what the branch delivers, which is rarely the concatenation of its
subjects: commits that undo each other cancel, and a fix to a feature added on
the same branch is part of the feature, not a separate line.
Stop and say so, rather than inventing, when the branch is empty, when it is
already merged, or when the working tree carries changes not yet committed —
uncommitted work is not part of the merge and must not be described as if it
were.
### 1. The changelog entry
`CHANGELOG.base.md` at the repository root is the SOURCE. `CHANGELOG.md` and
`CHANGELOG.fr.md` are generated by mmg and every direct edit to them is lost
at the next `make doc_markdown` — never open them to write.
The entry goes under `## [Unreleased]`, in the section that fits: Added /
Ajouté, Changed / Modifié, Fixed / Corrigé, Removed / Retiré, Security /
Sécurité. Create the pair of headings if the section does not exist yet, in
the file's own order.
The file alternates language blocks with markers. Within one section the
English bullets sit under `<!-- [en] -->` and the French translation under
`<!-- [fr] -->`, in the SAME order: the two lists are read side by side, and a
bullet added to one language only leaves the other half wrong. Nothing goes
under `<!-- [common] -->` but the version headings.
What a bullet says: what the software now DOES or REFUSES, in the present, for
someone who was not on this branch. It is longer than a commit subject and
shorter than the commit body — the reader is choosing whether to upgrade, not
reviewing the diff. Keep the failure mode removed, the figure that bounds it,
the flag or the file a user has to know. Drop the internals nobody outside
calls.
The two rules of `.claude/rules/04-code-conventions.md` hold here as
everywhere: nothing identifying — no customer, no real database, no host, no
address, no account path — and the code as the subject, never the session
that produced it.
Regenerate afterwards, and stage the three files together, the generated pair
being what most readers actually open:
```bash
make doc_markdown
git status --porcelain CHANGELOG.base.md CHANGELOG.md CHANGELOG.fr.md
```
### 2. The merge message
Resolve `{MODEL}` exactly as `/commit` does — the trailer is required here
too, a merge message being as AI-assisted as any other. Run:
```bash
python3 -c "
import glob, json, os, sys
sid = os.environ.get('CLAUDE_CODE_SESSION_ID', '')
hits = glob.glob(os.path.expanduser('~/.claude/projects/*/%s.jsonl' % sid)) if sid else []
mid = ''
for path in hits[:1]:
with open(path) as fh:
for line in fh:
try:
m = json.loads(line).get('message', {}).get('model', '')
except Exception:
continue
if m and not m.startswith('<'):
mid = m
if not mid:
sys.exit('UNKNOWN')
mid = mid.removeprefix('claude-')
parts = [p for p in mid.split('-') if not (len(p) == 8 and p.isdigit())]
print('Claude %s %s' % (parts[0].capitalize(), '.'.join(parts[1:])))
"
```
The shape, as this repository writes it:
```
Merge branch '<branch>'
[TAG] scope: what the branch delivers, imperative, 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 ---
The same body, translated.
Assisted-by: {MODEL}
```
The first line stays `Merge branch '<branch>'` — git writes it, tools read it,
and a merge whose first line says something else no longer looks like a merge
in `git log --oneline`. The tagged line beneath it is what a reader gets from
`--oneline` on the second row and from a release note, so it carries the same
duty as a commit subject: name the part of the system, then what is now
different about it. The evidence — the symptom, the quoted error, the
metaphor — belongs in the body.
The body: the same budget as a commit, per language, and here a merge covers
several commits, so it is a SUMMARY and not a list. Open by stating how many
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
`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.
### 3. Hand it over
Write the message to `tasks/merge_message.txt` — `tasks/` is not versioned,
which is why the repository sends working material there — and print the two
commands the human runs, with `--no-ff` so the branch keeps a merge commit and
its history stays readable:
```bash
git switch <target>
git merge --no-ff <branch> -F tasks/merge_message.txt
```
Do not run them. Do not switch branch, do not merge, do not push: the merge is
the human's decision and the last chance to read the message before it is
permanent. Report, in a sentence each, the changelog section written to and
the number of commits summarised.

View file

@ -3050,6 +3050,11 @@ class TODO(
print(f"🤖 {t('Deploy Claude Code commands!')}") print(f"🤖 {t('Deploy Claude Code commands!')}")
choices = [ choices = [
{"prompt_description": t("Commit - OCA/Odoo commit command")}, {"prompt_description": t("Commit - OCA/Odoo commit command")},
{
"prompt_description": t(
"Git prepare merge - Git merge preparation command"
)
},
{ {
"prompt_description": t( "prompt_description": t(
"Todo Add Command - Add a command to todo.py menu" "Todo Add Command - Add a command to todo.py menu"
@ -3071,11 +3076,16 @@ class TODO(
personalize=True, personalize=True,
) )
elif status == "2": elif status == "2":
self._setup_claude_command(
"git_prepare_merge",
"template_claude_commands_git_prepare_merge.md",
)
elif status == "3":
self._setup_claude_command( self._setup_claude_command(
"todo_add_command", "todo_add_command",
"template_claude_commands_todo_add_command.md", "template_claude_commands_todo_add_command.md",
) )
elif status == "3": elif status == "4":
self._list_claude_commands() self._list_claude_commands()
else: else:
print(t("Command not found !")) print(t("Command not found !"))
@ -3203,6 +3213,9 @@ class TODO(
print(f"{t('Deployed commands'):<22} ~/.claude/commands/") print(f"{t('Deployed commands'):<22} ~/.claude/commands/")
gabarits = { gabarits = {
"commit": "template_claude_commands_commit.md", "commit": "template_claude_commands_commit.md",
"git_prepare_merge": (
"template_claude_commands_git_prepare_merge.md"
),
"todo_add_command": "template_claude_commands_todo_add_command.md", "todo_add_command": "template_claude_commands_todo_add_command.md",
} }
for nom, gabarit in sorted(gabarits.items()): for nom, gabarit in sorted(gabarits.items()):

View file

@ -1176,6 +1176,10 @@ TRANSLATIONS = {
"fr": "Commit - Commande de commit OCA/Odoo", "fr": "Commit - Commande de commit OCA/Odoo",
"en": "Commit - OCA/Odoo commit command", "en": "Commit - OCA/Odoo commit command",
}, },
"Git prepare merge - Git merge preparation command": {
"fr": "Git prepare merge - Commande de préparation merge git",
"en": "Git prepare merge - Git merge preparation command",
},
"Todo Add Command - Add a command to todo.py menu": { "Todo Add Command - Add a command to todo.py menu": {
"fr": "Todo Add Command - Ajouter une commande au menu todo.py", "fr": "Todo Add Command - Ajouter une commande au menu todo.py",
"en": "Todo Add Command - Add a command to todo.py menu", "en": "Todo Add Command - Add a command to todo.py menu",