@@ -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
@@ -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',
@@ -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,
@@ -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."""