[Concept,24/32] patman: Add an 'upstream set' command to update settings

Message ID 20260226200106.1727176-25-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>

Add a 'set' subcommand for upstream that allows updating individual
fields on an existing upstream without having to delete and re-add it.

Supports -p/--patchwork-url, -I/--identity, -t/--series-to,
-m/--no-maintainers, --maintainers, --no-tags and --tags.

For example:
  patman upstream set us -I chromium -t concept -m

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

 tools/patman/cmdline.py      | 25 ++++++++++++++++++
 tools/patman/control.py      | 19 ++++++++++++++
 tools/patman/cseries.py      | 14 ++++++++++
 tools/patman/database.py     | 34 ++++++++++++++++++++++++
 tools/patman/test_cseries.py | 50 ++++++++++++++++++++++++++++++++++++
 5 files changed, 142 insertions(+)
  

Patch

diff --git a/tools/patman/cmdline.py b/tools/patman/cmdline.py
index 6141f106fde..5bf917e4712 100644
--- a/tools/patman/cmdline.py
+++ b/tools/patman/cmdline.py
@@ -411,6 +411,7 @@  def add_upstream_subparser(subparsers):
     upstream.defaults_cmds = [
         ['add', 'us', 'http://fred', '-p', 'http://pw', 'U-Boot'],
         ['delete', 'us'],
+        ['set', 'us'],
     ]
     upstream_subparsers = upstream.add_subparsers(dest='subcmd')
     uadd = upstream_subparsers.add_parser('add')
@@ -443,6 +444,30 @@  def add_upstream_subparser(subparsers):
         'remote_name',
         help="Git remote name used for this upstream, e.g. 'us'")
     upstream_subparsers.add_parser('ls', aliases=['list'])
+    uset = upstream_subparsers.add_parser('set')
+    uset.add_argument('remote_name',
+                      help="Git remote name used for this upstream, e.g. 'us'")
+    uset.add_argument(
+        '-p', '--patchwork-url',
+        help='URL of patchwork server for this upstream')
+    uset.add_argument(
+        '-I', '--identity',
+        help="Git sendemail identity to use, e.g. 'chromium'")
+    uset.add_argument(
+        '-t', '--series-to',
+        help="Patman alias for the To address, e.g. 'u-boot'")
+    uset.add_argument(
+        '-m', '--no-maintainers', action='store_true', default=None,
+        help='Skip get_maintainer.pl for this upstream')
+    uset.add_argument(
+        '--maintainers', action='store_true', default=None,
+        help='Enable get_maintainer.pl for this upstream')
+    uset.add_argument(
+        '--no-tags', action='store_true', default=None,
+        help='Skip subject-tag alias processing for this upstream')
+    uset.add_argument(
+        '--tags', action='store_true', default=None,
+        help='Enable subject-tag alias processing for this upstream')
     udef = upstream_subparsers.add_parser('default')
     udef.add_argument('-u', '--unset', action='store_true',
                       help='Unset the default upstream')
diff --git a/tools/patman/control.py b/tools/patman/control.py
index 689fd732dec..cabd2138bb3 100644
--- a/tools/patman/control.py
+++ b/tools/patman/control.py
@@ -267,6 +267,25 @@  def upstream(args, test_db=None):
                 print(result if result else 'unset')
         elif args.subcmd == 'delete':
             cser.upstream_delete(args.remote_name)
+        elif args.subcmd == 'set':
+            kwargs = {}
+            if args.patchwork_url is not None:
+                kwargs['patchwork_url'] = args.patchwork_url
+            if args.identity is not None:
+                kwargs['identity'] = args.identity
+            if args.series_to is not None:
+                kwargs['series_to'] = args.series_to
+            if args.no_maintainers:
+                kwargs['no_maintainers'] = True
+            elif args.maintainers:
+                kwargs['no_maintainers'] = False
+            if args.no_tags:
+                kwargs['no_tags'] = True
+            elif args.tags:
+                kwargs['no_tags'] = False
+            if not kwargs:
+                raise ValueError('No settings to update')
+            cser.upstream_set(args.remote_name, **kwargs)
         elif args.subcmd == 'ls':
             cser.upstream_list()
         else:
diff --git a/tools/patman/cseries.py b/tools/patman/cseries.py
index 36f0b432073..588e83298bd 100644
--- a/tools/patman/cseries.py
+++ b/tools/patman/cseries.py
@@ -1237,6 +1237,20 @@  class Cseries(cser_helper.CseriesHelper):
                 line += '  no-tags'
             print(line)
 
+    def upstream_set(self, name, **kwargs):
+        """Update settings on an existing upstream
+
+        See Database.upstream_set() for permitted kwargs.
+
+        Args:
+            name (str): Name of the upstream remote to update
+            kwargs: Fields to update
+        """
+        self.db.upstream_set(name, **kwargs)
+        self.commit()
+        parts = [f'{k}={v!r}' for k, v in kwargs.items()]
+        tout.notice(f"Updated upstream '{name}': {', '.join(parts)}")
+
     def upstream_set_default(self, name):
         """Set the default upstream target
 
diff --git a/tools/patman/database.py b/tools/patman/database.py
index 19df3c3bc82..dcd39ea2c69 100644
--- a/tools/patman/database.py
+++ b/tools/patman/database.py
@@ -863,6 +863,40 @@  class Database:  # pylint:disable=R0904
             self.rollback()
             raise ValueError(f"No such upstream '{name}'")
 
+    def upstream_set(self, name, **kwargs):
+        """Update fields on an existing upstream
+
+        Args:
+            name (str): Name of the upstream remote to update
+            kwargs: Fields to update, each being one of:
+                patchwork_url (str): URL of the patchwork server, e.g.
+                    'patchwork.ozlabs.org'
+                identity (str): Git sendemail identity to use when
+                    sending, corresponding to a [sendemail "<identity>"]
+                    section in .gitconfig
+                series_to (str): Mailing-list address to use as the
+                    default To: for this upstream
+                no_maintainers (bool): True to skip
+                    get_maintainer.pl when sending
+                no_tags (bool): True to skip processing of subject
+                    tags (e.g. 'dm:') when sending
+
+        Raises:
+            ValueError: Upstream does not exist or invalid field
+        """
+        valid = {'patchwork_url', 'identity', 'series_to',
+                 'no_maintainers', 'no_tags'}
+        invalid = set(kwargs) - valid
+        if invalid:
+            raise ValueError(f"Invalid upstream field(s): {invalid}")
+        for field, value in kwargs.items():
+            self.execute(
+                f'UPDATE upstream SET {field} = ? WHERE name = ?',
+                (value, name))
+            if self.rowcount() != 1:
+                self.rollback()
+                raise ValueError(f"No such upstream '{name}'")
+
     def upstream_get_patchwork_url(self, name):
         """Get the patchwork URL for an upstream
 
diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py
index 8f56627958e..fcfe6610321 100644
--- a/tools/patman/test_cseries.py
+++ b/tools/patman/test_cseries.py
@@ -1813,6 +1813,56 @@  Tested-by: Mary Smith <msmith@wibble.com>   # yak
             'us                                       https://one',
             lines[0])
 
+    def test_upstream_set(self):
+        """Test updating settings on an existing upstream"""
+        cser = self.get_cser()
+
+        with terminal.capture():
+            cser.upstream_add('us', 'https://one')
+
+        # Set identity and series_to
+        with terminal.capture():
+            cser.upstream_set('us', identity='chromium', series_to='concept')
+        settings = cser.db.upstream_get_send_settings('us')
+        self.assertEqual('chromium', settings[0])
+        self.assertEqual('concept', settings[1])
+
+        # Set boolean flags
+        with terminal.capture():
+            cser.upstream_set('us', no_maintainers=True, no_tags=True)
+        settings = cser.db.upstream_get_send_settings('us')
+        self.assertTrue(settings[2])
+        self.assertTrue(settings[3])
+
+        # Clear boolean flags
+        with terminal.capture():
+            cser.upstream_set('us', no_maintainers=False, no_tags=False)
+        settings = cser.db.upstream_get_send_settings('us')
+        self.assertFalse(settings[2])
+        self.assertFalse(settings[3])
+
+        # Non-existent upstream
+        with self.assertRaises(ValueError) as exc:
+            cser.upstream_set('nonexistent', identity='x')
+        self.assertIn('nonexistent', str(exc.exception))
+
+    def test_upstream_set_cmdline(self):
+        """Test upstream set via the command line"""
+        with terminal.capture():
+            self.run_args('upstream', 'add', 'us', 'https://one')
+
+        with terminal.capture():
+            self.run_args('upstream', 'set', 'us', '-I', 'chromium',
+                          '-t', 'concept', '-m', '--no-tags')
+
+        with terminal.capture() as (out, _):
+            self.run_args('upstream', 'list')
+        line = out.getvalue().strip()
+        self.assertIn('id:chromium', line)
+        self.assertIn('to:concept', line)
+        self.assertIn('no-maintainers', line)
+        self.assertIn('no-tags', line)
+
     def test_upstream_default(self):
         """Operation of the default upstream"""
         cser = self.get_cser()