[PATCH b4] Keep coverless review trailers on patches

Tamir Duberstein <[email protected]> Fri, 05 Jun 2026 08:31:04 -0400
Newsgroups org.kernel.linux.tools
Message-ID <[email protected]>
Trailer consolidation promoted matching per-patch trailers to the series
record even when the series had no cover letter. For a single-patch
series, this split the trailer and inline comments into two replies to
the same message.

Only promote trailers when there is a real cover letter, keeping a
coverless patch review together in one reply.

Assisted-by: Codex gpt-5.5
Signed-off-by: Tamir Duberstein <[email protected]>
---
See incorrectly split replies:
- https://lore.kernel.org/all/178066138611.3594.6559938011018564470.b4-review@b4/
- https://lore.kernel.org/all/178066138616.3594.4372925551839686675.b4-review@b4/
---
 src/b4/review_tui/_review_app.py |  7 +++++-
 src/tests/test_tui_review.py     | 52 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 58 insertions(+), 1 deletion(-)

diff --git a/src/b4/review_tui/_review_app.py b/src/b4/review_tui/_review_app.py
index 8786fbb..38a7ba3 100644
--- a/src/b4/review_tui/_review_app.py
+++ b/src/b4/review_tui/_review_app.py
@@ -1181,7 +1181,12 @@ class ReviewApp(CheckRunnerMixin, App[None]):
                             preview.pop('trailers', None)
                             b4.review._cleanup_review(patch, self._usercfg)
                         self._refresh_patch_item(pidx + 1)
-            elif self._selected_idx > 0 and new_trailers and self._patches:
+            elif (
+                self._has_cover
+                and self._selected_idx > 0
+                and new_trailers
+                and self._patches
+            ):
                 # Check if all patches now have the same trailer set —
                 # if so, promote to cover letter
                 new_names = {t.split(':', 1)[0].strip().lower() for t in new_trailers}
diff --git a/src/tests/test_tui_review.py b/src/tests/test_tui_review.py
index fb6fae0..ab6f4dd 100644
--- a/src/tests/test_tui_review.py
+++ b/src/tests/test_tui_review.py
@@ -10,6 +10,7 @@ cosmetic commit edits (e.g. reworded subjects via git rebase -i).
 """
 
 from typing import Any, Dict, List, Tuple
+from unittest import mock
 
 import pytest
 
@@ -179,6 +180,57 @@ def _rewrite_patches(
 # ---------------------------------------------------------------------------
 
 
+class TestTrailerConsolidation:
+    """Tests for consolidating matching trailers onto a cover letter."""
+
+    @pytest.mark.asyncio
+    async def test_coverless_patch_keeps_trailer_with_comments(
+        self, gitdir: str
+    ) -> None:
+        """A coverless patch sends its trailer and comments in one email."""
+        branch, _patch_shas = _create_review_branch_with_patches(
+            gitdir, 'coverless-trailer', ['patch 1']
+        )
+        session = _build_session(gitdir, branch)
+        session['cover_text'] = (
+            'patch 1\n\nNOTE: No cover letter provided by the author.'
+        )
+        my_email = str(session['usercfg']['email'])
+        session['patches'][0]['reviews'] = {
+            my_email: {
+                'name': str(session['usercfg']['name']),
+                'comments': [{'path': 'file', 'line': 1, 'text': 'Comment'}],
+            }
+        }
+
+        app = ReviewApp(session)
+        async with app.run_test(size=(120, 30)) as pilot:
+            await pilot.pause()
+            await pilot.press('t')
+            await pilot.pause()
+            await pilot.press('r')
+            await pilot.press('q')
+            await pilot.pause()
+
+            assert 'reviews' not in app._series
+            patch_review = app._patches[0]['reviews'][my_email]
+            assert patch_review['trailers'] == [f'Reviewed-by: {app._default_identity}']
+            assert patch_review['comments']
+
+            with mock.patch(
+                'b4.review._review._build_review_email',
+                return_value=mock.sentinel.email,
+            ):
+                msgs = b4.review.collect_review_emails(
+                    app._series,
+                    app._patches,
+                    app._cover_text,
+                    app._topdir,
+                    app._commit_shas,
+                )
+            assert msgs == [mock.sentinel.email]
+
+
 class TestReconcileAfterShell:
     """Tests for _reconcile_after_shell tracking fixup."""
 

---
base-commit: d5d981426ead3f490713ef5d2fd1aa3d0f13b005
change-id: 20260605-review-double-review-email-ba1b09cca823

Best regards,
--  
Tamir Duberstein <[email protected]>