[Concept,18/24] pickman: Add count-merges command

Message ID 20251217022823.392557-19-sjg@u-boot.org
State New
Headers
Series pickman: Refine the feature set |

Commit Message

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

Add command to show the total number of merge commits remaining to be
processed from a source branch. This helps track progress through a
large backlog of merges.

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

 tools/pickman/README.rst  |  7 ++++
 tools/pickman/__main__.py |  5 +++
 tools/pickman/control.py  | 36 +++++++++++++++++
 tools/pickman/ftest.py    | 83 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 131 insertions(+)
  

Patch

diff --git a/tools/pickman/README.rst b/tools/pickman/README.rst
index d94d399ab4d..4b99de552b2 100644
--- a/tools/pickman/README.rst
+++ b/tools/pickman/README.rst
@@ -68,6 +68,13 @@  This finds commits between the last cherry-picked commit and the next merge
 commit in the source branch. It stops at the merge commit since that typically
 represents a logical grouping of commits (e.g., a pull request).
 
+To count the total remaining merges to process::
+
+    ./tools/pickman/pickman count-merges us/next
+
+This shows how many merge commits remain on the first-parent chain between the
+last cherry-picked commit and the source branch tip.
+
 To show the next N merges that will be applied::
 
     ./tools/pickman/pickman next-merges us/next
diff --git a/tools/pickman/__main__.py b/tools/pickman/__main__.py
index 4a60ed7eedc..1258a0835c2 100755
--- a/tools/pickman/__main__.py
+++ b/tools/pickman/__main__.py
@@ -54,6 +54,11 @@  def parse_args(argv):
     commit_src.add_argument('commit', help='Commit hash to record')
 
     subparsers.add_parser('compare', help='Compare branches')
+
+    count_merges = subparsers.add_parser('count-merges',
+                                         help='Count remaining merges to process')
+    count_merges.add_argument('source', help='Source branch name')
+
     subparsers.add_parser('list-sources', help='List tracked source branches')
 
     next_merges = subparsers.add_parser('next-merges',
diff --git a/tools/pickman/control.py b/tools/pickman/control.py
index a6c371f0500..446697a0d5a 100644
--- a/tools/pickman/control.py
+++ b/tools/pickman/control.py
@@ -298,6 +298,41 @@  def do_next_merges(args, dbs):
     return 0
 
 
+def do_count_merges(args, dbs):
+    """Count total remaining merges to be applied from a source
+
+    Args:
+        args (Namespace): Parsed arguments with 'source' attribute
+        dbs (Database): Database instance
+
+    Returns:
+        int: 0 on success, 1 if source not found
+    """
+    source = args.source
+
+    # Get the last cherry-picked commit from database
+    last_commit = dbs.source_get(source)
+
+    if not last_commit:
+        tout.error(f"Source '{source}' not found in database")
+        return 1
+
+    # Count merge commits on the first-parent chain
+    fp_output = run_git([
+        'log', '--first-parent', '--merges', '--oneline',
+        f'{last_commit}..{source}'
+    ])
+
+    if not fp_output:
+        tout.info('0 merges remaining')
+        return 0
+
+    count = len([line for line in fp_output.split('\n') if line])
+    tout.info(f'{count} merges remaining from {source}')
+
+    return 0
+
+
 HISTORY_FILE = '.pickman-history'
 
 
@@ -907,6 +942,7 @@  COMMANDS = {
     'apply': do_apply,
     'commit-source': do_commit_source,
     'compare': do_compare,
+    'count-merges': do_count_merges,
     'list-sources': do_list_sources,
     'next-merges': do_next_merges,
     'next-set': do_next_set,
diff --git a/tools/pickman/ftest.py b/tools/pickman/ftest.py
index 3fbb4e20dd7..3e898eff188 100644
--- a/tools/pickman/ftest.py
+++ b/tools/pickman/ftest.py
@@ -1090,6 +1090,89 @@  class TestNextMerges(unittest.TestCase):
         self.assertIn('No merges remaining', stdout.getvalue())
 
 
+class TestCountMerges(unittest.TestCase):
+    """Tests for count-merges 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()
+        command.TEST_RESULT = None
+
+    def test_count_merges(self):
+        """Test count-merges shows total remaining"""
+        # Add source to database
+        with terminal.capture():
+            dbs = database.Database(self.db_path)
+            dbs.start()
+            dbs.source_set('us/next', 'abc123')
+            dbs.commit()
+            dbs.close()
+
+        database.Database.instances.clear()
+
+        # Mock git log with merge commits (oneline format)
+        log_output = (
+            'aaa111a Merge branch feature-1\n'
+            'bbb222b Merge branch feature-2\n'
+            'ccc333c Merge branch feature-3\n'
+        )
+        command.TEST_RESULT = command.CommandResult(stdout=log_output)
+
+        args = argparse.Namespace(cmd='count-merges', source='us/next')
+        with terminal.capture() as (stdout, _):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 0)
+        self.assertIn('3 merges remaining from us/next', stdout.getvalue())
+
+    def test_count_merges_none(self):
+        """Test count-merges with no merges remaining"""
+        # Add source to database
+        with terminal.capture():
+            dbs = database.Database(self.db_path)
+            dbs.start()
+            dbs.source_set('us/next', 'abc123')
+            dbs.commit()
+            dbs.close()
+
+        database.Database.instances.clear()
+
+        command.TEST_RESULT = command.CommandResult(stdout='')
+
+        args = argparse.Namespace(cmd='count-merges', source='us/next')
+        with terminal.capture() as (stdout, _):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 0)
+        self.assertIn('0 merges remaining', stdout.getvalue())
+
+    def test_count_merges_source_not_found(self):
+        """Test count-merges with unknown source"""
+        # Create empty database
+        with terminal.capture():
+            dbs = database.Database(self.db_path)
+            dbs.start()
+            dbs.close()
+
+        database.Database.instances.clear()
+
+        args = argparse.Namespace(cmd='count-merges', source='unknown')
+        with terminal.capture() as (_, stderr):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 1)
+        self.assertIn("Source 'unknown' not found", stderr.getvalue())
+
+
 class TestGetNextCommits(unittest.TestCase):
     """Tests for get_next_commits function."""