[Concept,28/37] patman: Add 'series info' command to show version details

Message ID 20260404213020.372253-29-sjg@u-boot.org
State New
Headers
Series patman: Autolink fixes and AI-assisted patch review |

Commit Message

Simon Glass April 4, 2026, 9:29 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

Add a 'patman series info' command that displays detailed information
about a series including description, upstream, and for each version:
the patchwork link, per-version description, cover letter name, patch
list with state, archive tag and review notes.

This helps diagnose autolink issues by showing what description each
version has stored, which is what patchwork searches use.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 tools/patman/cmdline.py      |  1 +
 tools/patman/control.py      |  2 ++
 tools/patman/cseries.py      | 47 ++++++++++++++++++++++++++++++++++++
 tools/patman/test_cseries.py | 42 ++++++++++++++++++++++++++++++++
 4 files changed, 92 insertions(+)
  

Patch

diff --git a/tools/patman/cmdline.py b/tools/patman/cmdline.py
index edefa778446..bee9fd21483 100644
--- a/tools/patman/cmdline.py
+++ b/tools/patman/cmdline.py
@@ -294,6 +294,7 @@  def add_series_subparser(subparsers):
 
     series_subparsers.add_parser('get-link')
     series_subparsers.add_parser('inc')
+    series_subparsers.add_parser('info')
     ls = series_subparsers.add_parser('ls', aliases=['list'])
     _add_archived(ls)
 
diff --git a/tools/patman/control.py b/tools/patman/control.py
index 3ce9736d6ba..9ba9b6e0b8e 100644
--- a/tools/patman/control.py
+++ b/tools/patman/control.py
@@ -199,6 +199,8 @@  def do_series(args, test_db=None, pwork=None, cser=None):
                                dry_run=args.dry_run, show_summary=True)
         elif args.subcmd == 'dec':
             cser.decrement(args.series, args.dry_run)
+        elif args.subcmd == 'info':
+            cser.show_info(args.series)
         elif args.subcmd == 'gather':
             cser.gather(pwork, args.series, args.version, args.show_comments,
                         args.show_cover_comments, args.gather_tags,
diff --git a/tools/patman/cseries.py b/tools/patman/cseries.py
index 1dd1550f367..e272a5839fc 100644
--- a/tools/patman/cseries.py
+++ b/tools/patman/cseries.py
@@ -861,6 +861,53 @@  class Cseries(cser_helper.CseriesHelper):
         if dry_run:
             tout.info('Dry run completed')
 
+    def show_info(self, series):
+        """Show detailed information about a series and all its versions
+
+        Args:
+            series (str): Series name, or None for current branch
+        """
+        ser, _ = self._parse_series_and_version(series, None)
+        if not ser.idnum:
+            raise ValueError(f"Series '{ser.name}' not found in database")
+
+        print(f"Series: {ser.name}")
+        print(f"  Description: {ser.desc}")
+        print(f"  Upstream: {ser.upstream or '(none)'}")
+
+        versions = self.db.ser_ver_get_for_series(ser.idnum)
+        if not isinstance(versions, list):
+            versions = [versions]
+
+        for sv in versions:
+            link_str = sv.link or '(none)'
+            print(f"\n  Version {sv.version}:")
+            print(f"    Link: {link_str}")
+            print(f"    Description: {sv.desc or '(none)'}")
+            if sv.name:
+                print(f"    Cover: {sv.name}")
+            if sv.archive_tag:
+                print(f"    Archive tag: {sv.archive_tag}")
+
+            # Show patches
+            try:
+                pclist = self.db.pcommit_get_list(sv.idnum)
+                print(f"    Patches: {len(pclist)}")
+                for pc in pclist:
+                    state = f' [{pc.state}]' if pc.state else ''
+                    print(f"      {pc.seq + 1}: {pc.subject}{state}")
+            except (ValueError, AttributeError):
+                pass
+
+            # Show notes if any
+            if sv.notes:
+                lines = sv.notes.strip().splitlines()
+                print(f"    Notes: {lines[0]}")
+                for line in lines[1:3]:
+                    print(f"           {line}")
+                if len(lines) > 3:
+                    print(f"           ... ({len(lines)} lines)")
+
     def set_upstream(self, series, ups, dry_run=False):
         """Set the upstream for a series
 
diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py
index e207e8bc173..5974c69253a 100644
--- a/tools/patman/test_cseries.py
+++ b/tools/patman/test_cseries.py
@@ -4438,3 +4438,45 @@  Date:   .*
         # Future 3 weeks
         when = datetime(2025, 3, 31, 10, 0, 0)
         self.assertEqual('in 3w', wf.friendly_time(now, when))
+
+    def test_series_info(self):
+        """Test the series info command"""
+        cser = self.get_database()
+
+        # Create a series with upstream and two versions
+        cser.db.upstream_add('us', 'https://us.example.com')
+        series_id = cser.db.series_add('test-info', 'My test series', ups='us')
+        svid1 = cser.db.ser_ver_add(series_id, 1, link='12345',
+                                     desc='First version desc')
+        svid2 = cser.db.ser_ver_add(series_id, 2, desc='Second version desc')
+
+        # Add patches to v1
+        from patman.database import Pcommit
+        cser.db.pcommit_add_list(svid1, [
+            Pcommit(idnum=None, seq=0, subject='Fix the widget',
+                    svid=svid1, change_id=None, state=None,
+                    patch_id=None, num_comments=0),
+            Pcommit(idnum=None, seq=1, subject='Add widget tests',
+                    svid=svid1, change_id=None, state=None,
+                    patch_id=None, num_comments=0)])
+
+        # Add notes to v2
+        cser.db.ser_ver_set_notes(svid2, 'Fixed review feedback')
+        cser.commit()
+
+        with terminal.capture() as (out, _):
+            cser.show_info('test-info')
+
+        output = out.getvalue()
+        self.assertIn('Series: test-info', output)
+        self.assertIn('Description: My test series', output)
+        self.assertIn('Upstream: us', output)
+        self.assertIn('Version 1:', output)
+        self.assertIn('Link: 12345', output)
+        self.assertIn('First version desc', output)
+        self.assertIn('Patches: 2', output)
+        self.assertIn('Fix the widget', output)
+        self.assertIn('Add widget tests', output)
+        self.assertIn('Version 2:', output)
+        self.assertIn('Second version desc', output)
+        self.assertIn('Notes: Fixed review feedback', output)