* [PATCH] review: Refresh series after manually linking a new revision
@ 2026-10-14 11:49 Mark Brown
0 siblings, 0 replies; only message in thread
From: Mark Brown @ 2026-10-14 11:49 UTC (permalink / raw)
To: Konstantin Ryabitsev; +Cc: tools, Mark Brown
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
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-10-14 11:53 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-14 11:49 [PATCH] review: Refresh series after manually linking a new revision Mark Brown
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox