[Concept,12/17] pickman: Add commit-source command

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

Commit Message

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

Add a command to update the database with the last cherry-picked commit.
This allows reviewing the cherry-picked branch before committing to the
database.

Usage:
  pickman commit-source us/next <commit-hash>

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    | 65 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 113 insertions(+)
  

Patch

diff --git a/tools/pickman/README.rst b/tools/pickman/README.rst
index ab37763a918..d15b15f3331 100644
--- a/tools/pickman/README.rst
+++ b/tools/pickman/README.rst
@@ -73,6 +73,13 @@  On successful cherry-pick, an entry is appended to ``.pickman-history`` with:
 This file is committed automatically and included in the MR description when
 using ``-p``.
 
+After successfully applying commits, update the database to record progress::
+
+    ./tools/pickman/pickman commit-source us/next <commit-hash>
+
+This updates the last cherry-picked commit for the source branch, so subsequent
+``next-set`` and ``apply`` commands will start from the new position.
+
 Requirements
 ------------
 
diff --git a/tools/pickman/__main__.py b/tools/pickman/__main__.py
index ac029a38382..1693af991ac 100755
--- a/tools/pickman/__main__.py
+++ b/tools/pickman/__main__.py
@@ -45,6 +45,11 @@  def parse_args(argv):
     apply_cmd.add_argument('-t', '--target', default='master',
                            help='Target branch for MR (default: master)')
 
+    commit_src = subparsers.add_parser('commit-source',
+                                        help='Update database with last commit')
+    commit_src.add_argument('source', help='Source branch name')
+    commit_src.add_argument('commit', help='Commit hash to record')
+
     subparsers.add_parser('compare', help='Compare branches')
     subparsers.add_parser('list-sources', help='List tracked source branches')
 
diff --git a/tools/pickman/control.py b/tools/pickman/control.py
index a482de85b00..fe840ee2740 100644
--- a/tools/pickman/control.py
+++ b/tools/pickman/control.py
@@ -401,6 +401,41 @@  def do_apply(args, dbs):  # pylint: disable=too-many-locals
     return 0 if success else 1
 
 
+def do_commit_source(args, dbs):
+    """Update the database with the last cherry-picked commit
+
+    Args:
+        args (Namespace): Parsed arguments with 'source' and 'commit' attributes
+        dbs (Database): Database instance
+
+    Returns:
+        int: 0 on success, 1 on failure
+    """
+    source = args.source
+    commit = args.commit
+
+    # Resolve the commit to a full hash
+    try:
+        full_hash = run_git(['rev-parse', commit])
+    except Exception:  # pylint: disable=broad-except
+        tout.error(f"Could not resolve commit '{commit}'")
+        return 1
+
+    old_commit = dbs.source_get(source)
+    if not old_commit:
+        tout.error(f"Source '{source}' not found in database")
+        return 1
+
+    dbs.source_set(source, full_hash)
+    dbs.commit()
+
+    short_old = old_commit[:12]
+    short_new = full_hash[:12]
+    tout.info(f"Updated '{source}': {short_old} -> {short_new}")
+
+    return 0
+
+
 
 def do_test(args, dbs):  # pylint: disable=unused-argument
     """Run tests for this module.
@@ -424,6 +459,7 @@  def do_test(args, dbs):  # pylint: disable=unused-argument
 COMMANDS = {
     'add-source': do_add_source,
     'apply': do_apply,
+    'commit-source': do_commit_source,
     'compare': do_compare,
     'list-sources': do_list_sources,
     'next-set': do_next_set,
diff --git a/tools/pickman/ftest.py b/tools/pickman/ftest.py
index 4f5c90980c6..9b445173b3b 100644
--- a/tools/pickman/ftest.py
+++ b/tools/pickman/ftest.py
@@ -127,6 +127,13 @@  class TestParseArgs(unittest.TestCase):
         self.assertEqual(args.source, 'us/next')
         self.assertEqual(args.branch, 'my-branch')
 
+    def test_parse_commit_source(self):
+        """Test parsing commit-source command."""
+        args = pickman.parse_args(['commit-source', 'us/next', 'abc123'])
+        self.assertEqual(args.cmd, 'commit-source')
+        self.assertEqual(args.source, 'us/next')
+        self.assertEqual(args.commit, 'abc123')
+
     def test_parse_compare(self):
         """Test parsing compare command."""
         args = pickman.parse_args(['compare'])
@@ -942,6 +949,64 @@  class TestApply(unittest.TestCase):
         self.assertIn('No new commits to cherry-pick', stdout.getvalue())
 
 
+class TestCommitSource(unittest.TestCase):
+    """Tests for commit-source 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_commit_source_not_found(self):
+        """Test commit-source with unknown source."""
+        with terminal.capture():
+            dbs = database.Database(self.db_path)
+            dbs.start()
+            dbs.close()
+
+        database.Database.instances.clear()
+        command.TEST_RESULT = command.CommandResult(stdout='fullhash123')
+
+        args = argparse.Namespace(cmd='commit-source', source='unknown',
+                                  commit='abc123')
+        with terminal.capture() as (_, stderr):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 1)
+        self.assertIn("Source 'unknown' not found", stderr.getvalue())
+
+    def test_commit_source_success(self):
+        """Test commit-source updates database."""
+        with terminal.capture():
+            dbs = database.Database(self.db_path)
+            dbs.start()
+            dbs.source_set('us/next', 'oldcommit12345')
+            dbs.commit()
+            dbs.close()
+
+        database.Database.instances.clear()
+        command.TEST_RESULT = command.CommandResult(stdout='newcommit67890')
+
+        args = argparse.Namespace(cmd='commit-source', source='us/next',
+                                  commit='abc123')
+        with terminal.capture() as (stdout, _):
+            ret = control.do_pickman(args)
+        self.assertEqual(ret, 0)
+        self.assertIn('oldcommit123', stdout.getvalue())
+        self.assertIn('newcommit678', stdout.getvalue())
+
+
 class TestParseUrl(unittest.TestCase):
     """Tests for parse_url function."""