Linux maintainer tooling and workflows
 help / color / mirror / Atom feed
From: Mark Brown <broonie@kernel.org>
To: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
Cc: tools@kernel.org, Mark Brown <broonie@kernel.org>
Subject: [PATCH] review: Refresh series after manually linking a new revision
Date: Wed, 14 Oct 2026 12:49:58 +0100	[thread overview]
Message-ID: <20261014115111.303997-1-broonie@kernel.org> (raw)

When a new revision is manually linked to an existing series by message ID
the new version is not immediately visible in the UI, you need to do an
update of the series to cause it to become visible and allow the series to
be upgraded. Fix this by forcing an immediate update as part of linking the
series, splitting action_update_one() to separate the UI hook from the
implementation.

Signed-off-by: Mark Brown <broonie@kernel.org>
---
 src/b4/review_tui/_tracking_app.py | 19 ++++++--
 src/tests/test_tui_tracking.py     | 74 +++++++++++++++++++++++++++++-
 2 files changed, 87 insertions(+), 6 deletions(-)

diff --git a/src/b4/review_tui/_tracking_app.py b/src/b4/review_tui/_tracking_app.py
index 4ae826e..e202eb0 100644
--- a/src/b4/review_tui/_tracking_app.py
+++ b/src/b4/review_tui/_tracking_app.py
@@ -2635,15 +2635,17 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
             self.notify('No series selected', severity='warning')
             return
 
+        self._update_series(self._selected_series)
+
+    def _update_series(self, series: Dict[str, Any]) -> None:
+        """Fetch thread and update revisions/trailers for a specific series."""
         config = b4.get_main_config()
         linkmask = str(config.get('linkmask', 'https://lore.kernel.org/r/%s'))
         topdir = b4.git_get_toplevel()
 
-        self._focus_change_id = self._selected_series.get('change_id')
+        self._focus_change_id = series.get('change_id')
         self.push_screen(
-            UpdateAllScreen(
-                [self._selected_series], self._identifier, linkmask, topdir
-            ),
+            UpdateAllScreen([series], self._identifier, linkmask, topdir),
             callback=self._on_update_complete,
         )
 
@@ -4322,7 +4324,14 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
         else:
             self.notify(f'Linked v{rev}')
         self._focus_change_id = change_id
-        self._invalidate_caches(change_id)
+        for series in b4.review.tracking.get_all_tracked_series(self._identifier):
+            if (
+                series.get('change_id') == change_id
+                and series.get('status') != 'archived'
+            ):
+                self._update_series(series)
+                return
+        self._invalidate_caches()
         self._load_series()
 
     def _do_switch_revision(
diff --git a/src/tests/test_tui_tracking.py b/src/tests/test_tui_tracking.py
index 03c7f3d..8c18b40 100644
--- a/src/tests/test_tui_tracking.py
+++ b/src/tests/test_tui_tracking.py
@@ -47,6 +47,7 @@ from b4.review_tui._modals import (
     ConfirmScreen,
     HelpScreen,
     LimitScreen,
+    LinkRevisionConfirmScreen,
     LinkRevisionScreen,
     RebaseScreen,
     SnoozeScreen,
@@ -66,6 +67,7 @@ from b4.review_tui._tracking_app import (
     _worktree_for_branch,
 )
 
+from .helpers.mail import MINIMAL_DIFF, make_msg
 from .helpers.tracking import create_review_branch, seed_db
 from .helpers.tui import static_text
 
@@ -1646,7 +1648,7 @@ class TestTrackingSnoozeRemembersChoice:
 
 
 class TestLinkRevisionAction:
-    """The 'link a revision' action is offered and opens the input modal."""
+    """Manually linking a revision updates the thread and revision display."""
 
     @pytest.mark.asyncio
     async def test_link_action_offered_and_dispatches(
@@ -1676,6 +1678,76 @@ class TestLinkRevisionAction:
             assert isinstance(app.screen, LinkRevisionScreen)
             await pilot.press('escape')
 
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize('outcome', ['updated', 'busy', 'cancelled'])
+    async def test_link_refreshes_thread_and_revisions(
+        self, tmp_path: pathlib.Path, outcome: str
+    ) -> None:
+        identifier = 'test-link-refresh'
+        change_id = 'link-1'
+        seed_db(
+            identifier,
+            [
+                {'change_id': 'unrelated'},
+                {
+                    'change_id': change_id,
+                    'subject': '[PATCH] linkable series',
+                    'message_id': 'link-v1@ex.com',
+                    'revisions': [1],
+                },
+            ],
+        )
+        lmbx = b4.LoreMailbox()
+        lmbx.add_message(
+            make_msg('link-v2@ex.com', '[PATCH v2] linkable series', body=MINIMAL_DIFF)
+        )
+        lser = lmbx.get_series()
+        assert lser is not None
+
+        app = TrackingApp(identifier, focus_change_id=change_id)
+        with (
+            patch.object(tracking, 'fetch_series_for_link', return_value=lser),
+            patch.object(
+                b4.review,
+                'update_all_tracking',
+                return_value={'series_checked': 1},
+                side_effect=b4.LockHeldError('busy') if outcome == 'busy' else None,
+            ) as update,
+        ):
+            async with app.run_test(size=(120, 30)) as pilot:
+                await pilot.pause()
+                await pilot.press('a', 'l')
+                await pilot.pause()
+                app.screen.query_one('#link-rev-input', Input).value = 'link-v2@ex.com'
+                await pilot.press('enter')
+                await app.workers.wait_for_complete()
+                await pilot.pause()
+                assert isinstance(app.screen, LinkRevisionConfirmScreen)
+                update.assert_not_called()
+
+                await pilot.press('escape' if outcome == 'cancelled' else 'enter')
+                await app.workers.wait_for_complete()
+                await pilot.pause()
+
+                if outcome == 'cancelled':
+                    update.assert_not_called()
+                    assert 'v2' not in static_text(
+                        app.query_one('#detail-revisions', Static)
+                    )
+                    return
+
+                update.assert_called_once()
+                updated_series = update.call_args.kwargs['series_list']
+                assert [s['change_id'] for s in updated_series] == [change_id]
+                assert app._selected_series is not None
+                assert app._selected_series['change_id'] == change_id
+                assert 'v2' in static_text(app.query_one('#detail-revisions', Static))
+                assert any('v1→v2' in row for row in _rendered_rows(app))
+                await pilot.press('a')
+                await pilot.pause()
+                assert 'upgrade' in _get_action_keys(app)
+                await pilot.press('escape')
+
 
 class TestSeriesLifecycle:
     """End-to-end lifecycle: new → reviewing → waiting → reviewing
-- 
2.47.3


                 reply	other threads:[~2026-10-14 11:53 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261014115111.303997-1-broonie@kernel.org \
    --to=broonie@kernel.org \
    --cc=konstantin@linuxfoundation.org \
    --cc=tools@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox