diff --git a/conf/template_claude_commands_git_prepare_merge.md b/conf/template_claude_commands_git_prepare_merge.md new file mode 100644 index 0000000..be801a8 --- /dev/null +++ b/conf/template_claude_commands_git_prepare_merge.md @@ -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 ..HEAD` for +the commits, `git diff ..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 `` and the French translation under +``, 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 `` 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 '' + +[TAG] scope: what the branch delivers, imperative, 72 characters maximum + + 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 ''` — 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 ..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 +git merge --no-ff -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. diff --git a/script/todo/todo.py b/script/todo/todo.py index 49ff113..a924289 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -3050,6 +3050,11 @@ class TODO( print(f"🤖 {t('Deploy Claude Code commands!')}") choices = [ {"prompt_description": t("Commit - OCA/Odoo commit command")}, + { + "prompt_description": t( + "Git prepare merge - Git merge preparation command" + ) + }, { "prompt_description": t( "Todo Add Command - Add a command to todo.py menu" @@ -3071,11 +3076,16 @@ class TODO( personalize=True, ) elif status == "2": + self._setup_claude_command( + "git_prepare_merge", + "template_claude_commands_git_prepare_merge.md", + ) + elif status == "3": self._setup_claude_command( "todo_add_command", "template_claude_commands_todo_add_command.md", ) - elif status == "3": + elif status == "4": self._list_claude_commands() else: print(t("Command not found !")) @@ -3203,6 +3213,9 @@ class TODO( print(f"{t('Deployed commands'):<22} ~/.claude/commands/") gabarits = { "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", } for nom, gabarit in sorted(gabarits.items()): diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index ec5e701..318e53e 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1176,6 +1176,10 @@ TRANSLATIONS = { "fr": "Commit - Commande de commit OCA/Odoo", "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": { "fr": "Todo Add Command - Ajouter une commande au menu todo.py", "en": "Todo Add Command - Add a command to todo.py menu",