[Concept,18/32] patman: Add per-upstream patchwork URL

Message ID 20260226200106.1727176-19-sjg@u-boot.org
State New
Headers
Series patman: Add multi-upstream support |

Commit Message

Simon Glass Feb. 26, 2026, 8 p.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Currently patman uses a single global --patchwork-url flag. Add a
patchwork_url field to each upstream so the URL can be looked up
automatically from the database.

Update upstream_add() and upstream_get_dict() to handle the new
field and add upstream_get_patchwork_url() for direct lookup.

Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 tools/patman/cmdline.py      |  6 +++-
 tools/patman/control.py      | 28 +++++++++++++++----
 tools/patman/cseries.py      | 27 ++++++++++++++----
 tools/patman/database.py     | 34 +++++++++++++++++++----
 tools/patman/test_cseries.py | 53 ++++++++++++++++++++++++++++++------
 5 files changed, 122 insertions(+), 26 deletions(-)
  

Patch

diff --git a/tools/patman/cmdline.py b/tools/patman/cmdline.py
index 090d9845ec1..f24d0aacdff 100644
--- a/tools/patman/cmdline.py
+++ b/tools/patman/cmdline.py
@@ -406,7 +406,7 @@  def add_upstream_subparser(subparsers):
     upstream = subparsers.add_parser('upstream', aliases=ALIASES['upstream'],
                                      help='Manage upstream destinations')
     upstream.defaults_cmds = [
-        ['add', 'us', 'http://fred', 'U-Boot'],
+        ['add', 'us', 'http://fred', '-p', 'http://pw', 'U-Boot'],
         ['delete', 'us'],
     ]
     upstream_subparsers = upstream.add_subparsers(dest='subcmd')
@@ -416,6 +416,10 @@  def add_upstream_subparser(subparsers):
     uadd.add_argument(
         'url', help='URL to use for this upstream, e.g. '
                     "'https://gitlab.denx.de/u-boot/u-boot.git'")
+    uadd.add_argument(
+        '-p', '--patchwork-url',
+        help='URL of patchwork server for this upstream, e.g. '
+             "'https://patchwork.ozlabs.org'")
     uadd.add_argument(
         'project_name', nargs='?',
         help="Patchwork project name, e.g. 'U-Boot'")
diff --git a/tools/patman/control.py b/tools/patman/control.py
index c65fd3d5f1f..e1f52300a17 100644
--- a/tools/patman/control.py
+++ b/tools/patman/control.py
@@ -134,8 +134,18 @@  def do_series(args, test_db=None, pwork=None, cser=None):
         cser.open_database()
         if args.subcmd in needs_patchwork:
             if not pwork:
-                pwork = Patchwork(args.patchwork_url)
                 ups = cser.get_series_upstream(args.series)
+                pw_url = None
+                if ups:
+                    pw_url = cser.db.upstream_get_patchwork_url(ups)
+                if not pw_url:
+                    pw_url = args.patchwork_url
+                if not pw_url:
+                    raise ValueError(
+                        'No patchwork URL found for upstream '
+                        f"'{ups}'; use 'patman upstream add' with "
+                        '-p or pass --patchwork-url')
+                pwork = Patchwork(pw_url)
                 proj = cser.project_get(ups)
                 if not proj:
                     proj = cser.project_get()
@@ -239,11 +249,9 @@  def upstream(args, test_db=None):
     try:
         cser.open_database()
         if args.subcmd == 'add':
-            pwork = None
-            if args.project_name:
-                pwork = Patchwork(args.patchwork_url)
             cser.upstream_add(args.remote_name, args.url,
-                              args.project_name, pwork)
+                              args.project_name,
+                              patchwork_url=args.patchwork_url)
         elif args.subcmd == 'default':
             if args.unset:
                 cser.upstream_set_default(None)
@@ -279,7 +287,15 @@  def patchwork(args, test_db=None, pwork=None):
             if not args.remote:
                 raise ValueError('Please specify the remote name')
             if not pwork:
-                pwork = Patchwork(args.patchwork_url)
+                pw_url = cser.db.upstream_get_patchwork_url(args.remote)
+                if not pw_url:
+                    pw_url = args.patchwork_url
+                if not pw_url:
+                    raise ValueError(
+                        f"No patchwork URL for remote '{args.remote}'"
+                        "; use 'patman upstream add' with -p"
+                        ' or pass --patchwork-url')
+                pwork = Patchwork(pw_url)
             cser.project_set(pwork, args.project_name,
                              ups=args.remote)
         elif args.subcmd == 'get-project':
diff --git a/tools/patman/cseries.py b/tools/patman/cseries.py
index c48267964a1..612ccfda7dc 100644
--- a/tools/patman/cseries.py
+++ b/tools/patman/cseries.py
@@ -16,6 +16,7 @@  from u_boot_pylib import terminal
 from u_boot_pylib import tout
 
 from patman import patchstream
+from patman.patchwork import Patchwork
 from patman import cser_helper
 from patman.cser_helper import AUTOLINK, oid
 from patman import send
@@ -1145,7 +1146,8 @@  class Cseries(cser_helper.CseriesHelper):
                 self.rollback()
                 tout.info('Dry run completed')
 
-    def upstream_add(self, name, url, project=None, pwork=None):
+    def upstream_add(self, name, url, project=None, pwork=None,
+                     patchwork_url=None):
         """Add a new upstream tree
 
         Args:
@@ -1154,12 +1156,21 @@  class Cseries(cser_helper.CseriesHelper):
             project (str or None): Patchwork project name to associate
             pwork (Patchwork or None): Patchwork object for looking up
                 the project
+            patchwork_url (str or None): URL of the patchwork server for
+                this upstream
         """
-        self.db.upstream_add(name, url)
+        self.db.upstream_add(name, url, patchwork_url)
         if project:
+            if not pwork:
+                if not patchwork_url:
+                    raise ValueError(
+                        'Patchwork URL is required when setting a project')
+                pwork = Patchwork(patchwork_url)
             self.project_set(pwork, project, ups=name, quiet=True)
         self.commit()
         msg = f"Added upstream '{name}' ({url})"
+        if patchwork_url:
+            msg += f" patchwork '{patchwork_url}'"
         if project:
             msg += f" project '{project}'"
         tout.notice(msg)
@@ -1167,14 +1178,20 @@  class Cseries(cser_helper.CseriesHelper):
     def upstream_list(self):
         """List the upstream repos
 
-        Shows a list of the repos, obtained from the database
+        Shows a list of the repos, obtained from the database, along with
+        any associated patchwork project
         """
         udict = self.get_upstream_dict()
 
         for name, items in udict.items():
-            url, is_default = items
+            url, is_default, patchwork_url = items
             default = 'default' if is_default else ''
-            print(f'{name:15.15} {default:8} {url}')
+            proj = self.db.patchwork_get(name)
+            proj_name = proj[0] if proj else ''
+            line = f'{name:10.10} {default:8} {proj_name:20} {url}'
+            if patchwork_url:
+                line += f'  pw:{patchwork_url}'
+            print(line)
 
     def upstream_set_default(self, name):
         """Set the default upstream target
diff --git a/tools/patman/database.py b/tools/patman/database.py
index 2f21e62605f..f7ab2d84877 100644
--- a/tools/patman/database.py
+++ b/tools/patman/database.py
@@ -791,19 +791,21 @@  class Database:  # pylint:disable=R0904
 
     # upstream functions
 
-    def upstream_add(self, name, url):
+    def upstream_add(self, name, url, patchwork_url=None):
         """Add a new upstream record
 
         Args:
             name (str): Name of the tree
             url (str): URL for the tree
+            patchwork_url (str or None): URL of the patchwork server
 
         Raises:
             ValueError if the name already exists in the database
         """
         try:
             self.execute(
-                'INSERT INTO upstream (name, url) VALUES (?, ?)', (name, url))
+                'INSERT INTO upstream (name, url, patchwork_url) '
+                'VALUES (?, ?, ?)', (name, url, patchwork_url))
         except sqlite3.IntegrityError as exc:
             if 'UNIQUE constraint failed: upstream.name' in str(exc):
                 raise ValueError(f"Upstream '{name}' already exists") from exc
@@ -853,18 +855,38 @@  class Database:  # pylint:disable=R0904
             self.rollback()
             raise ValueError(f"No such upstream '{name}'")
 
+    def upstream_get_patchwork_url(self, name):
+        """Get the patchwork URL for an upstream
+
+        Args:
+            name (str): Upstream name
+
+        Return:
+            str or None: Patchwork URL, or None if not set
+        """
+        res = self.execute(
+            'SELECT patchwork_url FROM upstream WHERE name = ?', (name,))
+        rec = res.fetchone()
+        if rec:
+            return rec[0]
+        return None
+
     def upstream_get_dict(self):
         """Get a list of upstream entries from the database
 
         Return:
             OrderedDict:
                 key (str): upstream name
-                value (str): url
+                value: tuple:
+                    str: url
+                    bool: is_default
+                    str or None: patchwork_url
         """
-        res = self.execute('SELECT name, url, is_default FROM upstream')
+        res = self.execute(
+            'SELECT name, url, is_default, patchwork_url FROM upstream')
         udict = OrderedDict()
-        for name, url, is_default in res.fetchall():
-            udict[name] = url, is_default
+        for name, url, is_default, patchwork_url in res.fetchall():
+            udict[name] = url, is_default, patchwork_url
         return udict
 
     # patchwork functions
diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py
index 083727889f8..b14c6c22ddc 100644
--- a/tools/patman/test_cseries.py
+++ b/tools/patman/test_cseries.py
@@ -1730,14 +1730,14 @@  Tested-by: Mary Smith <msmith@wibble.com>   # yak
             cser.upstream_add('us', 'https://one')
         ulist = cser.get_upstream_dict()
         self.assertEqual(1, len(ulist))
-        self.assertEqual(('https://one', None), ulist['us'])
+        self.assertEqual(('https://one', None, None), ulist['us'])
 
         with terminal.capture():
             cser.upstream_add('ci', 'git@two')
         ulist = cser.get_upstream_dict()
         self.assertEqual(2, len(ulist))
-        self.assertEqual(('https://one', None), ulist['us'])
-        self.assertEqual(('git@two', None), ulist['ci'])
+        self.assertEqual(('https://one', None, None), ulist['us'])
+        self.assertEqual(('git@two', None, None), ulist['ci'])
 
         # Try to add a duplicate
         with self.assertRaises(ValueError) as exc:
@@ -1748,8 +1748,39 @@  Tested-by: Mary Smith <msmith@wibble.com>   # yak
             cser.upstream_list()
         lines = out.getvalue().splitlines()
         self.assertEqual(2, len(lines))
-        self.assertEqual('us                       https://one', lines[0])
-        self.assertEqual('ci                       git@two', lines[1])
+        self.assertEqual(
+            'us                                       https://one',
+            lines[0])
+        self.assertEqual(
+            'ci                                       git@two',
+            lines[1])
+
+    def test_upstream_add_patchwork_url(self):
+        """Test adding an upstream with a patchwork URL"""
+        cser = self.get_cser()
+
+        with terminal.capture():
+            cser.upstream_add('us', 'https://one',
+                              patchwork_url='https://pw.example.com')
+        ulist = cser.get_upstream_dict()
+        self.assertEqual(1, len(ulist))
+        self.assertEqual(
+            ('https://one', None, 'https://pw.example.com'), ulist['us'])
+
+        # Check that the patchwork URL shows in the list
+        with terminal.capture() as (out, _):
+            cser.upstream_list()
+        lines = out.getvalue().splitlines()
+        self.assertEqual(1, len(lines))
+        self.assertIn('pw:https://pw.example.com', lines[0])
+
+        # Check database lookup
+        pw_url = cser.db.upstream_get_patchwork_url('us')
+        self.assertEqual('https://pw.example.com', pw_url)
+
+        # Non-existent upstream returns None
+        pw_url = cser.db.upstream_get_patchwork_url('nonexistent')
+        self.assertIsNone(pw_url)
 
     def test_upstream_add_cmdline(self):
         """Test adding an upsream with cmdline"""
@@ -1760,7 +1791,9 @@  Tested-by: Mary Smith <msmith@wibble.com>   # yak
             self.run_args('upstream', 'list')
         lines = out.getvalue().splitlines()
         self.assertEqual(1, len(lines))
-        self.assertEqual('us                       https://one', lines[0])
+        self.assertEqual(
+            'us                                       https://one',
+            lines[0])
 
     def test_upstream_default(self):
         """Operation of the default upstream"""
@@ -1791,8 +1824,12 @@  Tested-by: Mary Smith <msmith@wibble.com>   # yak
             cser.upstream_list()
         lines = out.getvalue().splitlines()
         self.assertEqual(2, len(lines))
-        self.assertEqual('us                       https://one', lines[0])
-        self.assertEqual('ci              default  git@two', lines[1])
+        self.assertEqual(
+            'us                                       https://one',
+            lines[0])
+        self.assertEqual(
+            'ci         default                       git@two',
+            lines[1])
 
         cser.upstream_set_default(None)
         self.assertIsNone(cser.upstream_get_default())