[Concept,05/17] pickman: Add list-sources command

Message ID 20251217022611.389379-6-sjg@u-boot.org
State New
Headers
Series pickman: Add a manager for cherry-picks |

Commit Message

Simon Glass Dec. 17, 2025, 2:25 a.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Add a command to list all tracked source branches and their last
cherry-picked commits from the database.

Usage: ./tools/pickman/pickman list-sources

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 tools/pickman/README.rst  |  4 +++
 tools/pickman/__main__.py |  1 +
 tools/pickman/control.py  | 23 +++++++++++++
 tools/pickman/database.py |  9 +++++
 tools/pickman/ftest.py    | 70 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 107 insertions(+)
  

Patch

diff --git a/tools/pickman/README.rst b/tools/pickman/README.rst
index aab0642d374..70f53a6e212 100644
--- a/tools/pickman/README.rst
+++ b/tools/pickman/README.rst
@@ -19,6 +19,10 @@  This finds the merge-base commit between the master branch (ci/master) and the
 source branch, and stores it in the database as the starting point for
 cherry-picking.
 
+To list all tracked source branches::
+
+    ./tools/pickman/pickman list-sources
+
 To compare branches and show commits that need to be cherry-picked::
 
     ./tools/pickman/pickman compare
diff --git a/tools/pickman/__main__.py b/tools/pickman/__main__.py
index 7263f4c5fb0..63930953ebb 100755
--- a/tools/pickman/__main__.py
+++ b/tools/pickman/__main__.py
@@ -35,6 +35,7 @@  def parse_args(argv):
     add_source.add_argument('source', help='Source branch name')
 
     subparsers.add_parser('compare', help='Compare branches')
+    subparsers.add_parser('list-sources', help='List tracked source branches')
     subparsers.add_parser('test', help='Run tests')
 
     return parser.parse_args(argv)
diff --git a/tools/pickman/control.py b/tools/pickman/control.py
index fa53a26b6ad..5780703bfba 100644
--- a/tools/pickman/control.py
+++ b/tools/pickman/control.py
@@ -92,6 +92,28 @@  def do_add_source(args, dbs):
     return 0
 
 
+def do_list_sources(args, dbs):  # pylint: disable=unused-argument
+    """List all tracked source branches
+
+    Args:
+        args (Namespace): Parsed arguments
+        dbs (Database): Database instance
+
+    Returns:
+        int: 0 on success
+    """
+    sources = dbs.source_get_all()
+
+    if not sources:
+        tout.info('No source branches tracked')
+    else:
+        tout.info('Tracked source branches:')
+        for name, last_commit in sources:
+            tout.info(f'  {name}: {last_commit[:12]}')
+
+    return 0
+
+
 def do_compare(args, dbs):  # pylint: disable=unused-argument
     """Compare branches and print results.
 
@@ -133,6 +155,7 @@  def do_test(args, dbs):  # pylint: disable=unused-argument
 COMMANDS = {
     'add-source': do_add_source,
     'compare': do_compare,
+    'list-sources': do_list_sources,
     'test': do_test,
 }
 
diff --git a/tools/pickman/database.py b/tools/pickman/database.py
index 436734fe1f7..46b8556945e 100644
--- a/tools/pickman/database.py
+++ b/tools/pickman/database.py
@@ -178,6 +178,15 @@  class Database:
             return rec[0]
         return None
 
+    def source_get_all(self):
+        """Get all source branches and their last commits
+
+        Return:
+            list of tuple: (name, last_commit) pairs
+        """
+        res = self.execute('SELECT name, last_commit FROM source ORDER BY name')
+        return res.fetchall()
+
     def source_set(self, name, commit):
         """Set the last cherry-picked commit for a source branch
 
diff --git a/tools/pickman/ftest.py b/tools/pickman/ftest.py
index 632cd56793f..91a003b649c 100644
--- a/tools/pickman/ftest.py
+++ b/tools/pickman/ftest.py
@@ -287,6 +287,76 @@  class TestDatabase(unittest.TestCase):
             self.assertIs(dbs1, dbs2)
             dbs1.close()
 
+    def test_source_get_all(self):
+        """Test getting all sources."""
+        with terminal.capture():
+            dbs = database.Database(self.db_path)
+            dbs.start()
+
+            # Empty initially
+            self.assertEqual(dbs.source_get_all(), [])
+
+            # Add some sources
+            dbs.source_set('branch-a', 'abc123')
+            dbs.source_set('branch-b', 'def456')
+            dbs.commit()
+
+            # Should be sorted by name
+            sources = dbs.source_get_all()
+            self.assertEqual(len(sources), 2)
+            self.assertEqual(sources[0], ('branch-a', 'abc123'))
+            self.assertEqual(sources[1], ('branch-b', 'def456'))
+            dbs.close()
+
+
+class TestListSources(unittest.TestCase):
+    """Tests for list-sources command."""
+
+    def setUp(self):
+        """Set up test fixtures."""
+        fd, self.db_path = tempfile.mkstemp(suffix='.db')
+        os.close(fd)
+        os.unlink(self.db_path)
+        self.old_db_fname = control.DB_FNAME
+        control.DB_FNAME = self.db_path
+        database.Database.instances.clear()
+
+    def tearDown(self):
+        """Clean up test fixtures."""
+        control.DB_FNAME = self.old_db_fname
+        if os.path.exists(self.db_path):
+            os.unlink(self.db_path)
+        database.Database.instances.clear()
+
+    def test_list_sources_empty(self):
+        """Test list-sources with no sources"""
+        args = argparse.Namespace(cmd='list-sources')
+        with terminal.capture() as (stdout, _):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 0)
+        self.assertIn('No source branches tracked', stdout.getvalue())
+
+    def test_list_sources(self):
+        """Test list-sources with sources"""
+        # Add some sources first
+        with terminal.capture():
+            dbs = database.Database(self.db_path)
+            dbs.start()
+            dbs.source_set('us/next', 'abc123def456')
+            dbs.source_set('other/branch', 'def456abc789')
+            dbs.commit()
+            dbs.close()
+
+        database.Database.instances.clear()
+        args = argparse.Namespace(cmd='list-sources')
+        with terminal.capture() as (stdout, _):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 0)
+        output = stdout.getvalue()
+        self.assertIn('Tracked source branches:', output)
+        self.assertIn('other/branch: def456abc789', output)
+        self.assertIn('us/next: abc123def456', output)
+
 
 if __name__ == '__main__':
     unittest.main()