[PATCH b4 4/4] review: drop the unused batch message-count updater

Christian Brauner <[email protected]> Tue, 21 Jul 2026 22:21:04 +0200
Newsgroups org.kernel.linux.tools
Message-ID <20260721-work-b4-accepted-unread-badge-v1-4-28581acc4988@kernel.org>
update_message_counts() was the old Stage-2 batch helper that
re-fetched threads to refresh message counts after the main update
loop. Since the counts are updated from the already-fetched thread
messages inside update_series_tracking(), nothing calls it anymore.

Signed-off-by: Christian Brauner (Amutable) <[email protected]>
---
 src/b4/review/tracking.py | 209 ----------------------------------------------
 1 file changed, 209 deletions(-)

diff --git a/src/b4/review/tracking.py b/src/b4/review/tracking.py
index 0d10dff..246f6fe 100644
--- a/src/b4/review/tracking.py
+++ b/src/b4/review/tracking.py
@@ -1759,41 +1759,6 @@ def get_review_branches(topdir: Optional[str] = None) -> list[str]:
     return b4.git_get_command_lines(topdir, gitargs)
 
 
-def _fetch_thread_mbox_bytes(message_id: str) -> Optional[bytes]:
-    """Fetch and decompress the full thread mbox for a message ID via LoreNode.
-
-    Returns the raw mbox bytes, or None on failure or when offline.
-    """
-    if not b4.can_network:
-        return None
-    try:
-        node = b4.get_lore_node()
-        return node.get_mbox_by_msgid(message_id)
-    except (liblore.RemoteError, Exception) as ex:
-        logger.debug('Could not fetch mbox for %s: %s', message_id, ex)
-        return None
-
-
-def _latest_date_from_mbox(mbox_bytes: bytes) -> Optional[str]:
-    """Return the most recent Date: header from mbox bytes as an ISO timestamp."""
-    latest: Optional[datetime.datetime] = None
-    for line in mbox_bytes.split(b'\n'):
-        if not line.lower().startswith(b'date:'):
-            continue
-        date_str = line[5:].strip().decode('utf-8', errors='replace')
-        try:
-            dt = email.utils.parsedate_to_datetime(date_str)
-            if dt.tzinfo is None:
-                dt = dt.replace(tzinfo=datetime.timezone.utc)
-            if latest is None or dt > latest:
-                latest = dt
-        except Exception:
-            continue
-    if latest is None:
-        return None
-    return latest.astimezone(datetime.timezone.utc).isoformat()
-
-
 def _latest_date_from_msgs(msgs: List[Any]) -> Optional[str]:
     """Return the most recent Date header from EmailMessage objects as ISO timestamp."""
     latest: Optional[datetime.datetime] = None
@@ -1875,56 +1840,6 @@ def update_message_count_from_msgs(
     return True
 
 
-def fetch_thread_message_count(message_id: str) -> Optional[int]:
-    """Fetch the total message count for a thread via public-inbox t.mbox.gz.
-
-    Returns the total number of messages in the thread, or None on failure
-    or when offline.
-    """
-    mbox_bytes = _fetch_thread_mbox_bytes(message_id)
-    if mbox_bytes is None:
-        return None
-    parsed = b4.split_and_dedupe_pi_results(mbox_bytes)
-    return len(parsed)
-
-
-def _fetch_new_since(
-    message_id: str, since: str
-) -> Optional[Tuple[int, Optional[str]]]:
-    """Fetch new thread messages since a timestamp via LoreNode.
-
-    Uses LoreNode.get_thread_updates_since() which queries the public-inbox
-    ``rt:`` (Received-date) search endpoint scoped to the thread.
-
-    *since* is an ISO-format timestamp stored in the database.
-
-    Returns ``(count, latest_date_iso)`` where *count* is the number of new
-    messages (0 if none) and *latest_date_iso* is the most recent Date: header
-    found (None if no messages or no parseable date).  Returns None on error.
-    """
-    if not b4.can_network:
-        return None
-    try:
-        since_dt = datetime.datetime.fromisoformat(since)
-    except (ValueError, TypeError) as ex:
-        logger.debug('Could not parse last_update_check timestamp %r: %s', since, ex)
-        return None
-
-    try:
-        node = b4.get_lore_node()
-        msgs = node.get_thread_updates_since(
-            message_id, since_dt, strict=False, sort=False
-        )
-        if not msgs:
-            return (0, None)
-        count = len(msgs)
-        latest_date = _latest_date_from_msgs(msgs)
-        return (count, latest_date)
-    except Exception as ex:
-        logger.debug('Thread update query failed for %s: %s', message_id, ex)
-        return None
-
-
 def _store_thread_blob(topdir: str, change_id: str, msgs: List[Any]) -> Optional[str]:
     """Serialize msgs to mboxrd and write as a git blob; update tracking commit.
 
@@ -2299,130 +2214,6 @@ def ensure_thread_context_blob(
     return ctx_sha
 
 
-def update_message_counts(
-    identifier: str,
-    series_list: List[Dict[str, Any]],
-    topdir: Optional[str] = None,
-    prefetched: Optional[Dict[Tuple[str, int], List[Any]]] = None,
-) -> Dict[str, int]:
-    """Fetch and store thread message counts for a list of series.
-
-    For each active series in *series_list* that has a message_id:
-
-    - **First fetch** (``message_count IS NULL``): downloads the full t.json
-      thread index and stores the count.  ``seen_message_count`` is initialised
-      to the same value so no badge appears until *new* activity arrives.
-    - **Incremental** (``message_count IS NOT NULL``): POSTs a ``dt:`` query
-      for messages newer than ``last_update_check``.  An empty response (nothing
-      new) produces **zero database writes**, keeping the DB mtime stable and
-      suppressing spurious list reloads in the TUI.
-
-    When *prefetched* is provided (a dict mapping ``(change_id, revision)`` to
-    a list of already-fetched ``EmailMessage`` objects), the first-fetch path
-    reuses those messages instead of re-downloading the thread from lore.  This
-    avoids duplicate HEAD + GET requests when called right after
-    ``update_series_tracking()``.
-
-    Returns ``{'updated': n, 'errors': n}`` where *updated* counts series whose
-    ``message_count`` actually changed.
-    """
-    updated = 0
-    errors = 0
-    skip_statuses = frozenset(('archived', 'accepted', 'thanked', 'snoozed'))
-    now = datetime.datetime.now(datetime.timezone.utc).isoformat()
-
-    try:
-        conn = get_db(identifier)
-    except FileNotFoundError:
-        return {'updated': 0, 'errors': 0}
-
-    for series in series_list:
-        if series.get('status') in skip_statuses:
-            continue
-        message_id = series.get('message_id', '')
-        if not message_id:
-            continue
-        change_id = series.get('change_id', '')
-        revision = series.get('revision', 1)
-        if not change_id:
-            continue
-
-        row = conn.execute(
-            'SELECT message_count, seen_message_count, last_update_check'
-            ' FROM series WHERE change_id = ? AND revision = ?',
-            (change_id, revision),
-        ).fetchone()
-
-        existing_count = row['message_count'] if row else None
-        last_check = row['last_update_check'] if row else None
-
-        if existing_count is None or last_check is None:
-            # ── First fetch ──────────────────────────────────────────────────
-            # Reuse pre-fetched messages from update_series_tracking when
-            # available, otherwise fall back to a full lore download.
-            pre_msgs = prefetched.get((change_id, revision)) if prefetched else None
-            if pre_msgs is not None:
-                count = len(pre_msgs)
-                last_activity = _latest_date_from_msgs(pre_msgs)
-                conn.execute(
-                    'UPDATE series'
-                    ' SET message_count = ?, seen_message_count = ?,'
-                    '     last_update_check = ?, last_activity_at = ?'
-                    ' WHERE change_id = ? AND revision = ?',
-                    (count, count, now, last_activity, change_id, revision),
-                )
-                conn.commit()
-                updated += 1
-                if topdir and pre_msgs:
-                    _store_thread_blob(topdir, change_id, pre_msgs)
-            else:
-                mbox_bytes = _fetch_thread_mbox_bytes(message_id)
-                if mbox_bytes is None:
-                    errors += 1
-                    continue
-                parsed = b4.split_and_dedupe_pi_results(mbox_bytes)
-                count = len(parsed)
-                last_activity = _latest_date_from_mbox(mbox_bytes)
-                conn.execute(
-                    'UPDATE series'
-                    ' SET message_count = ?, seen_message_count = ?,'
-                    '     last_update_check = ?, last_activity_at = ?'
-                    ' WHERE change_id = ? AND revision = ?',
-                    (count, count, now, last_activity, change_id, revision),
-                )
-                conn.commit()
-                updated += 1
-                if topdir and parsed:
-                    _store_thread_blob(topdir, change_id, parsed)
-        else:
-            # ── Incremental: query for messages since last check ─────────
-            result = _fetch_new_since(message_id, last_check)
-            if result is None:
-                errors += 1
-                continue
-            new_count, new_activity = result
-            if new_count > 0:
-                # New replies arrived — update count, timestamp, and latest activity
-                conn.execute(
-                    'UPDATE series'
-                    ' SET message_count = message_count + ?, last_update_check = ?,'
-                    '     last_activity_at = COALESCE(?, last_activity_at)'
-                    ' WHERE change_id = ? AND revision = ?',
-                    (new_count, now, new_activity, change_id, revision),
-                )
-                conn.commit()
-                updated += 1
-                if topdir:
-                    new_mbox = _fetch_thread_mbox_bytes(message_id)
-                    if new_mbox:
-                        parsed = b4.split_and_dedupe_pi_results(new_mbox)
-                        if parsed:
-                            _store_thread_blob(topdir, change_id, parsed)
-
-    conn.close()
-    return {'updated': updated, 'errors': errors}
-
-
 def mark_all_messages_seen(
     conn: sqlite3.Connection, change_id: str, revision: int
 ) -> None:

-- 
2.53.0