[Concept,13/24] pickman: Add next-merges command

Message ID 20251217022823.392557-14-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 a new next-merges command that shows the next N merges to be applied
from a source branch, useful for seeing what's coming up.

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 |   6 +++
 tools/pickman/control.py  |  51 +++++++++++++++++++
 tools/pickman/ftest.py    | 101 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 165 insertions(+)
  

Patch

diff --git a/tools/pickman/README.rst b/tools/pickman/README.rst
index b469470138b..a382c98eac0 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 show the next N merges that will be applied::
+
+    ./tools/pickman/pickman next-merges us/next
+
+This shows the upcoming merge commits on the first-parent chain, useful for
+seeing what's coming up. Use ``-c`` to specify the count (default 10).
+
 To apply the next set of commits using a Claude agent::
 
     ./tools/pickman/pickman apply us/next
diff --git a/tools/pickman/__main__.py b/tools/pickman/__main__.py
index 2d2366c1b80..4a60ed7eedc 100755
--- a/tools/pickman/__main__.py
+++ b/tools/pickman/__main__.py
@@ -56,6 +56,12 @@  def parse_args(argv):
     subparsers.add_parser('compare', help='Compare branches')
     subparsers.add_parser('list-sources', help='List tracked source branches')
 
+    next_merges = subparsers.add_parser('next-merges',
+                                         help='Show next N merges to be applied')
+    next_merges.add_argument('source', help='Source branch name')
+    next_merges.add_argument('-c', '--count', type=int, default=10,
+                             help='Number of merges to show (default: 10)')
+
     next_set = subparsers.add_parser('next-set',
                                      help='Show next set of commits to cherry-pick')
     next_set.add_argument('source', help='Source branch name')
diff --git a/tools/pickman/control.py b/tools/pickman/control.py
index 892100f3479..7ac0fffc67a 100644
--- a/tools/pickman/control.py
+++ b/tools/pickman/control.py
@@ -248,6 +248,56 @@  def do_next_set(args, dbs):
     return 0
 
 
+def do_next_merges(args, dbs):
+    """Show the next N merges to be applied from a source
+
+    Args:
+        args (Namespace): Parsed arguments with 'source' and 'count' attributes
+        dbs (Database): Database instance
+
+    Returns:
+        int: 0 on success, 1 if source not found
+    """
+    source = args.source
+    count = args.count
+
+    # 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
+
+    # Find merge commits on the first-parent chain
+    out = run_git([
+        'log', '--reverse', '--first-parent', '--merges',
+        '--format=%H|%h|%s',
+        f'{last_commit}..{source}'
+    ])
+
+    if not out:
+        tout.info('No merges remaining')
+        return 0
+
+    merges = []
+    for line in out.split('\n'):
+        if not line:
+            continue
+        parts = line.split('|', 2)
+        commit_hash = parts[0]
+        short_hash = parts[1]
+        subject = parts[2] if len(parts) > 2 else ''
+        merges.append((commit_hash, short_hash, subject))
+        if len(merges) >= count:
+            break
+
+    tout.info(f'Next {len(merges)} merges from {source}:')
+    for i, (_, short_hash, subject) in enumerate(merges, 1):
+        tout.info(f'  {i}. {short_hash} {subject}')
+
+    return 0
+
+
 HISTORY_FILE = '.pickman-history'
 
 
@@ -786,6 +836,7 @@  COMMANDS = {
     'commit-source': do_commit_source,
     'compare': do_compare,
     'list-sources': do_list_sources,
+    'next-merges': do_next_merges,
     'next-set': do_next_set,
     'poll': do_poll,
     'review': do_review,
diff --git a/tools/pickman/ftest.py b/tools/pickman/ftest.py
index 784e8dd5d1b..d452fc823ca 100644
--- a/tools/pickman/ftest.py
+++ b/tools/pickman/ftest.py
@@ -904,6 +904,107 @@  class TestNextSet(unittest.TestCase):
         self.assertIn('bbb222b Second commit', output)
 
 
+class TestNextMerges(unittest.TestCase):
+    """Tests for next-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_next_merges(self):
+        """Test next-merges shows upcoming merges"""
+        # 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
+        log_output = (
+            'aaa111|aaa111a|Merge branch feature-1\n'
+            'bbb222|bbb222b|Merge branch feature-2\n'
+            'ccc333|ccc333c|Merge branch feature-3\n'
+        )
+        command.TEST_RESULT = command.CommandResult(stdout=log_output)
+
+        args = argparse.Namespace(cmd='next-merges', source='us/next', count=10)
+        with terminal.capture() as (stdout, _):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 0)
+        output = stdout.getvalue()
+        self.assertIn('Next 3 merges from us/next:', output)
+        self.assertIn('1. aaa111a Merge branch feature-1', output)
+        self.assertIn('2. bbb222b Merge branch feature-2', output)
+        self.assertIn('3. ccc333c Merge branch feature-3', output)
+
+    def test_next_merges_with_count(self):
+        """Test next-merges respects count parameter"""
+        # 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
+        log_output = (
+            'aaa111|aaa111a|Merge branch feature-1\n'
+            'bbb222|bbb222b|Merge branch feature-2\n'
+            'ccc333|ccc333c|Merge branch feature-3\n'
+        )
+        command.TEST_RESULT = command.CommandResult(stdout=log_output)
+
+        args = argparse.Namespace(cmd='next-merges', source='us/next', count=2)
+        with terminal.capture() as (stdout, _):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 0)
+        output = stdout.getvalue()
+        self.assertIn('Next 2 merges from us/next:', output)
+        self.assertIn('1. aaa111a', output)
+        self.assertIn('2. bbb222b', output)
+        self.assertNotIn('3. ccc333c', output)
+
+    def test_next_merges_no_merges(self):
+        """Test next-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='next-merges', source='us/next', count=10)
+        with terminal.capture() as (stdout, _):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 0)
+        self.assertIn('No merges remaining', stdout.getvalue())
+
+
 class TestGetNextCommits(unittest.TestCase):
     """Tests for get_next_commits function."""