[PATCH b4] prep: keep auto-to-cc trailers before cover changelogs
Akihiko Odaki <[email protected]> Sun, 28 Jun 2026 15:03:21 +0900
| Newsgroups | org.kernel.linux.tools |
|---|---|
| Message-ID | <[email protected]> |
After get_body_parts() stopped splitting free-form messages on every "---" line, prep --auto-to-cc no longer saw existing cover trailers when they were followed by the generated changelog block. That made it append duplicate To:/Cc: trailers below the changelog. Teach auto-to-cc to collect existing cover recipients with the same broad trailer scan used by the send path. Then let fix_trailers() treat the cover's "---" line as a separator even when the cover has no diff, so new To:/Cc: trailers are inserted before the changelog instead of being appended below it. Add a regression test for a rerolled cover whose existing To: trailer is above "Changes in v2:". Closes: https://github.com/mricon/b4/issues/78 Signed-off-by: Akihiko Odaki <[email protected]> Assisted-by: Codex gpt-5.5 --- src/b4/__init__.py | 6 ++++-- src/b4/ez.py | 7 ++++--- src/tests/test_ez.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/b4/__init__.py b/src/b4/__init__.py index d7b41ee..5ad1c39 100644 --- a/src/b4/__init__.py +++ b/src/b4/__init__.py @@ -2896,6 +2896,7 @@ class LoreMessage: @staticmethod def get_body_parts( body: str, + force_patch_separator: bool = False, ) -> Tuple[List[LoreTrailer], str, List[LoreTrailer], str, str]: # remove any starting/trailing blank lines body = body.replace('\r', '') @@ -2917,7 +2918,7 @@ class LoreMessage: # diff content. Free-form replies should not be split on '---', which # may appear as a visual separator unrelated to any patch. parts = [body] - if DIFF_RE.search(body) or DIFFSTAT_RE.search(body): + if force_patch_separator or DIFF_RE.search(body) or DIFFSTAT_RE.search(body): parts = re.split(r'^---\s*\n', body, maxsplit=1, flags=re.M) if len(parts) == 2: basement = parts[1] @@ -2972,6 +2973,7 @@ class LoreMessage: def fix_trailers( self, + force_patch_separator: bool = False, extras: Optional[List[LoreTrailer]] = None, copyccs: bool = False, addmysob: bool = False, @@ -2982,7 +2984,7 @@ class LoreMessage: config = get_main_config() bheaders, message, btrailers, basement, signature = LoreMessage.get_body_parts( - self.body + self.body, force_patch_separator=force_patch_separator ) sobtr = LoreTrailer() diff --git a/src/b4/ez.py b/src/b4/ez.py index 268d783..3f43176 100644 --- a/src/b4/ez.py +++ b/src/b4/ez.py @@ -3865,9 +3865,8 @@ def auto_to_cc() -> None: logger.debug('Getting addresses from cover letter') cover, tracking = load_cover(strip_comments=False) - parts = b4.LoreMessage.get_body_parts(cover) seen = set() - for ltr in parts[2]: + for ltr in b4.LoreMessage.find_trailers(cover)[0]: if not ltr.addr: continue seen.add(ltr.addr[1]) @@ -3922,7 +3921,9 @@ def auto_to_cc() -> None: cmsg.set_payload(cover, charset='utf-8') clm = b4.LoreMessage(cmsg) fallback_order = str(config.get('send-trailer-order', 'To,Cc,*')) - clm.fix_trailers(extras=extras, fallback_order=fallback_order) + clm.fix_trailers( + force_patch_separator=True, extras=extras, fallback_order=fallback_order + ) logger.info('---') logger.info('You can trim/expand this list with: b4 prep --edit-cover') store_cover(clm.body, tracking) diff --git a/src/tests/test_ez.py b/src/tests/test_ez.py index 192b000..b0b8570 100644 --- a/src/tests/test_ez.py +++ b/src/tests/test_ez.py @@ -635,6 +635,48 @@ def test_mixin_cover_keeps_notes_with_midsection_trailer_line() -> None: assert body.index('base-commit:') > body.index('diff --git') +def test_auto_to_cc_sees_cover_trailers_before_changelog(prepdir: str) -> None: + fname = os.path.join(prepdir, 'auto-to-cc.txt') + with open(fname, 'w') as fh: + fh.write('content\n') + ecode, out = b4.git_run_command(None, ['add', fname], logstderr=True) + assert ecode == 0, f'git add failed: {out}' + ecode, out = b4.git_run_command( + None, ['commit', '-m', 'feat: exercise auto-to-cc'], logstderr=True + ) + assert ecode == 0, f'git commit failed: {out}' + + _cover, tracking = b4.ez.load_cover() + cover = ( + 'Cover title\n' + '\n' + 'Cover body text.\n' + '\n' + 'To: [email protected]\n' + '\n' + '---\n' + 'Changes in v2:\n' + '- some change\n' + '- Link to v1: https://example.com/r/msgid%40example.com\n' + ) + b4.ez.store_cover(cover, tracking) + b4.MAIN_CONFIG.update( + { + 'send-series-to': '[email protected]', + 'send-series-cc': '[email protected]', + } + ) + + b4.ez.auto_to_cc() + + new_cover, _tracking = b4.ez.load_cover() + assert new_cover.count('To: [email protected]') == 1 + assert new_cover.count('Cc: [email protected]') == 1 + assert new_cover.index('Cc: [email protected]') < new_cover.index( + 'Changes in v2:' + ) + + # A single patch whose commit message body is empty: the payload jumps straight # from the (header-borne) subject to the '---' cutline. This is what b4 emits # when the author leaves the commit message blank. --- base-commit: 4217c3e5d3e1eb259626142fd71b91ec6d5e3d1e change-id: 20260628-prep-2c600976cfe6 Best regards, -- Akihiko Odaki <[email protected]>