All of lore.kernel.org
 help / color / mirror / Atom feed
From: Christian Brauner <brauner@kernel.org>
To: "Kernel.org Tools" <tools@kernel.org>
Cc: Konstantin Ryabitsev <konstantin@linuxfoundation.org>,
	 "Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH RFC v2 20/25] review-tui: add a "Find older revisions" action
Date: Wed, 12 Aug 2026 23:47:01 +0200	[thread overview]
Message-ID: <20260812-work-b4-multiver-rows-v2-20-305d53cd723a@kernel.org> (raw)
In-Reply-To: <20260812-work-b4-multiver-rows-v2-0-305d53cd723a@kernel.org>

Offer discover_older_revisions() from the tracker's action menu, wherever
manual revision linking is offered.  The search runs in a lore worker; on
success the list reloads, deferring to the DB mtime poll when a modal is
up.

The documentation also gains the "Link a revision" line the partial block
never listed, although that action has been offered for partial series
all along.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 docs/maintainer/review.rst         |  9 ++++
 src/b4/review_tui/_tracking_app.py | 96 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 105 insertions(+)

diff --git a/docs/maintainer/review.rst b/docs/maintainer/review.rst
index ca063269..5ddd9dd2 100644
--- a/docs/maintainer/review.rst
+++ b/docs/maintainer/review.rst
@@ -252,6 +252,8 @@ actions depend on the series status:
 * ``[s]`` **Snooze** — defer until a date, duration, or git tag
 * ``[U]`` **Upgrade** — switch to a newer revision (when available)
 * ``[l]`` **Link a revision** — manually associate a revision by message-id
+* ``[o]`` **Find older revisions** — search lore for versions posted before
+  the series was tracked
 * ``[A]`` **Abandon** / ``[x]`` **Archive**
 
 **Partial** (some patches applied, remainder still in review):
@@ -262,6 +264,9 @@ actions depend on the series status:
 * ``[w]`` **Mark as waiting** — waiting on a new revision
 * ``[s]`` **Snooze** — defer until later
 * ``[U]`` **Upgrade** — switch to a newer revision (when available)
+* ``[l]`` **Link a revision** — manually associate a revision by message-id
+* ``[o]`` **Find older revisions** — search lore for versions posted before
+  the series was tracked
 * ``[A]`` **Abandon** / ``[x]`` **Archive**
 
 **New / gone:**
@@ -270,6 +275,8 @@ actions depend on the series status:
 * ``[U]`` **Upgrade** — switch to a newer revision (new only, when available)
 * ``[s]`` **Snooze** — defer until later (new only)
 * ``[l]`` **Link a revision** — manually associate a revision by message-id (new only)
+* ``[o]`` **Find older revisions** — search lore for versions posted before
+  the series was tracked (new only)
 * ``[A]`` **Abandon**
 
 **Waiting:**
@@ -277,6 +284,8 @@ actions depend on the series status:
 * ``[U]`` **Upgrade** — switch to the newer revision (when available)
 * ``[r]`` **Review** — return to reviewing
 * ``[l]`` **Link a revision** — manually associate a revision by message-id
+* ``[o]`` **Find older revisions** — search lore for versions posted before
+  the series was tracked
 * ``[A]`` **Abandon** / ``[x]`` **Archive**
 
 **Snoozed:**
diff --git a/src/b4/review_tui/_tracking_app.py b/src/b4/review_tui/_tracking_app.py
index f0ad512a..999513ca 100644
--- a/src/b4/review_tui/_tracking_app.py
+++ b/src/b4/review_tui/_tracking_app.py
@@ -32,6 +32,7 @@ from typing import (
     Union,
 )
 
+from rich.markup import escape
 from rich.text import Text as RichText
 from textual.app import App, ComposeResult
 from textual.binding import Binding
@@ -45,6 +46,7 @@ import b4.mbox
 import b4.review
 import b4.review.tracking
 import b4.ty
+import liblore
 from b4.review._review import NO_COVER_NOTE
 from b4.review_tui._common import (
     QUIT_BINDINGS,
@@ -65,6 +67,7 @@ from b4.review_tui._common import (
     resolve_styles,
     run_lore_worker,
     suspend_and_edit,
+    worker_cancelled,
 )
 from b4.review_tui._modals import (
     QUEUE_BUSY,
@@ -105,6 +108,7 @@ _ACTION_SHORTCUTS: Dict[str, str] = {
     'unsnooze': 'u',
     'upgrade': 'U',
     'link': 'l',
+    'discover': 'o',
     'thank': 't',
     'abandon': 'A',
     'archive': 'x',
@@ -514,6 +518,32 @@ def _resolve_worktree_take_conflict(
     return True
 
 
+def _discovery_error_notice(error: str) -> str:
+    """Message for a discovery run that came back with an error.
+
+    *error* is a lore exception's text, so it is escaped rather than
+    trusted: notify() renders Rich markup, and an unescaped bracket in it
+    is either swallowed as a style tag -- taking the diagnostic the message
+    exists to carry -- or, when the text holds a '[/...]' path fragment,
+    raises MarkupError inside the toast render.  See _conflicts_notice.
+    """
+    return f'Older-revision search failed: {escape(error)}'
+
+
+def _conflicts_notice(conflicts: List[int]) -> str:
+    """Message for versions a discovery run found another series tracking.
+
+    The ``[l]`` is escaped because notify() renders Rich markup: unescaped
+    it is parsed as a style tag, and the one key the message exists to name
+    is dropped from what the maintainer actually reads.
+    """
+    clist = ', '.join(f'v{r}' for r in conflicts)
+    return (
+        f'{clist} already tracked as a separate series'
+        f' — use {escape("[l]")} to link and absorb'
+    )
+
+
 def _format_snooze_until(value: str) -> str:
     """Format a snoozed_until value for display.
 
@@ -1089,6 +1119,44 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
             elif event.state == WorkerState.ERROR:
                 self.notify('Could not fetch series', severity='error')
             return
+        if event.worker.name == '_discover_older':
+            if event.state == WorkerState.SUCCESS:
+                result = event.worker.result or {}
+                error = result.get('error')
+                found = result.get('found', 0)
+                conflicts = result.get('conflicts') or []
+                if error == 'offline':
+                    # Not a failure, just nothing to search with.
+                    self.notify('Offline — cannot search for older revisions')
+                elif error:
+                    self.notify(_discovery_error_notice(str(error)), severity='error')
+                elif found:
+                    # found is len(revisions), so the list is never empty here.
+                    rlist = ', '.join(f'v{r}' for r in result.get('revisions') or [])
+                    self.notify(f'Found and added: {rlist}')
+                    # Reload so the new revisions show up right away; if a
+                    # modal is up, the DB mtime poll picks it up instead.
+                    if len(self.app.screen_stack) == 1:
+                        if self._selected_series:
+                            self._focus_change_id = self._selected_series.get(
+                                'change_id'
+                            )
+                        self._invalidate_caches()
+                        self._load_series()
+                elif not conflicts:
+                    # Only when there is nothing else to say.  A run that
+                    # found versions and skipped every one of them as a
+                    # conflict reports 0 found, and saying "none found"
+                    # ahead of the list of them contradicts itself.
+                    self.notify('No older revisions found')
+                if conflicts:
+                    self.notify(_conflicts_notice(conflicts), severity='warning')
+            elif event.state == WorkerState.ERROR:
+                if isinstance(event.worker.error, liblore.OperationCancelledError):
+                    # Shutting down or navigating away, not a failure.
+                    return
+                self.notify('Older-revision search failed', severity='error')
+            return
         if event.worker.name != '_startup_rescan':
             return
         if event.state == WorkerState.SUCCESS:
@@ -1516,6 +1584,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
                 actions.append(('upgrade', 'Upgrade to newer revision'))
             if status == 'new':
                 actions.append(('link', 'Manually link a revision'))
+                actions.append(('discover', 'Find older revisions'))
             actions.append(('abandon', 'Abandon series'))
             if status == 'new':
                 actions.append(('waiting', 'Mark as waiting on new revision'))
@@ -1548,6 +1617,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
                 actions.append(('thank', 'Send thank-you'))
             if status in ('reviewing', 'replied', 'partial', 'waiting'):
                 actions.append(('link', 'Manually link a revision'))
+                actions.append(('discover', 'Find older revisions'))
             # 'Return to reviewing' sits just above the abandon/archive block
             # rather than at the top of the menu.
             if status in ('accepted', 'partial', 'thanked'):
@@ -1572,6 +1642,7 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
             'thank': self.action_thank,
             'upgrade': self.action_update_revision,
             'link': self.action_link_revision,
+            'discover': self.action_discover_older,
             'archive': self.action_archive,
             'waiting': self.action_waiting,
             'snooze': self.action_snooze,
@@ -3957,6 +4028,31 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
             ),
         )
 
+    def action_discover_older(self) -> None:
+        """Search lore for older revisions of the selected series."""
+        if not self._selected_series or not self._identifier:
+            return
+        series = dict(self._selected_series)
+        config = b4.get_main_config()
+        linkmask = str(config.get('linkmask', ''))
+        topdir = b4.git_get_toplevel()
+        identifier = self._identifier
+        self.notify('Searching lore for older revisions…')
+
+        def _discover() -> Dict[str, Any]:
+            # The search machinery logs to the console; keep it from
+            # scribbling over the TUI.
+            with _quiet_worker():
+                return b4.review.tracking.discover_older_revisions(
+                    identifier,
+                    series,
+                    linkmask,
+                    topdir=topdir,
+                    cancel_cb=worker_cancelled,
+                )
+
+        run_lore_worker(self, _discover, name='_discover_older')
+
     def action_link_revision(self) -> None:
         """Manually link another revision to the selected series by msgid.
 

-- 
2.53.0


  parent reply	other threads:[~2026-08-12 21:47 UTC|newest]

Thread overview: 26+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-12 21:46 [PATCH RFC v2 00/25] review: track and browse every version of a tracked series Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 01/25] review-tui: fix rethreaded series thread viewing Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 02/25] review: do not clear fields a re-adding caller does not know Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 03/25] review-tui: keep the rethread flag on an upgraded series row Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 04/25] review: guard the tracking-commit amend on the worktree, not the checkout Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 05/25] review-tui: recompute an evicted A·R·T cache entry Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 06/25] review: test the prerequisite fixes Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 07/25] review: serialize schema migrations against a concurrent opener Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 08/25] review: test the migration serialization Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 09/25] review: track message counts for all revisions of a series Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 10/25] review: give per-change_id state its own table Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 11/25] review: test per-revision message tracking Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 12/25] review-tui: poll every revision on u/U updates Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 13/25] review: test the per-revision poll sweep Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 14/25] review-tui: resolve the tracked revision in revision lists Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 15/25] review-tui: fall back when a cached thread blob has no series Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 16/25] review-tui: test revision resolution and the range-diff fallback Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 17/25] review: skip the catalog mirror when nothing moved Christian Brauner
2026-08-12 21:46 ` [PATCH RFC v2 18/25] review: match a stray posting by message-id Christian Brauner
2026-08-12 21:47 ` [PATCH RFC v2 19/25] review: add backward discovery of older series revisions Christian Brauner
2026-08-12 21:47 ` Christian Brauner [this message]
2026-08-12 21:47 ` [PATCH RFC v2 21/25] review: test the catalog mirror, stray matching and backward discovery Christian Brauner
2026-08-12 21:47 ` [PATCH RFC v2 22/25] review-tui: extract the Msgs column renderer from TrackedSeriesItem Christian Brauner
2026-08-12 21:47 ` [PATCH RFC v2 23/25] review-tui: give the unseen badge a column of its own Christian Brauner
2026-08-12 21:47 ` [PATCH RFC v2 24/25] review-tui: expand tracked series into per-version rows Christian Brauner
2026-08-12 21:47 ` [PATCH RFC v2 25/25] review-tui: test per-version tracker rows Christian Brauner

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=20260812-work-b4-multiver-rows-v2-20-305d53cd723a@kernel.org \
    --to=brauner@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.