@@ -11,9 +11,17 @@ Pickman is a tool to help manage cherry-picking commits between branches.
Usage
-----
+To add a source branch to track::
+
+ ./tools/pickman/pickman add-source us/next
+
+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 compare branches and show commits that need to be cherry-picked::
- ./tools/pickman/pickman
+ ./tools/pickman/pickman compare
This shows:
@@ -21,10 +29,18 @@ This shows:
master branch (ci/master)
- The last common commit between the two branches
+Database
+--------
+
+Pickman uses a sqlite3 database (``.pickman.db``) to track state:
+
+- **source table**: Tracks source branches and the last commit that was
+ cherry-picked into master
+
Configuration
-------------
-The branches to compare are configured as constants at the top of the script:
+The branches to compare are configured as constants in control.py:
- ``BRANCH_MASTER``: The main branch to compare against (default: ci/master)
- ``BRANCH_SOURCE``: The source branch with commits to cherry-pick
@@ -35,5 +51,8 @@ Testing
To run the functional tests::
- cd tools/pickman
- python3 -m pytest ftest.py -v
+ ./tools/pickman/pickman test
+
+Or using pytest::
+
+ python3 -m pytest tools/pickman/ftest.py -v
@@ -30,6 +30,10 @@ def parse_args(argv):
parser = argparse.ArgumentParser(description='Check commit differences')
subparsers = parser.add_subparsers(dest='cmd', required=True)
+ add_source = subparsers.add_parser('add-source',
+ help='Add a source branch to track')
+ add_source.add_argument('source', help='Source branch name')
+
subparsers.add_parser('compare', help='Compare branches')
subparsers.add_parser('test', help='Run tests')
@@ -15,10 +15,14 @@ our_path = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, os.path.join(our_path, '..'))
# pylint: disable=wrong-import-position,import-error
+from pickman import database
from pickman import ftest
from u_boot_pylib import command
from u_boot_pylib import tout
+# Default database filename
+DB_FNAME = '.pickman.db'
+
# Branch names to compare
BRANCH_MASTER = 'ci/master'
BRANCH_SOURCE = 'us/next'
@@ -56,11 +60,44 @@ def compare_branches(master, source):
return count, Commit(full_hash, short_hash, subject, date)
-def do_compare(args): # pylint: disable=unused-argument
+def do_add_source(args, dbs):
+ """Add a source branch to the database
+
+ Finds the merge-base commit between master and source and stores it.
+
+ Args:
+ args (Namespace): Parsed arguments with 'source' attribute
+ dbs (Database): Database instance
+
+ Returns:
+ int: 0 on success
+ """
+ source = args.source
+
+ # Find the merge base commit
+ base_hash = run_git(['merge-base', BRANCH_MASTER, source])
+
+ # Get commit details for display
+ info = run_git(['log', '-1', '--format=%h%n%s', base_hash])
+ short_hash, subject = info.split('\n')
+
+ # Store in database
+ dbs.source_set(source, base_hash)
+ dbs.commit()
+
+ tout.info(f"Added source '{source}' with base commit:")
+ tout.info(f' Hash: {short_hash}')
+ tout.info(f' Subject: {subject}')
+
+ return 0
+
+
+def do_compare(args, dbs): # pylint: disable=unused-argument
"""Compare branches and print results.
Args:
args (Namespace): Parsed arguments
+ dbs (Database): Database instance
"""
count, base = compare_branches(BRANCH_MASTER, BRANCH_SOURCE)
@@ -74,11 +111,12 @@ def do_compare(args): # pylint: disable=unused-argument
return 0
-def do_test(args): # pylint: disable=unused-argument
+def do_test(args, dbs): # pylint: disable=unused-argument
"""Run tests for this module.
Args:
args (Namespace): Parsed arguments
+ dbs (Database): Database instance
Returns:
int: 0 if tests passed, 1 otherwise
@@ -91,6 +129,14 @@ def do_test(args): # pylint: disable=unused-argument
return 0 if result.wasSuccessful() else 1
+# Command dispatch table
+COMMANDS = {
+ 'add-source': do_add_source,
+ 'compare': do_compare,
+ 'test': do_test,
+}
+
+
def do_pickman(args):
"""Main entry point for pickman commands.
@@ -102,9 +148,12 @@ def do_pickman(args):
"""
tout.init(tout.INFO)
- if args.cmd == 'compare':
- return do_compare(args)
- if args.cmd == 'test':
- return do_test(args)
-
+ handler = COMMANDS.get(args.cmd)
+ if handler:
+ dbs = database.Database(DB_FNAME)
+ dbs.start()
+ try:
+ return handler(args, dbs)
+ finally:
+ dbs.close()
return 1
@@ -5,6 +5,7 @@
#
"""Tests for pickman."""
+import argparse
import os
import sys
import tempfile
@@ -104,6 +105,12 @@ class TestCompareBranches(unittest.TestCase):
class TestParseArgs(unittest.TestCase):
"""Tests for parse_args function."""
+ def test_parse_add_source(self):
+ """Test parsing add-source command."""
+ args = pickman.parse_args(['add-source', 'us/next'])
+ self.assertEqual(args.cmd, 'add-source')
+ self.assertEqual(args.source, 'us/next')
+
def test_parse_compare(self):
"""Test parsing compare command."""
args = pickman.parse_args(['compare'])
@@ -124,6 +131,48 @@ class TestParseArgs(unittest.TestCase):
class TestMain(unittest.TestCase):
"""Tests for main function."""
+ def test_add_source(self):
+ """Test add-source command"""
+ results = iter([
+ 'abc123def456', # merge-base
+ 'abc123d\nTest subject', # log
+ ])
+
+ def handle_command(**_):
+ return command.CommandResult(stdout=next(results))
+
+ # Use a temp database file
+ fd, db_path = tempfile.mkstemp(suffix='.db')
+ os.close(fd)
+ os.unlink(db_path)
+ old_db_fname = control.DB_FNAME
+ control.DB_FNAME = db_path
+ database.Database.instances.clear()
+
+ command.TEST_RESULT = handle_command
+ try:
+ args = argparse.Namespace(cmd='add-source', source='us/next')
+ with terminal.capture() as (stdout, _):
+ ret = control.do_pickman(args)
+ self.assertEqual(ret, 0)
+ output = stdout.getvalue()
+ self.assertIn("Added source 'us/next' with base commit:", output)
+ self.assertIn('Hash: abc123d', output)
+ self.assertIn('Subject: Test subject', output)
+
+ # Verify database was updated
+ database.Database.instances.clear()
+ dbs = database.Database(db_path)
+ dbs.start()
+ self.assertEqual(dbs.source_get('us/next'), 'abc123def456')
+ dbs.close()
+ finally:
+ command.TEST_RESULT = None
+ control.DB_FNAME = old_db_fname
+ if os.path.exists(db_path):
+ os.unlink(db_path)
+ database.Database.instances.clear()
+
def test_main_compare(self):
"""Test main with compare command."""
results = iter([
@@ -135,12 +184,23 @@ class TestMain(unittest.TestCase):
def handle_command(**_):
return command.CommandResult(stdout=next(results))
+ # Use a temp database file
+ fd, db_path = tempfile.mkstemp(suffix='.db')
+ os.close(fd)
+ os.unlink(db_path)
+ old_db_fname = control.DB_FNAME
+ control.DB_FNAME = db_path
+ database.Database.instances.clear()
+
command.TEST_RESULT = handle_command
try:
with terminal.capture() as (stdout, _):
ret = pickman.main(['compare'])
self.assertEqual(ret, 0)
- lines = iter(stdout.getvalue().splitlines())
+ # Filter out database migration messages
+ output_lines = [l for l in stdout.getvalue().splitlines()
+ if not l.startswith(('Update database', 'Creating'))]
+ lines = iter(output_lines)
self.assertEqual('Commits in us/next not in ci/master: 10',
next(lines))
self.assertEqual('', next(lines))
@@ -152,6 +212,10 @@ class TestMain(unittest.TestCase):
self.assertRaises(StopIteration, next, lines)
finally:
command.TEST_RESULT = None
+ control.DB_FNAME = old_db_fname
+ if os.path.exists(db_path):
+ os.unlink(db_path)
+ database.Database.instances.clear()
class TestDatabase(unittest.TestCase):