[PATCH 3/9] patman: Remove the patch-management code
Simon Glass
sjg at chromium.org
Tue Jun 16 16:28:43 CEST 2026
Delete the command-line tool and its supporting modules, now that this
functionality lives in the standalone patch-manager package.
Keep the modules that buildman still imports (commit and patchstream,
plus their dependencies series, get_maintainer and settings), along with
the stub command. Trim __init__.py to match.
Signed-off-by: Simon Glass <sjg at chromium.org>
---
tools/patman/__init__.py | 5 +-
tools/patman/checkpatch.py | 287 -------
tools/patman/cmdline.py | 516 ------------
tools/patman/control.py | 333 --------
tools/patman/cser_helper.py | 1524 -----------------------------------
tools/patman/cseries.py | 1165 --------------------------
tools/patman/database.py | 823 -------------------
tools/patman/patchwork.py | 852 --------------------
tools/patman/project.py | 27 -
tools/patman/send.py | 197 -----
tools/patman/status.py | 405 ----------
11 files changed, 1 insertion(+), 6133 deletions(-)
delete mode 100644 tools/patman/checkpatch.py
delete mode 100644 tools/patman/cmdline.py
delete mode 100644 tools/patman/control.py
delete mode 100644 tools/patman/cser_helper.py
delete mode 100644 tools/patman/cseries.py
delete mode 100644 tools/patman/database.py
delete mode 100644 tools/patman/patchwork.py
delete mode 100644 tools/patman/project.py
delete mode 100644 tools/patman/send.py
delete mode 100644 tools/patman/status.py
diff --git a/tools/patman/__init__.py b/tools/patman/__init__.py
index 0cca6f42435..bfe0f1fe946 100644
--- a/tools/patman/__init__.py
+++ b/tools/patman/__init__.py
@@ -1,8 +1,5 @@
# SPDX-License-Identifier: GPL-2.0+
__all__ = [
- 'checkpatch', 'cmdline', 'commit', 'control', 'cser_helper', 'cseries',
- 'database', 'func_test', 'get_maintainer', '__main__', 'patchstream',
- 'patchwork', 'project', 'send', 'series', 'settings', 'setup', 'status',
- 'test_checkpatch', 'test_common', 'test_cseries', 'test_settings'
+ 'commit', 'get_maintainer', 'patchstream', 'series', 'settings',
]
diff --git a/tools/patman/checkpatch.py b/tools/patman/checkpatch.py
deleted file mode 100644
index f9204a907ef..00000000000
--- a/tools/patman/checkpatch.py
+++ /dev/null
@@ -1,287 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-# Copyright (c) 2011 The Chromium OS Authors.
-#
-
-import collections
-import concurrent.futures
-import os
-import re
-import sys
-
-from u_boot_pylib import command
-from u_boot_pylib import gitutil
-from u_boot_pylib import terminal
-
-EMACS_PREFIX = r'(?:[0-9]{4}.*\.patch:[0-9]+: )?'
-TYPE_NAME = r'([A-Z_]+:)?'
-RE_ERROR = re.compile(r'ERROR:%s (.*)' % TYPE_NAME)
-RE_WARNING = re.compile(EMACS_PREFIX + r'WARNING:%s (.*)' % TYPE_NAME)
-RE_CHECK = re.compile(r'CHECK:%s (.*)' % TYPE_NAME)
-RE_FILE = re.compile(r'#(\d+): (FILE: ([^:]*):(\d+):)?')
-RE_NOTE = re.compile(r'NOTE: (.*)')
-
-
-def find_check_patch():
- top_level = gitutil.get_top_level() or ''
- try_list = [
- os.getcwd(),
- os.path.join(os.getcwd(), '..', '..'),
- os.path.join(top_level, 'tools'),
- os.path.join(top_level, 'scripts'),
- '%s/bin' % os.getenv('HOME'),
- ]
- # Look in current dir
- for path in try_list:
- fname = os.path.join(path, 'checkpatch.pl')
- if os.path.isfile(fname):
- return fname
-
- # Look upwwards for a Chrome OS tree
- while not os.path.ismount(path):
- fname = os.path.join(path, 'src', 'third_party', 'kernel', 'files',
- 'scripts', 'checkpatch.pl')
- if os.path.isfile(fname):
- return fname
- path = os.path.dirname(path)
-
- sys.exit('Cannot find checkpatch.pl - please put it in your ' +
- '~/bin directory or use --no-check')
-
-
-def check_patch_parse_one_message(message):
- """Parse one checkpatch message
-
- Args:
- message: string to parse
-
- Returns:
- dict:
- 'type'; error or warning
- 'msg': text message
- 'file' : filename
- 'line': line number
- """
-
- if RE_NOTE.match(message):
- return {}
-
- item = {}
-
- err_match = RE_ERROR.match(message)
- warn_match = RE_WARNING.match(message)
- check_match = RE_CHECK.match(message)
- if err_match:
- item['cptype'] = err_match.group(1)
- item['msg'] = err_match.group(2)
- item['type'] = 'error'
- elif warn_match:
- item['cptype'] = warn_match.group(1)
- item['msg'] = warn_match.group(2)
- item['type'] = 'warning'
- elif check_match:
- item['cptype'] = check_match.group(1)
- item['msg'] = check_match.group(2)
- item['type'] = 'check'
- else:
- message_indent = ' '
- print('patman: failed to parse checkpatch message:\n%s' %
- (message_indent + message.replace('\n', '\n' + message_indent)),
- file=sys.stderr)
- return {}
-
- file_match = RE_FILE.search(message)
- # some messages have no file, catch those here
- no_file_match = any(s in message for s in [
- '\nSubject:', 'Missing Signed-off-by: line(s)',
- 'does MAINTAINERS need updating'
- ])
-
- if file_match:
- err_fname = file_match.group(3)
- if err_fname:
- item['file'] = err_fname
- item['line'] = int(file_match.group(4))
- else:
- item['file'] = '<patch>'
- item['line'] = int(file_match.group(1))
- elif no_file_match:
- item['file'] = '<patch>'
- else:
- message_indent = ' '
- print('patman: failed to find file / line information:\n%s' %
- (message_indent + message.replace('\n', '\n' + message_indent)),
- file=sys.stderr)
-
- return item
-
-
-def check_patch_parse(checkpatch_output, verbose=False):
- """Parse checkpatch.pl output
-
- Args:
- checkpatch_output: string to parse
- verbose: True to print out every line of the checkpatch output as it is
- parsed
-
- Returns:
- namedtuple containing:
- ok: False=failure, True=ok
- problems (list of problems): each a dict:
- 'type'; error or warning
- 'msg': text message
- 'file' : filename
- 'line': line number
- errors: Number of errors
- warnings: Number of warnings
- checks: Number of checks
- lines: Number of lines
- stdout: checkpatch_output
- """
- fields = ['ok', 'problems', 'errors', 'warnings', 'checks', 'lines',
- 'stdout']
- result = collections.namedtuple('CheckPatchResult', fields)
- result.stdout = checkpatch_output
- result.ok = False
- result.errors, result.warnings, result.checks = 0, 0, 0
- result.lines = 0
- result.problems = []
-
- # total: 0 errors, 0 warnings, 159 lines checked
- # or:
- # total: 0 errors, 2 warnings, 7 checks, 473 lines checked
- emacs_stats = r'(?:[0-9]{4}.*\.patch )?'
- re_stats = re.compile(emacs_stats +
- r'total: (\d+) errors, (\d+) warnings, (\d+)')
- re_stats_full = re.compile(emacs_stats +
- r'total: (\d+) errors, (\d+) warnings, (\d+)'
- r' checks, (\d+)')
- re_ok = re.compile(r'.*has no obvious style problems')
- re_bad = re.compile(r'.*has style problems, please review')
-
- # A blank line indicates the end of a message
- for message in result.stdout.split('\n\n'):
- if verbose:
- print(message)
-
- # either find stats, the verdict, or delegate
- match = re_stats_full.match(message)
- if not match:
- match = re_stats.match(message)
- if match:
- result.errors = int(match.group(1))
- result.warnings = int(match.group(2))
- if len(match.groups()) == 4:
- result.checks = int(match.group(3))
- result.lines = int(match.group(4))
- else:
- result.lines = int(match.group(3))
- elif re_ok.match(message):
- result.ok = True
- elif re_bad.match(message):
- result.ok = False
- else:
- problem = check_patch_parse_one_message(message)
- if problem:
- result.problems.append(problem)
-
- return result
-
-
-def check_patch(fname, verbose=False, show_types=False, use_tree=False,
- cwd=None):
- """Run checkpatch.pl on a file and parse the results.
-
- Args:
- fname: Filename to check
- verbose: True to print out every line of the checkpatch output as it is
- parsed
- show_types: Tell checkpatch to show the type (number) of each message
- use_tree (bool): If False we'll pass '--no-tree' to checkpatch.
- cwd (str): Path to use for patch files (None to use current dir)
-
- Returns:
- namedtuple containing:
- ok: False=failure, True=ok
- problems: List of problems, each a dict:
- 'type'; error or warning
- 'msg': text message
- 'file' : filename
- 'line': line number
- errors: Number of errors
- warnings: Number of warnings
- checks: Number of checks
- lines: Number of lines
- stdout: Full output of checkpatch
- """
- chk = find_check_patch()
- args = [chk]
- if not use_tree:
- args.append('--no-tree')
- if show_types:
- args.append('--show-types')
- output = command.output(
- *args, os.path.join(cwd or '', fname), raise_on_error=False,
- capture_stderr=not use_tree)
-
- return check_patch_parse(output, verbose)
-
-
-def get_warning_msg(col, msg_type, fname, line, msg):
- '''Create a message for a given file/line
-
- Args:
- msg_type: Message type ('error' or 'warning')
- fname: Filename which reports the problem
- line: Line number where it was noticed
- msg: Message to report
- '''
- if msg_type == 'warning':
- msg_type = col.build(col.YELLOW, msg_type)
- elif msg_type == 'error':
- msg_type = col.build(col.RED, msg_type)
- elif msg_type == 'check':
- msg_type = col.build(col.MAGENTA, msg_type)
- line_str = '' if line is None else '%d' % line
- return '%s:%s: %s: %s\n' % (fname, line_str, msg_type, msg)
-
-def check_patches(verbose, args, use_tree, cwd):
- '''Run the checkpatch.pl script on each patch'''
- error_count, warning_count, check_count = 0, 0, 0
- col = terminal.Color()
-
- with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
- futures = []
- for fname in args:
- f = executor.submit(check_patch, fname, verbose, use_tree=use_tree,
- cwd=cwd)
- futures.append(f)
-
- for fname, f in zip(args, futures):
- result = f.result()
- if not result.ok:
- error_count += result.errors
- warning_count += result.warnings
- check_count += result.checks
- print('%d errors, %d warnings, %d checks for %s:' % (result.errors,
- result.warnings, result.checks, col.build(col.BLUE, fname)))
- if (len(result.problems) != result.errors + result.warnings +
- result.checks):
- print("Internal error: some problems lost")
- # Python seems to get confused by this
- # pylint: disable=E1133
- for item in result.problems:
- sys.stderr.write(
- get_warning_msg(col, item.get('type', '<unknown>'),
- item.get('file', '<unknown>'),
- item.get('line', 0), item.get('msg', 'message')))
- print
- if error_count or warning_count or check_count:
- str = 'checkpatch.pl found %d error(s), %d warning(s), %d checks(s)'
- color = col.GREEN
- if warning_count:
- color = col.YELLOW
- if error_count:
- color = col.RED
- print(col.build(color, str % (error_count, warning_count, check_count)))
- return False
- return True
diff --git a/tools/patman/cmdline.py b/tools/patman/cmdline.py
deleted file mode 100644
index 924f0ad4e42..00000000000
--- a/tools/patman/cmdline.py
+++ /dev/null
@@ -1,516 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2023 Google LLC
-#
-
-"""Handles parsing of buildman arguments
-
-This creates the argument parser and uses it to parse the arguments passed in
-"""
-
-import argparse
-import os
-import pathlib
-import sys
-
-from u_boot_pylib import gitutil
-from patman import project
-from patman import settings
-
-PATMAN_DIR = pathlib.Path(__file__).parent
-HAS_TESTS = os.path.exists(PATMAN_DIR / "func_test.py")
-
-# Aliases for subcommands
-ALIASES = {
- 'series': ['s', 'ser'],
- 'status': ['st'],
- 'patchwork': ['pw'],
- 'upstream': ['us'],
-
- # Series aliases
- 'archive': ['ar'],
- 'autolink': ['au'],
- 'gather': ['g'],
- 'open': ['o'],
- 'progress': ['p', 'pr', 'prog'],
- 'rm-version': ['rmv'],
- 'unarchive': ['unar'],
- }
-
-
-class ErrorCatchingArgumentParser(argparse.ArgumentParser):
- def __init__(self, **kwargs):
- self.exit_state = None
- self.catch_error = False
- super().__init__(**kwargs)
-
- def error(self, message):
- if self.catch_error:
- self.message = message
- else:
- super().error(message)
-
- def exit(self, status=0, message=None):
- if self.catch_error:
- self.exit_state = True
- else:
- super().exit(status, message)
-
-
-def add_send_args(par):
- """Add arguments for the 'send' command
-
- Arguments:
- par (ArgumentParser): Parser to add to
- """
- par.add_argument(
- '-c', '--count', dest='count', type=int, default=-1,
- help='Automatically create patches from top n commits')
- par.add_argument(
- '-e', '--end', type=int, default=0,
- help='Commits to skip at end of patch list')
- par.add_argument(
- '-i', '--ignore-errors', action='store_true',
- dest='ignore_errors', default=False,
- help='Send patches email even if patch errors are found')
- par.add_argument(
- '-l', '--limit-cc', dest='limit', type=int, default=None,
- help='Limit the cc list to LIMIT entries [default: %(default)s]')
- par.add_argument(
- '-m', '--no-maintainers', action='store_false',
- dest='add_maintainers', default=True,
- help="Don't cc the file maintainers automatically")
- default_arg = None
- top_level = gitutil.get_top_level()
- if top_level:
- default_arg = os.path.join(top_level, 'scripts',
- 'get_maintainer.pl') + ' --norolestats'
- par.add_argument(
- '--get-maintainer-script', dest='get_maintainer_script', type=str,
- action='store',
- default=default_arg,
- help='File name of the get_maintainer.pl (or compatible) script.')
- par.add_argument(
- '-r', '--in-reply-to', type=str, action='store',
- help="Message ID that this series is in reply to")
- par.add_argument(
- '-s', '--start', dest='start', type=int, default=0,
- help='Commit to start creating patches from (0 = HEAD)')
- par.add_argument(
- '-t', '--ignore-bad-tags', action='store_true', default=False,
- help='Ignore bad tags / aliases (default=warn)')
- par.add_argument(
- '--no-binary', action='store_true', dest='ignore_binary',
- default=False,
- help="Do not output contents of changes in binary files")
- par.add_argument(
- '--no-check', action='store_false', dest='check_patch', default=True,
- help="Don't check for patch compliance")
- par.add_argument(
- '--tree', dest='check_patch_use_tree', default=False,
- action='store_true',
- help=("Set `tree` to True. If `tree` is False then we'll pass "
- "'--no-tree' to checkpatch (default: tree=%(default)s)"))
- par.add_argument(
- '--no-tree', dest='check_patch_use_tree', action='store_false',
- help="Set `tree` to False")
- par.add_argument(
- '--no-tags', action='store_false', dest='process_tags', default=True,
- help="Don't process subject tags as aliases")
- par.add_argument(
- '--no-signoff', action='store_false', dest='add_signoff',
- default=True, help="Don't add Signed-off-by to patches")
- par.add_argument(
- '--smtp-server', type=str,
- help="Specify the SMTP server to 'git send-email'")
- par.add_argument(
- '--keep-change-id', action='store_true',
- help='Preserve Change-Id tags in patches to send.')
-
-
-def _add_show_comments(parser):
- parser.add_argument('-c', '--show-comments', action='store_true',
- help='Show comments from each patch')
-
-
-def _add_show_cover_comments(parser):
- parser.add_argument('-C', '--show-cover-comments', action='store_true',
- help='Show comments from the cover letter')
-
-
-def add_patchwork_subparser(subparsers):
- """Add the 'patchwork' subparser
-
- Args:
- subparsers (argparse action): Subparser parent
-
- Return:
- ArgumentParser: patchwork subparser
- """
- patchwork = subparsers.add_parser(
- 'patchwork', aliases=ALIASES['patchwork'],
- help='Manage patchwork connection')
- patchwork.defaults_cmds = [
- ['set-project', 'U-Boot'],
- ]
- patchwork_subparsers = patchwork.add_subparsers(dest='subcmd')
- patchwork_subparsers.add_parser('get-project')
- uset = patchwork_subparsers.add_parser('set-project')
- uset.add_argument(
- 'project_name', help="Patchwork project name, e.g. 'U-Boot'")
- return patchwork
-
-
-def add_series_subparser(subparsers):
- """Add the 'series' subparser
-
- Args:
- subparsers (argparse action): Subparser parent
-
- Return:
- ArgumentParser: series subparser
- """
- def _add_allow_unmarked(parser):
- parser.add_argument('-M', '--allow-unmarked', action='store_true',
- default=False,
- help="Don't require commits to be marked")
-
- def _add_mark(parser):
- parser.add_argument(
- '-m', '--mark', action='store_true',
- help='Mark unmarked commits with a Change-Id field')
-
- def _add_update(parser):
- parser.add_argument('-u', '--update', action='store_true',
- help='Update the branch commit')
-
- def _add_wait(parser, default_s):
- """Add a -w option to a parser
-
- Args:
- parser (ArgumentParser): Parser to adjust
- default_s (int): Default value to use, in seconds
- """
- parser.add_argument(
- '-w', '--autolink-wait', type=int, default=default_s,
- help='Seconds to wait for patchwork to get a sent series')
-
- def _upstream_add(parser):
- parser.add_argument('-U', '--upstream', help='Commit to end before')
-
- def _add_gather(parser):
- parser.add_argument(
- '-G', '--no-gather-tags', dest='gather_tags', default=True,
- action='store_false',
- help="Don't gather review/test tags / update local series")
-
- series = subparsers.add_parser('series', aliases=ALIASES['series'],
- help='Manage series of patches')
- series.defaults_cmds = [
- ['set-link', 'fred'],
- ]
- series.add_argument(
- '-n', '--dry-run', action='store_true', dest='dry_run', default=False,
- help="Do a dry run (create but don't email patches)")
- series.add_argument('-s', '--series', help='Name of series')
- series.add_argument('-V', '--version', type=int,
- help='Version number to link')
- series_subparsers = series.add_subparsers(dest='subcmd')
-
- # This causes problem at present, perhaps due to the 'defaults' handling in
- # settings
- # series_subparsers.required = True
-
- add = series_subparsers.add_parser('add')
- add.add_argument('-D', '--desc',
- help='Series description / cover-letter title')
- add.add_argument(
- '-f', '--force-version', action='store_true',
- help='Change the Series-version on a series to match its branch')
- _add_mark(add)
- _add_allow_unmarked(add)
- _upstream_add(add)
-
- series_subparsers.add_parser('archive', aliases=ALIASES['archive'])
-
- auto = series_subparsers.add_parser('autolink',
- aliases=ALIASES['autolink'])
- _add_update(auto)
- _add_wait(auto, 0)
-
- aall = series_subparsers.add_parser('autolink-all')
- aall.add_argument('-a', '--link-all-versions', action='store_true',
- help='Link all series versions, not just the latest')
- aall.add_argument('-r', '--replace-existing', action='store_true',
- help='Replace existing links')
- _add_update(aall)
-
- series_subparsers.add_parser('dec')
-
- gat = series_subparsers.add_parser('gather', aliases=ALIASES['gather'])
- _add_gather(gat)
- _add_show_comments(gat)
- _add_show_cover_comments(gat)
-
- sall = series_subparsers.add_parser('gather-all')
- sall.add_argument(
- '-a', '--gather-all-versions', action='store_true',
- help='Gather tags from all series versions, not just the latest')
- _add_gather(sall)
- _add_show_comments(sall)
- _add_show_cover_comments(sall)
-
- series_subparsers.add_parser('get-link')
- series_subparsers.add_parser('inc')
- series_subparsers.add_parser('ls')
-
- mar = series_subparsers.add_parser('mark')
- mar.add_argument('-m', '--allow-marked', action='store_true',
- default=False,
- help="Don't require commits to be unmarked")
-
- series_subparsers.add_parser('open', aliases=ALIASES['open'])
- pat = series_subparsers.add_parser(
- 'patches', epilog='Show a list of patches and optional details')
- pat.add_argument('-t', '--commit', action='store_true',
- help='Show the commit and diffstat')
- pat.add_argument('-p', '--patch', action='store_true',
- help='Show the patch body')
-
- prog = series_subparsers.add_parser('progress',
- aliases=ALIASES['progress'])
- prog.add_argument('-a', '--show-all-versions', action='store_true',
- help='Show all series versions, not just the latest')
- prog.add_argument('-l', '--list-patches', action='store_true',
- help='List patch subject and status')
-
- ren = series_subparsers.add_parser('rename')
- ren.add_argument('-N', '--new-name', help='New name for the series')
-
- series_subparsers.add_parser('rm')
- series_subparsers.add_parser('rm-version', aliases=ALIASES['rm-version'])
-
- scan = series_subparsers.add_parser('scan')
- _add_mark(scan)
- _add_allow_unmarked(scan)
- _upstream_add(scan)
-
- ssend = series_subparsers.add_parser('send')
- add_send_args(ssend)
- ssend.add_argument(
- '--no-autolink', action='store_false', default=True, dest='autolink',
- help='Monitor patchwork after sending so the series can be autolinked')
- _add_wait(ssend, 120)
-
- setl = series_subparsers.add_parser('set-link')
- _add_update(setl)
-
- setl.add_argument(
- 'link', help='Link to use, i.e. patchwork series number (e.g. 452329)')
- stat = series_subparsers.add_parser('status', aliases=ALIASES['status'])
- _add_show_comments(stat)
- _add_show_cover_comments(stat)
-
- series_subparsers.add_parser('summary')
-
- series_subparsers.add_parser('unarchive', aliases=ALIASES['unarchive'])
-
- unm = series_subparsers.add_parser('unmark')
- _add_allow_unmarked(unm)
-
- ver = series_subparsers.add_parser(
- 'version-change', help='Change a version to a different version')
- ver.add_argument('--new-version', type=int,
- help='New version number to change this one too')
-
- return series
-
-
-def add_send_subparser(subparsers):
- """Add the 'send' subparser
-
- Args:
- subparsers (argparse action): Subparser parent
-
- Return:
- ArgumentParser: send subparser
- """
- send = subparsers.add_parser(
- 'send', help='Format, check and email patches (default command)')
- send.add_argument(
- '-b', '--branch', type=str,
- help="Branch to process (by default, the current branch)")
- send.add_argument(
- '-n', '--dry-run', action='store_true', dest='dry_run',
- default=False, help="Do a dry run (create but don't email patches)")
- send.add_argument(
- '--cc-cmd', dest='cc_cmd', type=str, action='store',
- default=None, help='Output cc list for patch file (used by git)')
- add_send_args(send)
- send.add_argument('patchfiles', nargs='*')
- return send
-
-
-def add_status_subparser(subparsers):
- """Add the 'status' subparser
-
- Args:
- subparsers (argparse action): Subparser parent
-
- Return:
- ArgumentParser: status subparser
- """
- status = subparsers.add_parser('status', aliases=ALIASES['status'],
- help='Check status of patches in patchwork')
- _add_show_comments(status)
- status.add_argument(
- '-d', '--dest-branch', type=str,
- help='Name of branch to create with collected responses')
- status.add_argument('-f', '--force', action='store_true',
- help='Force overwriting an existing branch')
- status.add_argument('-T', '--single-thread', action='store_true',
- help='Disable multithreading when reading patchwork')
- return status
-
-
-def add_upstream_subparser(subparsers):
- """Add the 'status' subparser
-
- Args:
- subparsers (argparse action): Subparser parent
-
- Return:
- ArgumentParser: status subparser
- """
- upstream = subparsers.add_parser('upstream', aliases=ALIASES['upstream'],
- help='Manage upstream destinations')
- upstream.defaults_cmds = [
- ['add', 'us', 'http://fred'],
- ['delete', 'us'],
- ]
- upstream_subparsers = upstream.add_subparsers(dest='subcmd')
- uadd = upstream_subparsers.add_parser('add')
- uadd.add_argument('remote_name',
- help="Git remote name used for this upstream, e.g. 'us'")
- uadd.add_argument(
- 'url', help='URL to use for this upstream, e.g. '
- "'https://gitlab.denx.de/u-boot/u-boot.git'")
- udel = upstream_subparsers.add_parser('delete')
- udel.add_argument(
- 'remote_name',
- help="Git remote name used for this upstream, e.g. 'us'")
- upstream_subparsers.add_parser('list')
- udef = upstream_subparsers.add_parser('default')
- udef.add_argument('-u', '--unset', action='store_true',
- help='Unset the default upstream')
- udef.add_argument('remote_name', nargs='?',
- help="Git remote name used for this upstream, e.g. 'us'")
- return upstream
-
-
-def setup_parser():
- """Set up command-line parser
-
- Returns:
- argparse.Parser object
- """
- epilog = '''Create patches from commits in a branch, check them and email
- them as specified by tags you place in the commits. Use -n to do a dry
- run first.'''
-
- parser = ErrorCatchingArgumentParser(epilog=epilog)
- parser.add_argument(
- '-D', '--debug', action='store_true',
- help='Enabling debugging (provides a full traceback on error)')
- parser.add_argument(
- '-N', '--no-capture', action='store_true',
- help='Disable capturing of console output in tests')
- parser.add_argument('-p', '--project', default=project.detect_project(),
- help="Project name; affects default option values and "
- "aliases [default: %(default)s]")
- parser.add_argument('-P', '--patchwork-url',
- default='https://patchwork.ozlabs.org',
- help='URL of patchwork server [default: %(default)s]')
- parser.add_argument(
- '-T', '--thread', action='store_true', dest='thread',
- default=False, help='Create patches as a single thread')
- parser.add_argument(
- '-v', '--verbose', action='store_true', dest='verbose', default=False,
- help='Verbose output of errors and warnings')
- parser.add_argument(
- '-X', '--test-preserve-dirs', action='store_true',
- help='Preserve and display test-created directories')
- parser.add_argument(
- '-H', '--full-help', action='store_true', dest='full_help',
- default=False, help='Display the README file')
-
- subparsers = parser.add_subparsers(dest='cmd')
- add_send_subparser(subparsers)
- patchwork = add_patchwork_subparser(subparsers)
- series = add_series_subparser(subparsers)
- add_status_subparser(subparsers)
- upstream = add_upstream_subparser(subparsers)
-
- # Only add the 'test' action if the test data files are available.
- if HAS_TESTS:
- test_parser = subparsers.add_parser('test', help='Run tests')
- test_parser.add_argument('testname', type=str, default=None, nargs='?',
- help="Specify the test to run")
-
- parsers = {
- 'main': parser,
- 'series': series,
- 'patchwork': patchwork,
- 'upstream': upstream,
- }
- return parsers
-
-
-def parse_args(argv=None, config_fname=None, parsers=None):
- """Parse command line arguments from sys.argv[]
-
- Args:
- argv (str or None): Arguments to process, or None to use sys.argv[1:]
- config_fname (str): Config file to read, or None for default, or False
- for an empty config
-
- Returns:
- tuple containing:
- options: command line options
- args: command lin arguments
- """
- if not parsers:
- parsers = setup_parser()
- parser = parsers['main']
-
- # Parse options twice: first to get the project and second to handle
- # defaults properly (which depends on project)
- # Use parse_known_args() in case 'cmd' is omitted
- if not argv:
- argv = sys.argv[1:]
-
- args, rest = parser.parse_known_args(argv)
- if hasattr(args, 'project'):
- settings.Setup(parser, args.project, argv, config_fname)
- args, rest = parser.parse_known_args(argv)
-
- # If we have a command, it is safe to parse all arguments
- if args.cmd:
- args = parser.parse_args(argv)
- elif not args.full_help:
- # No command, so insert it after the known arguments and before the ones
- # that presumably relate to the 'send' subcommand
- nargs = len(rest)
- argv = argv[:-nargs] + ['send'] + rest
- args = parser.parse_args(argv)
-
- # Resolve aliases
- for full, aliases in ALIASES.items():
- if args.cmd in aliases:
- args.cmd = full
- if 'subcmd' in args and args.subcmd in aliases:
- args.subcmd = full
- if args.cmd in ['series', 'upstream', 'patchwork'] and not args.subcmd:
- parser.parse_args([args.cmd, '--help'])
-
- return args
diff --git a/tools/patman/control.py b/tools/patman/control.py
deleted file mode 100644
index 3e09b16e87b..00000000000
--- a/tools/patman/control.py
+++ /dev/null
@@ -1,333 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2020 Google LLC
-#
-"""Handles the main control logic of patman
-
-This module provides various functions called by the main program to implement
-the features of patman.
-"""
-
-import re
-import traceback
-
-try:
- from importlib import resources
-except ImportError:
- # for Python 3.6
- import importlib_resources as resources
-
-from u_boot_pylib import gitutil
-from u_boot_pylib import terminal
-from u_boot_pylib import tools
-from u_boot_pylib import tout
-from patman import cseries
-from patman import cser_helper
-from patman import patchstream
-from patman.patchwork import Patchwork
-from patman import send
-from patman import settings
-
-
-def setup():
- """Do required setup before doing anything"""
- gitutil.setup()
- alias_fname = gitutil.get_alias_file()
- if alias_fname:
- settings.ReadGitAliases(alias_fname)
-
-
-def do_send(args):
- """Create, check and send patches by email
-
- Args:
- args (argparse.Namespace): Arguments to patman
- """
- setup()
- send.send(args)
-
-
-def patchwork_status(branch, count, start, end, dest_branch, force,
- show_comments, url, single_thread=False):
- """Check the status of patches in patchwork
-
- This finds the series in patchwork using the Series-link tag, checks for new
- comments and review tags, displays then and creates a new branch with the
- review tags.
-
- Args:
- branch (str): Branch to create patches from (None = current)
- count (int): Number of patches to produce, or -1 to produce patches for
- the current branch back to the upstream commit
- start (int): Start partch to use (0=first / top of branch)
- end (int): End patch to use (0=last one in series, 1=one before that,
- etc.)
- dest_branch (str): Name of new branch to create with the updated tags
- (None to not create a branch)
- force (bool): With dest_branch, force overwriting an existing branch
- show_comments (bool): True to display snippets from the comments
- provided by reviewers
- url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org'.
- This is ignored if the series provides a Series-patchwork-url tag.
-
- Raises:
- ValueError: if the branch has no Series-link value
- """
- if not branch:
- branch = gitutil.get_branch()
- if count == -1:
- # Work out how many patches to send if we can
- count = gitutil.count_commits_to_branch(branch) - start
-
- series = patchstream.get_metadata(branch, start, count - end)
- warnings = 0
- for cmt in series.commits:
- if cmt.warn:
- print('%d warnings for %s:' % (len(cmt.warn), cmt.hash))
- for warn in cmt.warn:
- print('\t', warn)
- warnings += 1
- print
- if warnings:
- raise ValueError('Please fix warnings before running status')
- links = series.get('links')
- if not links:
- raise ValueError("Branch has no Series-links value")
-
- _, version = cser_helper.split_name_version(branch)
- link = series.get_link_for_version(version, links)
- if not link:
- raise ValueError('Series-links has no link for v{version}')
- tout.debug(f"Link '{link}")
-
- # Allow the series to override the URL
- if 'patchwork_url' in series:
- url = series.patchwork_url
- pwork = Patchwork(url, single_thread=single_thread)
-
- # Import this here to avoid failing on other commands if the dependencies
- # are not present
- from patman import status
- pwork = Patchwork(url)
- status.check_and_show_status(series, link, branch, dest_branch, force,
- show_comments, False, pwork)
-
-
-def do_series(args, test_db=None, pwork=None, cser=None):
- """Process a series subcommand
-
- Args:
- args (Namespace): Arguments to process
- test_db (str or None): Directory containing the test database, None to
- use the normal one
- pwork (Patchwork): Patchwork object to use, None to create one if
- needed
- cser (Cseries): Cseries object to use, None to create one
- """
- if not cser:
- cser = cseries.Cseries(test_db)
- needs_patchwork = [
- 'autolink', 'autolink-all', 'open', 'send', 'status', 'gather',
- 'gather-all'
- ]
- try:
- cser.open_database()
- if args.subcmd in needs_patchwork:
- if not pwork:
- pwork = Patchwork(args.patchwork_url)
- proj = cser.project_get()
- if not proj:
- raise ValueError(
- "Please set project ID with 'patman patchwork set-project'")
- _, proj_id, link_name = cser.project_get()
- pwork.project_set(proj_id, link_name)
- elif pwork and pwork is not True:
- raise ValueError(
- f"Internal error: command '{args.subcmd}' should not have patchwork")
- if args.subcmd == 'add':
- cser.add(args.series, args.desc, mark=args.mark,
- allow_unmarked=args.allow_unmarked, end=args.upstream,
- dry_run=args.dry_run)
- elif args.subcmd == 'archive':
- cser.archive(args.series)
- elif args.subcmd == 'autolink':
- cser.link_auto(pwork, args.series, args.version, args.update,
- args.autolink_wait)
- elif args.subcmd == 'autolink-all':
- cser.link_auto_all(pwork, update_commit=args.update,
- link_all_versions=args.link_all_versions,
- replace_existing=args.replace_existing,
- dry_run=args.dry_run, show_summary=True)
- elif args.subcmd == 'dec':
- cser.decrement(args.series, args.dry_run)
- elif args.subcmd == 'gather':
- cser.gather(pwork, args.series, args.version, args.show_comments,
- args.show_cover_comments, args.gather_tags,
- dry_run=args.dry_run)
- elif args.subcmd == 'gather-all':
- cser.gather_all(
- pwork, args.show_comments, args.show_cover_comments,
- args.gather_all_versions, args.gather_tags, args.dry_run)
- elif args.subcmd == 'get-link':
- link = cser.link_get(args.series, args.version)
- print(link)
- elif args.subcmd == 'inc':
- cser.increment(args.series, args.dry_run)
- elif args.subcmd == 'ls':
- cser.series_list()
- elif args.subcmd == 'open':
- cser.open(pwork, args.series, args.version)
- elif args.subcmd == 'mark':
- cser.mark(args.series, args.allow_marked, dry_run=args.dry_run)
- elif args.subcmd == 'patches':
- cser.list_patches(args.series, args.version, args.commit,
- args.patch)
- elif args.subcmd == 'progress':
- cser.progress(args.series, args.show_all_versions,
- args.list_patches)
- elif args.subcmd == 'rm':
- cser.remove(args.series, dry_run=args.dry_run)
- elif args.subcmd == 'rm-version':
- cser.version_remove(args.series, args.version, dry_run=args.dry_run)
- elif args.subcmd == 'rename':
- cser.rename(args.series, args.new_name, dry_run=args.dry_run)
- elif args.subcmd == 'scan':
- cser.scan(args.series, mark=args.mark,
- allow_unmarked=args.allow_unmarked, end=args.upstream,
- dry_run=args.dry_run)
- elif args.subcmd == 'send':
- cser.send(pwork, args.series, args.autolink, args.autolink_wait,
- args)
- elif args.subcmd == 'set-link':
- cser.link_set(args.series, args.version, args.link, args.update)
- elif args.subcmd == 'status':
- cser.status(pwork, args.series, args.version, args.show_comments,
- args.show_cover_comments)
- elif args.subcmd == 'summary':
- cser.summary(args.series)
- elif args.subcmd == 'unarchive':
- cser.unarchive(args.series)
- elif args.subcmd == 'unmark':
- cser.unmark(args.series, args.allow_unmarked, dry_run=args.dry_run)
- elif args.subcmd == 'version-change':
- cser.version_change(args.series, args.version, args.new_version,
- dry_run=args.dry_run)
- else:
- raise ValueError(f"Unknown series subcommand '{args.subcmd}'")
- finally:
- cser.close_database()
-
-
-def upstream(args, test_db=None):
- """Process an 'upstream' subcommand
-
- Args:
- args (Namespace): Arguments to process
- test_db (str or None): Directory containing the test database, None to
- use the normal one
- """
- cser = cseries.Cseries(test_db)
- try:
- cser.open_database()
- if args.subcmd == 'add':
- cser.upstream_add(args.remote_name, args.url)
- elif args.subcmd == 'default':
- if args.unset:
- cser.upstream_set_default(None)
- elif args.remote_name:
- cser.upstream_set_default(args.remote_name)
- else:
- result = cser.upstream_get_default()
- print(result if result else 'unset')
- elif args.subcmd == 'delete':
- cser.upstream_delete(args.remote_name)
- elif args.subcmd == 'list':
- cser.upstream_list()
- else:
- raise ValueError(f"Unknown upstream subcommand '{args.subcmd}'")
- finally:
- cser.close_database()
-
-
-def patchwork(args, test_db=None, pwork=None):
- """Process a 'patchwork' subcommand
- Args:
- args (Namespace): Arguments to process
- test_db (str or None): Directory containing the test database, None to
- use the normal one
- pwork (Patchwork): Patchwork object to use
- """
- cser = cseries.Cseries(test_db)
- try:
- cser.open_database()
- if args.subcmd == 'set-project':
- if not pwork:
- pwork = Patchwork(args.patchwork_url)
- cser.project_set(pwork, args.project_name)
- elif args.subcmd == 'get-project':
- info = cser.project_get()
- if not info:
- raise ValueError("Project has not been set; use 'patman patchwork set-project'")
- name, pwid, link_name = info
- print(f"Project '{name}' patchwork-ID {pwid} link-name {link_name}")
- else:
- raise ValueError(f"Unknown patchwork subcommand '{args.subcmd}'")
- finally:
- cser.close_database()
-
-def do_patman(args, test_db=None, pwork=None, cser=None):
- """Process a patman command
-
- Args:
- args (Namespace): Arguments to process
- test_db (str or None): Directory containing the test database, None to
- use the normal one
- pwork (Patchwork): Patchwork object to use, or None to create one
- cser (Cseries): Cseries object to use when executing the command,
- or None to create one
- """
- if args.full_help:
- with resources.path('patman', 'README.rst') as readme:
- tools.print_full_help(str(readme))
- return 0
- if args.cmd == 'send':
- # Called from git with a patch filename as argument
- # Printout a list of additional CC recipients for this patch
- if args.cc_cmd:
- re_line = re.compile(r'(\S*) (.*)')
- with open(args.cc_cmd, 'r', encoding='utf-8') as inf:
- for line in inf.readlines():
- match = re_line.match(line)
- if match and match.group(1) == args.patchfiles[0]:
- for cca in match.group(2).split('\0'):
- cca = cca.strip()
- if cca:
- print(cca)
- else:
- # If we are not processing tags, no need to warning about bad ones
- if not args.process_tags:
- args.ignore_bad_tags = True
- do_send(args)
- return 0
-
- ret_code = 0
- try:
- # Check status of patches in patchwork
- if args.cmd == 'status':
- patchwork_status(args.branch, args.count, args.start, args.end,
- args.dest_branch, args.force, args.show_comments,
- args.patchwork_url)
- elif args.cmd == 'series':
- do_series(args, test_db, pwork, cser)
- elif args.cmd == 'upstream':
- upstream(args, test_db)
- elif args.cmd == 'patchwork':
- patchwork(args, test_db, pwork)
- except Exception as exc:
- terminal.tprint(f'patman: {type(exc).__name__}: {exc}',
- colour=terminal.Color.RED)
- if args.debug:
- print()
- traceback.print_exc()
- ret_code = 1
- return ret_code
diff --git a/tools/patman/cser_helper.py b/tools/patman/cser_helper.py
deleted file mode 100644
index 81ad212daee..00000000000
--- a/tools/patman/cser_helper.py
+++ /dev/null
@@ -1,1524 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2025 Simon Glass <sjg at chromium.org>
-#
-"""Helper functions for handling the 'series' subcommand
-"""
-
-import asyncio
-from collections import OrderedDict, defaultdict, namedtuple
-from datetime import datetime
-import hashlib
-import os
-import re
-import sys
-import time
-from types import SimpleNamespace
-
-import aiohttp
-import pygit2
-from pygit2.enums import CheckoutStrategy
-
-from u_boot_pylib import gitutil
-from u_boot_pylib import terminal
-from u_boot_pylib import tout
-
-from patman import patchstream
-from patman.database import Database, Pcommit, SerVer
-from patman import patchwork
-from patman.series import Series
-from patman import status
-
-
-# Tag to use for Change IDs
-CHANGE_ID_TAG = 'Change-Id'
-
-# Length of hash to display
-HASH_LEN = 10
-
-# Shorter version of some states, to save horizontal space
-SHORTEN_STATE = {
- 'handled-elsewhere': 'elsewhere',
- 'awaiting-upstream': 'awaiting',
- 'not-applicable': 'n/a',
- 'changes-requested': 'changes',
-}
-
-# Summary info returned from Cseries.link_auto_all()
-AUTOLINK = namedtuple('autolink', 'name,version,link,desc,result')
-
-
-def oid(oid_val):
- """Convert a hash string into a shortened hash
-
- The number of hex digits git uses for showing hashes depends on the size of
- the repo. For the purposes of showing hashes to the user in lists, we use a
- fixed value for now
-
- Args:
- str or Pygit2.oid: Hash value to shorten
-
- Return:
- str: Shortened hash
- """
- return str(oid_val)[:HASH_LEN]
-
-
-def split_name_version(in_name):
- """Split a branch name into its series name and its version
-
- For example:
- 'series' returns ('series', 1)
- 'series3' returns ('series', 3)
- Args:
- in_name (str): Name to parse
-
- Return:
- tuple:
- str: series name
- int: series version, or None if there is none in in_name
- """
- m_ver = re.match(r'([^0-9]*)(\d*)', in_name)
- version = None
- if m_ver:
- name = m_ver.group(1)
- if m_ver.group(2):
- version = int(m_ver.group(2))
- else:
- name = in_name
- return name, version
-
-
-class CseriesHelper:
- """Helper functions for Cseries
-
- This class handles database read/write as well as operations in a git
- directory to update series information.
- """
- def __init__(self, topdir=None, colour=terminal.COLOR_IF_TERMINAL):
- """Set up a new CseriesHelper
-
- Args:
- topdir (str): Top-level directory of the repo
- colour (terminal.enum): Whether to enable ANSI colour or not
-
- Properties:
- gitdir (str): Git directory (typically topdir + '/.git')
- db (Database): Database handler
- col (terminal.Colour): Colour object
- _fake_time (float): Holds the current fake time for tests, in
- seconds
- _fake_sleep (func): Function provided by a test; called to fake a
- 'time.sleep()' call and take whatever action it wants to take.
- The only argument is the (Float) time to sleep for; it returns
- nothing
- loop (asyncio event loop): Loop used for Patchwork operations
- """
- self.topdir = topdir
- self.gitdir = None
- self.db = None
- self.col = terminal.Color(colour)
- self._fake_time = None
- self._fake_sleep = None
- self.fake_now = None
- self.loop = asyncio.get_event_loop()
-
- def open_database(self):
- """Open the database ready for use"""
- if not self.topdir:
- self.topdir = gitutil.get_top_level()
- if not self.topdir:
- raise ValueError('No git repo detected in current directory')
- self.gitdir = os.path.join(self.topdir, '.git')
- fname = f'{self.topdir}/.patman.db'
-
- # For the first instance, start it up with the expected schema
- self.db, is_new = Database.get_instance(fname)
- if is_new:
- self.db.start()
- else:
- # If a previous test has already checked the schema, just open it
- self.db.open_it()
-
- def close_database(self):
- """Close the database"""
- if self.db:
- self.db.close()
-
- def commit(self):
- """Commit changes to the database"""
- self.db.commit()
-
- def rollback(self):
- """Roll back changes to the database"""
- self.db.rollback()
-
- def set_fake_time(self, fake_sleep):
- """Setup the fake timer
-
- Args:
- fake_sleep (func(float)): Function to call to fake a sleep
- """
- self._fake_time = 0
- self._fake_sleep = fake_sleep
-
- def inc_fake_time(self, inc_s):
- """Increment the fake time
-
- Args:
- inc_s (float): Amount to increment the fake time by
- """
- self._fake_time += inc_s
-
- def get_time(self):
- """Get the current time, fake or real
-
- This function should always be used to read the time so that faking the
- time works correctly in tests.
-
- Return:
- float: Fake time, if time is being faked, else real time
- """
- if self._fake_time is not None:
- return self._fake_time
- return time.monotonic()
-
- def sleep(self, time_s):
- """Sleep for a while
-
- This function should always be used to sleep so that faking the time
- works correctly in tests.
-
- Args:
- time_s (float): Amount of seconds to sleep for
- """
- print(f'Sleeping for {time_s} seconds')
- if self._fake_time is not None:
- self._fake_sleep(time_s)
- else:
- time.sleep(time_s)
-
- def get_now(self):
- """Get the time now
-
- This function should always be used to read the datetime, so that
- faking the time works correctly in tests
-
- Return:
- DateTime object
- """
- if self.fake_now:
- return self.fake_now
- return datetime.now()
-
- def get_ser_ver_list(self):
- """Get a list of patchwork entries from the database
-
- Return:
- list of SER_VER
- """
- return self.db.ser_ver_get_list()
-
- def get_ser_ver_dict(self):
- """Get a dict of patchwork entries from the database
-
- Return: dict contain all records:
- key (int): ser_ver id
- value (SER_VER): Information about one ser_ver record
- """
- svlist = self.get_ser_ver_list()
- svdict = {}
- for sver in svlist:
- svdict[sver.idnum] = sver
- return svdict
-
- def get_upstream_dict(self):
- """Get a list of upstream entries from the database
-
- Return:
- OrderedDict:
- key (str): upstream name
- value (str): url
- """
- return self.db.upstream_get_dict()
-
- def get_pcommit_dict(self, find_svid=None):
- """Get a dict of pcommits entries from the database
-
- Args:
- find_svid (int): If not None, finds the records associated with a
- particular series and version
-
- Return:
- OrderedDict:
- key (int): record ID if find_svid is None, else seq
- value (PCOMMIT): record data
- """
- pcdict = OrderedDict()
- for rec in self.db.pcommit_get_list(find_svid):
- if find_svid is not None:
- pcdict[rec.seq] = rec
- else:
- pcdict[rec.idnum] = rec
- return pcdict
-
- def _get_series_info(self, idnum):
- """Get information for a series from the database
-
- Args:
- idnum (int): Series ID to look up
-
- Return: tuple:
- str: Series name
- str: Series description
-
- Raises:
- ValueError: Series is not found
- """
- return self.db.series_get_info(idnum)
-
- def prep_series(self, name, end=None):
- """Prepare to work with a series
-
- Args:
- name (str): Branch name with version appended, e.g. 'fix2'
- end (str or None): Commit to end at, e.g. 'my_branch~16'. Only
- commits up to that are processed. None to process commits up to
- the upstream branch
-
- Return: tuple:
- str: Series name, e.g. 'fix'
- Series: Collected series information, including name
- int: Version number, e.g. 2
- str: Message to show
- """
- ser, version = self._parse_series_and_version(name, None)
- if not name:
- name = self._get_branch_name(ser.name, version)
-
- # First check we have a branch with this name
- if not gitutil.check_branch(name, git_dir=self.gitdir):
- raise ValueError(f"No branch named '{name}'")
-
- count = gitutil.count_commits_to_branch(name, self.gitdir, end)
- if not count:
- raise ValueError('Cannot detect branch automatically: '
- 'Perhaps use -U <upstream-commit> ?')
-
- series = patchstream.get_metadata(name, 0, count, git_dir=self.gitdir)
- self._copy_db_fields_to(series, ser)
- msg = None
- if end:
- repo = pygit2.Repository(self.gitdir)
- target = repo.revparse_single(end)
- first_line = target.message.splitlines()[0]
- msg = f'Ending before {oid(target.id)} {first_line}'
-
- return name, series, version, msg
-
- def _copy_db_fields_to(self, series, in_series):
- """Copy over fields used by Cseries from one series to another
-
- This copes desc, idnum and name
-
- Args:
- series (Series): Series to copy to
- in_series (Series): Series to copy from
- """
- series.desc = in_series.desc
- series.idnum = in_series.idnum
- series.name = in_series.name
-
- def _handle_mark(self, branch_name, in_series, version, mark,
- allow_unmarked, force_version, dry_run):
- """Handle marking a series, checking for unmarked commits, etc.
-
- Args:
- branch_name (str): Name of branch to sync, or None for current one
- in_series (Series): Series object
- version (int): branch version, e.g. 2 for 'mychange2'
- mark (bool): True to mark each commit with a change ID
- allow_unmarked (str): True to not require each commit to be marked
- force_version (bool): True if ignore a Series-version tag that
- doesn't match its branch name
- dry_run (bool): True to do a dry run
-
- Returns:
- Series: New series object, if the series was marked;
- copy_db_fields_to() is used to copy fields over
-
- Raises:
- ValueError: Series being unmarked when it should be marked, etc.
- """
- series = in_series
- if 'version' in series and int(series.version) != version:
- msg = (f"Series name '{branch_name}' suggests version {version} "
- f"but Series-version tag indicates {series.version}")
- if not force_version:
- raise ValueError(msg + ' (see --force-version)')
-
- tout.warning(msg)
- tout.warning(f'Updating Series-version tag to version {version}')
- self.update_series(branch_name, series, int(series.version),
- new_name=None, dry_run=dry_run,
- add_vers=version)
-
- # Collect the commits again, as the hashes have changed
- series = patchstream.get_metadata(branch_name, 0,
- len(series.commits),
- git_dir=self.gitdir)
- self._copy_db_fields_to(series, in_series)
-
- if mark:
- add_oid = self._mark_series(branch_name, series, dry_run=dry_run)
-
- # Collect the commits again, as the hashes have changed
- series = patchstream.get_metadata(add_oid, 0, len(series.commits),
- git_dir=self.gitdir)
- self._copy_db_fields_to(series, in_series)
-
- bad_count = 0
- for commit in series.commits:
- if not commit.change_id:
- bad_count += 1
- if bad_count and not allow_unmarked:
- raise ValueError(
- f'{bad_count} commit(s) are unmarked; please use -m or -M')
-
- return series
-
- def _add_series_commits(self, series, svid):
- """Add a commits from a series into the database
-
- Args:
- series (Series): Series containing commits to add
- svid (int): ser_ver-table ID to use for each commit
- """
- to_add = [Pcommit(None, seq, commit.subject, None, commit.change_id,
- None, None, None)
- for seq, commit in enumerate(series.commits)]
-
- self.db.pcommit_add_list(svid, to_add)
-
- def get_series_by_name(self, name, include_archived=False):
- """Get a Series object from the database by name
-
- Args:
- name (str): Name of series to get
- include_archived (bool): True to search in archives series
-
- Return:
- Series: Object containing series info, or None if none
- """
- idnum = self.db.series_find_by_name(name, include_archived)
- if not idnum:
- return None
- name, desc = self.db.series_get_info(idnum)
-
- return Series.from_fields(idnum, name, desc)
-
- def _get_branch_name(self, name, version):
- """Get the branch name for a particular version
-
- Args:
- name (str): Base name of branch
- version (int): Version number to use
- """
- return name + (f'{version}' if version > 1 else '')
-
- def _ensure_version(self, ser, version):
- """Ensure that a version exists in a series
-
- Args:
- ser (Series): Series information, with idnum and name used here
- version (int): Version to check
-
- Returns:
- list of int: List of versions
- """
- versions = self._get_version_list(ser.idnum)
- if version not in versions:
- raise ValueError(
- f"Series '{ser.name}' does not have a version {version}")
- return versions
-
- def _set_link(self, ser_id, name, version, link, update_commit,
- dry_run=False):
- """Add / update a series-links link for a series
-
- Args:
- ser_id (int): Series ID number
- name (str): Series name (used to find the branch)
- version (int): Version number (used to update the database)
- link (str): Patchwork link-string for the series
- update_commit (bool): True to update the current commit with the
- link
- dry_run (bool): True to do a dry run
-
- Return:
- bool: True if the database was update, False if the ser_id or
- version was not found
- """
- if update_commit:
- branch_name = self._get_branch_name(name, version)
- _, ser, max_vers, _ = self.prep_series(branch_name)
- self.update_series(branch_name, ser, max_vers, add_vers=version,
- dry_run=dry_run, add_link=link)
- if link is None:
- link = ''
- updated = 1 if self.db.ser_ver_set_link(ser_id, version, link) else 0
- if dry_run:
- self.rollback()
- else:
- self.commit()
-
- return updated
-
- def _get_autolink_dict(self, sdict, link_all_versions):
- """Get a dict of ser_vers to fetch, along with their patchwork links
-
- Note that this returns items that already have links, as well as those
- without links
-
- Args:
- sdict:
- key: series ID
- value: Series with idnum, name and desc filled out
- link_all_versions (bool): True to sync all versions of a series,
- False to sync only the latest version
-
- Return: tuple:
- dict:
- key (int): svid
- value (tuple):
- int: series ID
- str: series name
- int: series version
- str: patchwork link for the series, or None if none
- desc: cover-letter name / series description
- """
- svdict = self.get_ser_ver_dict()
- to_fetch = {}
-
- if link_all_versions:
- for svinfo in self.get_ser_ver_list():
- ser = sdict[svinfo.series_id]
-
- pwc = self.get_pcommit_dict(svinfo.idnum)
- count = len(pwc)
- branch = self._join_name_version(ser.name, svinfo.version)
- series = patchstream.get_metadata(branch, 0, count,
- git_dir=self.gitdir)
- self._copy_db_fields_to(series, ser)
-
- to_fetch[svinfo.idnum] = (svinfo.series_id, series.name,
- svinfo.version, svinfo.link, series)
- else:
- # Find the maximum version for each series
- max_vers = self._series_all_max_versions()
-
- # Get a list of links to fetch
- for svid, ser_id, version in max_vers:
- svinfo = svdict[svid]
- ser = sdict[ser_id]
-
- pwc = self.get_pcommit_dict(svid)
- count = len(pwc)
- branch = self._join_name_version(ser.name, version)
- series = patchstream.get_metadata(branch, 0, count,
- git_dir=self.gitdir)
- self._copy_db_fields_to(series, ser)
-
- to_fetch[svid] = (ser_id, series.name, version, svinfo.link,
- series)
- return to_fetch
-
- def _get_version_list(self, idnum):
- """Get a list of the versions available for a series
-
- Args:
- idnum (int): ID of series to look up
-
- Return:
- str: List of versions
- """
- if idnum is None:
- raise ValueError('Unknown series idnum')
- return self.db.series_get_version_list(idnum)
-
- def _join_name_version(self, in_name, version):
- """Convert a series name plus a version into a branch name
-
- For example:
- ('series', 1) returns 'series'
- ('series', 3) returns 'series3'
-
- Args:
- in_name (str): Series name
- version (int): Version number
-
- Return:
- str: associated branch name
- """
- if version == 1:
- return in_name
- return f'{in_name}{version}'
-
- def _parse_series(self, name, include_archived=False):
- """Parse the name of a series, or detect it from the current branch
-
- Args:
- name (str or None): name of series
- include_archived (bool): True to search in archives series
-
- Return:
- Series: New object with the name set; idnum is also set if the
- series exists in the database
- """
- if not name:
- name = gitutil.get_branch(self.gitdir)
- name, _ = split_name_version(name)
- ser = self.get_series_by_name(name, include_archived)
- if not ser:
- ser = Series()
- ser.name = name
- return ser
-
- def _parse_series_and_version(self, in_name, in_version):
- """Parse name and version of a series, or detect from current branch
-
- Figures out the name from in_name, or if that is None, from the current
- branch.
-
- Uses the version in_version, or if that is None, uses the int at the
- end of the name (e.g. 'series' is version 1, 'series4' is version 4)
-
- Args:
- in_name (str or None): name of series
- in_version (str or None): version of series
-
- Return:
- tuple:
- Series: New object with the name set; idnum is also set if the
- series exists in the database
- int: Series version-number detected from the name
- (e.g. 'fred' is version 1, 'fred2' is version 2)
- """
- name = in_name
- if not name:
- name = gitutil.get_branch(self.gitdir)
- if not name:
- raise ValueError('No branch detected: please use -s <series>')
- name, version = split_name_version(name)
- if not name:
- raise ValueError(f"Series name '{in_name}' cannot be a number, "
- f"use '<name><version>'")
- if in_version:
- if version and version != in_version:
- tout.warning(
- f"Version mismatch: -V has {in_version} but branch name "
- f'indicates {version}')
- version = in_version
- if not version:
- version = 1
- if version > 99:
- raise ValueError(f"Version {version} exceeds 99")
- ser = self.get_series_by_name(name)
- if not ser:
- ser = Series()
- ser.name = name
- return ser, version
-
- def _series_get_version_stats(self, idnum, vers):
- """Get the stats for a series
-
- Args:
- idnum (int): ID number of series to process
- vers (int): Version number to process
-
- Return:
- tuple:
- str: Status string, '<accepted>/<count>'
- OrderedDict:
- key (int): record ID if find_svid is None, else seq
- value (PCOMMIT): record data
- """
- svid, link = self._get_series_svid_link(idnum, vers)
- pwc = self.get_pcommit_dict(svid)
- count = len(pwc.values())
- if link:
- accepted = 0
- for pcm in pwc.values():
- accepted += pcm.state == 'accepted'
- else:
- accepted = '-'
- return f'{accepted}/{count}', pwc
-
- def get_series_svid(self, series_id, version):
- """Get the patchwork ID of a series version
-
- Args:
- series_id (int): id of the series to look up
- version (int): version number to look up
-
- Return:
- str: link found
-
- Raises:
- ValueError: No matching series found
- """
- return self._get_series_svid_link(series_id, version)[0]
-
- def _get_series_svid_link(self, series_id, version):
- """Get the patchwork ID of a series version
-
- Args:
- series_id (int): series ID to look up
- version (int): version number to look up
-
- Return:
- tuple:
- int: record id
- str: link
- """
- recs = self.get_ser_ver(series_id, version)
- return recs.idnum, recs.link
-
- def get_ser_ver(self, series_id, version):
- """Get the patchwork details for a series version
-
- Args:
- series_id (int): series ID to look up
- version (int): version number to look up
-
- Return:
- SER_VER: Requested information
-
- Raises:
- ValueError: There is no matching idnum/version
- """
- return self.db.ser_ver_get_for_series(series_id, version)
-
- def _prepare_process(self, name, count, new_name=None, quiet=False):
- """Get ready to process all commits in a branch
-
- Args:
- name (str): Name of the branch to process
- count (int): Number of commits
- new_name (str or None): New name, if a new branch is to be created
- quiet (bool): True to avoid output (used for testing)
-
- Return: tuple:
- pygit2.repo: Repo to use
- pygit2.oid: Upstream commit, onto which commits should be added
- Pygit2.branch: Original branch, for later use
- str: (Possibly new) name of branch to process
- list of Commit: commits to process, in order
- pygit2.Reference: Original head before processing started
- """
- upstream_guess = gitutil.get_upstream(self.gitdir, name)[0]
-
- tout.debug(f"_process_series name '{name}' new_name '{new_name}' "
- f"upstream_guess '{upstream_guess}'")
- dirty = gitutil.check_dirty(self.gitdir, self.topdir)
- if dirty:
- raise ValueError(
- f"Modified files exist: use 'git status' to check: "
- f'{dirty[:5]}')
- repo = pygit2.Repository(self.gitdir)
-
- commit = None
- upstream_name = None
- if upstream_guess:
- try:
- upstream = repo.lookup_reference(upstream_guess)
- upstream_name = upstream.name
- commit = upstream.peel(pygit2.enums.ObjectType.COMMIT)
- except KeyError:
- pass
- except pygit2.repository.InvalidSpecError as exc:
- print(f"Error '{exc}'")
- if not upstream_name:
- upstream_name = f'{name}~{count}'
- commit = repo.revparse_single(upstream_name)
-
- branch = repo.lookup_branch(name)
- if not quiet:
- tout.info(
- f'Checking out upstream commit {upstream_name}: '
- f'{oid(commit.oid)}')
-
- old_head = repo.head
- if old_head.shorthand == name:
- old_head = None
- else:
- old_head = repo.head
-
- if new_name:
- name = new_name
- repo.set_head(commit.oid)
-
- commits = []
- cmt = repo.get(branch.target)
- for _ in range(count):
- commits.append(cmt)
- cmt = cmt.parents[0]
-
- return (repo, repo.head, branch, name, commit, list(reversed(commits)),
- old_head)
-
- def _pick_commit(self, repo, cmt):
- """Apply a commit to the source tree, without committing it
-
- _prepare_process() must be called before starting to pick commits
-
- This function must be called before _finish_commit()
-
- Note that this uses a cherry-pick method, creating a new tree_id each
- time, so can make source-code changes
-
- Args:
- repo (pygit2.repo): Repo to use
- cmt (Commit): Commit to apply
-
- Return: tuple:
- tree_id (pygit2.oid): Oid of index with source-changes applied
- commit (pygit2.oid): Old commit being cherry-picked
- """
- tout.detail(f"- adding {oid(cmt.hash)} {cmt}")
- repo.cherrypick(cmt.hash)
- if repo.index.conflicts:
- raise ValueError('Conflicts detected')
-
- tree_id = repo.index.write_tree()
- cherry = repo.get(cmt.hash)
- tout.detail(f"cherry {oid(cherry.oid)}")
- return tree_id, cherry
-
- def _finish_commit(self, repo, tree_id, commit, cur, msg=None):
- """Complete a commit
-
- This must be called after _pick_commit().
-
- Args:
- repo (pygit2.repo): Repo to use
- tree_id (pygit2.oid): Oid of index with source-changes applied; if
- None then the existing commit.tree_id is used
- commit (pygit2.oid): Old commit being cherry-picked
- cur (pygit2.reference): Reference to parent to use for the commit
- msg (str): Commit subject and message; None to use commit.message
- """
- if msg is None:
- msg = commit.message
- if not tree_id:
- tree_id = commit.tree_id
- repo.create_commit('HEAD', commit.author, commit.committer,
- msg, tree_id, [cur.target])
- return repo.head
-
- def _finish_process(self, repo, branch, name, cur, old_head, new_name=None,
- switch=False, dry_run=False, quiet=False):
- """Finish processing commits
-
- Args:
- repo (pygit2.repo): Repo to use
- branch (pygit2.branch): Branch returned by _prepare_process()
- name (str): Name of the branch to process
- new_name (str or None): New name, if a new branch is being created
- switch (bool): True to switch to the new branch after processing;
- otherwise HEAD remains at the original branch, as amended
- dry_run (bool): True to do a dry run, restoring the original tree
- afterwards
- quiet (bool): True to avoid output (used for testing)
-
- Return:
- pygit2.reference: Final commit after everything is completed
- """
- repo.state_cleanup()
-
- # Update the branch
- target = repo.revparse_single('HEAD')
- if not quiet:
- tout.info(f'Updating branch {name} from {oid(branch.target)} to '
- f'{str(target.oid)[:HASH_LEN]}')
- if dry_run:
- if new_name:
- repo.head.set_target(branch.target)
- else:
- branch_oid = branch.peel(pygit2.enums.ObjectType.COMMIT).oid
- repo.head.set_target(branch_oid)
- repo.head.set_target(branch.target)
- repo.set_head(branch.name)
- else:
- if new_name:
- new_branch = repo.branches.create(new_name, target)
- if branch.upstream:
- new_branch.upstream = branch.upstream
- branch = new_branch
- else:
- branch.set_target(cur.target)
- repo.set_head(branch.name)
- if old_head:
- if not switch:
- repo.set_head(old_head.name)
- return target
-
- def make_change_id(self, commit):
- """Make a Change ID for a commit
-
- This is similar to the gerrit script:
- git var GIT_COMMITTER_IDENT ; echo "$refhash" ; cat "README"; }
- | git hash-object --stdin)
-
- Args:
- commit (pygit2.commit): Commit to process
-
- Return:
- Change ID in hex format
- """
- sig = commit.committer
- val = hashlib.sha1()
- to_hash = f'{sig.name} <{sig.email}> {sig.time} {sig.offset}'
- val.update(to_hash.encode('utf-8'))
- val.update(str(commit.tree_id).encode('utf-8'))
- val.update(commit.message.encode('utf-8'))
- return val.hexdigest()
-
- def _filter_commits(self, name, series, seq_to_drop):
- """Filter commits to drop one
-
- This function rebases the current branch, dropping a single commit,
- thus changing the resulting code in the tree.
-
- Args:
- name (str): Name of the branch to process
- series (Series): Series object
- seq_to_drop (int): Commit sequence to drop; commits are numbered
- from 0, which is the one after the upstream branch, to
- count - 1
- """
- count = len(series.commits)
- (repo, cur, branch, name, commit, _, _) = self._prepare_process(
- name, count, quiet=True)
- repo.checkout_tree(commit, strategy=CheckoutStrategy.FORCE |
- CheckoutStrategy.RECREATE_MISSING)
- repo.set_head(commit.oid)
- for seq, cmt in enumerate(series.commits):
- if seq != seq_to_drop:
- tree_id, cherry = self._pick_commit(repo, cmt)
- cur = self._finish_commit(repo, tree_id, cherry, cur)
- self._finish_process(repo, branch, name, cur, None, quiet=True)
-
- def process_series(self, name, series, new_name=None, switch=False,
- dry_run=False):
- """Rewrite a series commit messages, leaving code alone
-
- This uses a 'vals' namespace to pass things to the controlling
- function.
-
- Each time _process_series() yields, it sets up:
- commit (Commit): The pygit2 commit that is being processed
- msg (str): Commit message, which can be modified
- info (str): Initially empty; the controlling function can add a
- short message here which will be shown to the user
- final (bool): True if this is the last commit to apply
- seq (int): Current sequence number in the commits to apply (0,,n-1)
-
- It also sets git HEAD at the commit before this commit being
- processed
-
- The function can change msg and info, e.g. to add or remove tags from
- the commit.
-
- Args:
- name (str): Name of the branch to process
- series (Series): Series object
- new_name (str or None): New name, if a new branch is to be created
- switch (bool): True to switch to the new branch after processing;
- otherwise HEAD remains at the original branch, as amended
- dry_run (bool): True to do a dry run, restoring the original tree
- afterwards
-
- Return:
- pygit.oid: oid of the new branch
- """
- count = len(series.commits)
- repo, cur, branch, name, _, commits, old_head = self._prepare_process(
- name, count, new_name)
- vals = SimpleNamespace()
- vals.final = False
- tout.info(f"Processing {count} commits from branch '{name}'")
-
- # Record the message lines
- lines = []
- for seq, cmt in enumerate(series.commits):
- commit = commits[seq]
- vals.commit = commit
- vals.msg = commit.message
- vals.info = ''
- vals.final = seq == len(series.commits) - 1
- vals.seq = seq
- yield vals
-
- cur = self._finish_commit(repo, None, commit, cur, vals.msg)
- lines.append([vals.info.strip(),
- f'{oid(cmt.hash)} as {oid(cur.target)} {cmt}'])
-
- max_len = max(len(info) for info, rest in lines) + 1
- for info, rest in lines:
- if info:
- info += ':'
- tout.info(f'- {info.ljust(max_len)} {rest}')
- target = self._finish_process(repo, branch, name, cur, old_head,
- new_name, switch, dry_run)
- vals.oid = target.oid
-
- def _mark_series(self, name, series, dry_run=False):
- """Mark a series with Change-Id tags
-
- Args:
- name (str): Name of the series to mark
- series (Series): Series object
- dry_run (bool): True to do a dry run, restoring the original tree
- afterwards
-
- Return:
- pygit.oid: oid of the new branch
- """
- vals = None
- for vals in self.process_series(name, series, dry_run=dry_run):
- if CHANGE_ID_TAG not in vals.msg:
- change_id = self.make_change_id(vals.commit)
- vals.msg = vals.msg + f'\n{CHANGE_ID_TAG}: {change_id}'
- tout.detail(" - adding mark")
- vals.info = 'marked'
- else:
- vals.info = 'has mark'
-
- return vals.oid
-
- def update_series(self, branch_name, series, max_vers, new_name=None,
- dry_run=False, add_vers=None, add_link=None,
- add_rtags=None, switch=False):
- """Rewrite a series to update the Series-version/Series-links lines
-
- This updates the series in git; it does not update the database
-
- Args:
- branch_name (str): Name of the branch to process
- series (Series): Series object
- max_vers (int): Version number of the series being updated
- new_name (str or None): New name, if a new branch is to be created
- dry_run (bool): True to do a dry run, restoring the original tree
- afterwards
- add_vers (int or None): Version number to add to the series, if any
- add_link (str or None): Link to add to the series, if any
- add_rtags (list of dict): List of review tags to add, one item for
- each commit, each a dict:
- key: Response tag (e.g. 'Reviewed-by')
- value: Set of people who gave that response, each a name/email
- string
- switch (bool): True to switch to the new branch after processing;
- otherwise HEAD remains at the original branch, as amended
-
- Return:
- pygit.oid: oid of the new branch
- """
- def _do_version():
- if add_vers:
- if add_vers == 1:
- vals.info += f'rm v{add_vers} '
- else:
- vals.info += f'add v{add_vers} '
- out.append(f'Series-version: {add_vers}')
-
- def _do_links(new_links):
- if add_link:
- if 'add' not in vals.info:
- vals.info += 'add '
- vals.info += f"links '{new_links}' "
- else:
- vals.info += f"upd links '{new_links}' "
- out.append(f'Series-links: {new_links}')
-
- added_version = False
- added_link = False
- for vals in self.process_series(branch_name, series, new_name, switch,
- dry_run):
- out = []
- for line in vals.msg.splitlines():
- m_ver = re.match('Series-version:(.*)', line)
- m_links = re.match('Series-links:(.*)', line)
- if m_ver and add_vers:
- if ('version' in series and
- int(series.version) != max_vers):
- tout.warning(
- f'Branch {branch_name}: Series-version tag '
- f'{series.version} does not match expected '
- f'version {max_vers}')
- _do_version()
- added_version = True
- elif m_links:
- links = series.get_links(m_links.group(1), max_vers)
- if add_link:
- links[max_vers] = add_link
- _do_links(series.build_links(links))
- added_link = True
- else:
- out.append(line)
- if vals.final:
- if not added_version and add_vers and add_vers > 1:
- _do_version()
- if not added_link and add_link:
- _do_links(f'{max_vers}:{add_link}')
-
- vals.msg = '\n'.join(out) + '\n'
- if add_rtags and add_rtags[vals.seq]:
- lines = []
- for tag, people in add_rtags[vals.seq].items():
- for who in people:
- lines.append(f'{tag}: {who}')
- vals.msg = patchstream.insert_tags(vals.msg.rstrip(),
- sorted(lines))
- vals.info += (f'added {len(lines)} '
- f"tag{'' if len(lines) == 1 else 's'}")
-
- def _build_col(self, state, prefix='', base_str=None):
- """Build a patch-state string with colour
-
- Args:
- state (str): State to colourise (also indicates the colour to use)
- prefix (str): Prefix string to also colourise
- base_str (str or None): String to show instead of state, or None to
- show state
-
- Return:
- str: String with ANSI colour characters
- """
- bright = True
- if state == 'accepted':
- col = self.col.GREEN
- elif state == 'awaiting-upstream':
- bright = False
- col = self.col.GREEN
- elif state in ['changes-requested']:
- col = self.col.CYAN
- elif state in ['rejected', 'deferred', 'not-applicable', 'superseded',
- 'handled-elsewhere']:
- col = self.col.RED
- elif not state:
- state = 'unknown'
- col = self.col.MAGENTA
- else:
- # under-review, rfc, needs-review-ack
- col = self.col.WHITE
- out = base_str or SHORTEN_STATE.get(state, state)
- pad = ' ' * (10 - len(out))
- col_state = self.col.build(col, prefix + out, bright)
- return col_state, pad
-
- def _get_patches(self, series, version):
- """Get a Series object containing the patches in a series
-
- Args:
- series (str): Name of series to use, or None to use current branch
- version (int): Version number, or None to detect from name
-
- Return: tuple:
- str: Name of branch, e.g. 'mary2'
- Series: Series object containing the commits and idnum, desc, name
- int: Version number of series, e.g. 2
- OrderedDict:
- key (int): record ID if find_svid is None, else seq
- value (PCOMMIT): record data
- str: series name (for this version)
- str: patchwork link
- str: cover_id
- int: cover_num_comments
- """
- ser, version = self._parse_series_and_version(series, version)
- if not ser.idnum:
- raise ValueError(f"Unknown series '{series}'")
- self._ensure_version(ser, version)
- svinfo = self.get_ser_ver(ser.idnum, version)
- pwc = self.get_pcommit_dict(svinfo.idnum)
-
- count = len(pwc)
- branch = self._join_name_version(ser.name, version)
- series = patchstream.get_metadata(branch, 0, count,
- git_dir=self.gitdir)
- self._copy_db_fields_to(series, ser)
-
- return (branch, series, version, pwc, svinfo.name, svinfo.link,
- svinfo.cover_id, svinfo.cover_num_comments)
-
- def _list_patches(self, branch, pwc, series, desc, cover_id, num_comments,
- show_commit, show_patch, list_patches, state_totals):
- """List patches along with optional status info
-
- Args:
- branch (str): Branch name if self.show_progress
- pwc (dict): pcommit records:
- key (int): seq
- value (PCOMMIT): Record from database
- series (Series): Series to show, or None to just use the database
- desc (str): Series title
- cover_id (int): Cover-letter ID
- num_comments (int): The number of comments on the cover letter
- show_commit (bool): True to show the commit and diffstate
- show_patch (bool): True to show the patch
- list_patches (bool): True to list all patches for each series,
- False to just show the series summary on a single line
- state_totals (dict): Holds totals for each state across all patches
- key (str): state name
- value (int): Number of patches in that state
-
- Return:
- bool: True if OK, False if any commit subjects don't match their
- patchwork subjects
- """
- lines = []
- states = defaultdict(int)
- count = len(pwc)
- ok = True
- for seq, item in enumerate(pwc.values()):
- if series:
- cmt = series.commits[seq]
- if cmt.subject != item.subject:
- ok = False
-
- col_state, pad = self._build_col(item.state)
- patch_id = item.patch_id if item.patch_id else ''
- if item.num_comments:
- comments = str(item.num_comments)
- elif item.num_comments is None:
- comments = '-'
- else:
- comments = ''
-
- if show_commit or show_patch:
- subject = self.col.build(self.col.BLACK, item.subject,
- bright=False, back=self.col.YELLOW)
- else:
- subject = item.subject
-
- line = (f'{seq:3} {col_state}{pad} {comments.rjust(3)} '
- f'{patch_id:7} {oid(cmt.hash)} {subject}')
- lines.append(line)
- states[item.state] += 1
- out = ''
- for state, freq in states.items():
- out += ' ' + self._build_col(state, f'{freq}:')[0]
- state_totals[state] += freq
- name = ''
- if not list_patches:
- name = desc or series.desc
- name = self.col.build(self.col.YELLOW, name[:41].ljust(41))
- if not ok:
- out = '*' + out[1:]
- print(f"{branch:16} {name} {len(pwc):5} {out}")
- return ok
- print(f"Branch '{branch}' (total {len(pwc)}):{out}{name}")
-
- print(self.col.build(
- self.col.MAGENTA,
- f"Seq State Com PatchId {'Commit'.ljust(HASH_LEN)} Subject"))
-
- comments = '' if num_comments is None else str(num_comments)
- if desc or comments or cover_id:
- cov = 'Cov' if cover_id else ''
- print(self.col.build(
- self.col.WHITE,
- f"{cov:14} {comments.rjust(3)} {cover_id or '':7} "
- f'{desc or series.desc}',
- bright=False))
- for seq in range(count):
- line = lines[seq]
- print(line)
- if show_commit or show_patch:
- print()
- cmt = series.commits[seq] if series else ''
- msg = gitutil.show_commit(
- cmt.hash, show_commit, True, show_patch,
- colour=self.col.enabled(), git_dir=self.gitdir)
- sys.stdout.write(msg)
- if seq != count - 1:
- print()
- print()
-
- return ok
-
- def _find_matched_commit(self, commits, pcm):
- """Find a commit in a list of possible matches
-
- Args:
- commits (dict of Commit): Possible matches
- key (int): sequence number of patch (from 0)
- value (Commit): Commit object
- pcm (PCOMMIT): Patch to check
-
- Return:
- int: Sequence number of matching commit, or None if not found
- """
- for seq, cmt in commits.items():
- tout.debug(f"- match subject: '{cmt.subject}'")
- if pcm.subject == cmt.subject:
- return seq
- return None
-
- def _find_matched_patch(self, patches, cmt):
- """Find a patch in a list of possible matches
-
- Args:
- patches: dict of ossible matches
- key (int): sequence number of patch
- value (PCOMMIT): patch
- cmt (Commit): Commit to check
-
- Return:
- int: Sequence number of matching patch, or None if not found
- """
- for seq, pcm in patches.items():
- tout.debug(f"- match subject: '{pcm.subject}'")
- if cmt.subject == pcm.subject:
- return seq
- return None
-
- def _sync_one(self, svid, series_name, version, show_comments,
- show_cover_comments, gather_tags, cover, patches, dry_run):
- """Sync one series to the database
-
- Args:
- svid (int): Ser/ver ID
- cover (dict or None): Cover letter from patchwork, with keys:
- id (int): Cover-letter ID in patchwork
- num_comments (int): Number of comments
- name (str): Cover-letter name
- patches (list of Patch): Patches in the series
- """
- pwc = self.get_pcommit_dict(svid)
- if gather_tags:
- count = len(pwc)
- branch = self._join_name_version(series_name, version)
- series = patchstream.get_metadata(branch, 0, count,
- git_dir=self.gitdir)
-
- _, new_rtag_list = status.do_show_status(
- series, cover, patches, show_comments, show_cover_comments,
- self.col, warnings_on_stderr=False)
- self.update_series(branch, series, version, None, dry_run,
- add_rtags=new_rtag_list)
-
- updated = 0
- for seq, item in enumerate(pwc.values()):
- if seq >= len(patches):
- continue
- patch = patches[seq]
- if patch.id:
- if self.db.pcommit_update(
- Pcommit(item.idnum, seq, None, None, None, patch.state,
- patch.id, len(patch.comments))):
- updated += 1
- if cover:
- info = SerVer(svid, None, None, None, cover.id,
- cover.num_comments, cover.name, None)
- else:
- info = SerVer(svid, None, None, None, None, None, patches[0].name,
- None)
- self.db.ser_ver_set_info(info)
-
- return updated, 1 if cover else 0
-
- async def _gather(self, pwork, link, show_cover_comments):
- """Sync the series status from patchwork
-
- Creates a new client sesion and calls _sync()
-
- Args:
- pwork (Patchwork): Patchwork object to use
- link (str): Patchwork link for the series
- show_cover_comments (bool): True to show the comments on the cover
- letter
-
- Return: tuple:
- COVER object, or None if none or not read_cover_comments
- list of PATCH objects
- """
- async with aiohttp.ClientSession() as client:
- return await pwork.series_get_state(client, link, True,
- show_cover_comments)
-
- def _get_fetch_dict(self, sync_all_versions):
- """Get a dict of ser_vers to fetch, along with their patchwork links
-
- Args:
- sync_all_versions (bool): True to sync all versions of a series,
- False to sync only the latest version
-
- Return: tuple:
- dict: things to fetch
- key (int): svid
- value (str): patchwork link for the series
- int: number of series which are missing a link
- """
- missing = 0
- svdict = self.get_ser_ver_dict()
- sdict = self.db.series_get_dict_by_id()
- to_fetch = {}
-
- if sync_all_versions:
- for svinfo in self.get_ser_ver_list():
- ser_ver = svdict[svinfo.idnum]
- if svinfo.link:
- to_fetch[svinfo.idnum] = patchwork.STATE_REQ(
- svinfo.link, svinfo.series_id,
- sdict[svinfo.series_id].name, svinfo.version, False,
- False)
- else:
- missing += 1
- else:
- # Find the maximum version for each series
- max_vers = self._series_all_max_versions()
-
- # Get a list of links to fetch
- for svid, series_id, version in max_vers:
- ser_ver = svdict[svid]
- if series_id not in sdict:
- # skip archived item
- continue
- if ser_ver.link:
- to_fetch[svid] = patchwork.STATE_REQ(
- ser_ver.link, series_id, sdict[series_id].name,
- version, False, False)
- else:
- missing += 1
-
- # order by series name, version
- ordered = OrderedDict()
- for svid in sorted(
- to_fetch,
- key=lambda k: (to_fetch[k].series_name, to_fetch[k].version)):
- sync = to_fetch[svid]
- ordered[svid] = sync
-
- return ordered, missing
-
- async def _sync_all(self, client, pwork, to_fetch):
- """Sync all series status from patchwork
-
- Args:
- pwork (Patchwork): Patchwork object to use
- sync_all_versions (bool): True to sync all versions of a series,
- False to sync only the latest version
- gather_tags (bool): True to gather review/test tags
-
- Return: list of tuple:
- COVER object, or None if none or not read_cover_comments
- list of PATCH objects
- """
- with pwork.collect_stats() as stats:
- tasks = [pwork.series_get_state(client, sync.link, True, True)
- for sync in to_fetch.values() if sync.link]
- result = await asyncio.gather(*tasks)
- return result, stats.request_count
-
- async def _do_series_sync_all(self, pwork, to_fetch):
- async with aiohttp.ClientSession() as client:
- return await self._sync_all(client, pwork, to_fetch)
-
- def _progress_one(self, ser, show_all_versions, list_patches,
- state_totals):
- """Show progress information for all versions in a series
-
- Args:
- ser (Series): Series to use
- show_all_versions (bool): True to show all versions of a series,
- False to show only the final version
- list_patches (bool): True to list all patches for each series,
- False to just show the series summary on a single line
- state_totals (dict): Holds totals for each state across all patches
- key (str): state name
- value (int): Number of patches in that state
-
- Return: tuple
- int: Number of series shown
- int: Number of patches shown
- int: Number of version which need a 'scan'
- """
- max_vers = self._series_max_version(ser.idnum)
- name, desc = self._get_series_info(ser.idnum)
- coloured = self.col.build(self.col.BLACK, desc, bright=False,
- back=self.col.YELLOW)
- versions = self._get_version_list(ser.idnum)
- vstr = list(map(str, versions))
-
- if list_patches:
- print(f"{name}: {coloured} (versions: {' '.join(vstr)})")
- add_blank_line = False
- total_series = 0
- total_patches = 0
- need_scan = 0
- for ver in versions:
- if not show_all_versions and ver != max_vers:
- continue
- if add_blank_line:
- print()
- _, pwc = self._series_get_version_stats(ser.idnum, ver)
- count = len(pwc)
- branch = self._join_name_version(ser.name, ver)
- series = patchstream.get_metadata(branch, 0, count,
- git_dir=self.gitdir)
- svinfo = self.get_ser_ver(ser.idnum, ver)
- self._copy_db_fields_to(series, ser)
-
- ok = self._list_patches(
- branch, pwc, series, svinfo.name, svinfo.cover_id,
- svinfo.cover_num_comments, False, False, list_patches,
- state_totals)
- if not ok:
- need_scan += 1
- add_blank_line = list_patches
- total_series += 1
- total_patches += count
- return total_series, total_patches, need_scan
-
- def _summary_one(self, ser):
- """Show summary information for the latest version in a series
-
- Args:
- series (str): Name of series to use, or None to show progress for
- all series
- """
- max_vers = self._series_max_version(ser.idnum)
- name, desc = self._get_series_info(ser.idnum)
- stats, pwc = self._series_get_version_stats(ser.idnum, max_vers)
- states = {x.state for x in pwc.values()}
- state = 'accepted'
- for val in ['awaiting-upstream', 'changes-requested', 'rejected',
- 'deferred', 'not-applicable', 'superseded',
- 'handled-elsewhere']:
- if val in states:
- state = val
- state_str, pad = self._build_col(state, base_str=name)
- print(f"{state_str}{pad} {stats.rjust(6)} {desc}")
-
- def _series_max_version(self, idnum):
- """Find the latest version of a series
-
- Args:
- idnum (int): Series ID to look up
-
- Return:
- int: maximum version
- """
- return self.db.series_get_max_version(idnum)
-
- def _series_all_max_versions(self):
- """Find the latest version of all series
-
- Return: list of:
- int: ser_ver ID
- int: series ID
- int: Maximum version
- """
- return self.db.series_get_all_max_versions()
diff --git a/tools/patman/cseries.py b/tools/patman/cseries.py
deleted file mode 100644
index 0844b5f0257..00000000000
--- a/tools/patman/cseries.py
+++ /dev/null
@@ -1,1165 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2025 Google LLC
-#
-"""Handles the 'series' subcommand
-"""
-
-import asyncio
-from collections import OrderedDict, defaultdict
-
-import pygit2
-
-from u_boot_pylib import cros_subprocess
-from u_boot_pylib import gitutil
-from u_boot_pylib import terminal
-from u_boot_pylib import tout
-
-from patman import patchstream
-from patman import cser_helper
-from patman.cser_helper import AUTOLINK, oid
-from patman import send
-from patman import status
-
-
-class Cseries(cser_helper.CseriesHelper):
- """Database with information about series
-
- This class handles database read/write as well as operations in a git
- directory to update series information.
- """
- def __init__(self, topdir=None, colour=terminal.COLOR_IF_TERMINAL):
- """Set up a new Cseries
-
- Args:
- topdir (str): Top-level directory of the repo
- colour (terminal.enum): Whether to enable ANSI colour or not
- """
- super().__init__(topdir, colour)
-
- def add(self, branch_name, desc=None, mark=False, allow_unmarked=False,
- end=None, force_version=False, dry_run=False):
- """Add a series (or new version of a series) to the database
-
- Args:
- branch_name (str): Name of branch to sync, or None for current one
- desc (str): Description to use, or None to use the series subject
- mark (str): True to mark each commit with a change ID
- allow_unmarked (str): True to not require each commit to be marked
- end (str): Add only commits up to but exclu
- force_version (bool): True if ignore a Series-version tag that
- doesn't match its branch name
- dry_run (bool): True to do a dry run
- """
- name, ser, version, msg = self.prep_series(branch_name, end)
- tout.info(f"Adding series '{ser.name}' v{version}: mark {mark} "
- f'allow_unmarked {allow_unmarked}')
- if msg:
- tout.info(msg)
- if desc is None:
- if not ser.cover:
- raise ValueError(f"Branch '{name}' has no cover letter - "
- 'please provide description')
- desc = ser['cover'][0]
-
- ser = self._handle_mark(name, ser, version, mark, allow_unmarked,
- force_version, dry_run)
- link = ser.get_link_for_version(version)
-
- msg = 'Added'
- added = False
- series_id = self.db.series_find_by_name(ser.name)
- if not series_id:
- series_id = self.db.series_add(ser.name, desc)
- added = True
- msg += f" series '{ser.name}'"
-
- if version not in self._get_version_list(series_id):
- svid = self.db.ser_ver_add(series_id, version, link)
- msg += f" v{version}"
- if not added:
- msg += f" to existing series '{ser.name}'"
- added = True
-
- self._add_series_commits(ser, svid)
- count = len(ser.commits)
- msg += f" ({count} commit{'s' if count > 1 else ''})"
- if not added:
- tout.info(f"Series '{ser.name}' v{version} already exists")
- msg = None
- elif not dry_run:
- self.commit()
- else:
- self.rollback()
- series_id = None
- ser.desc = desc
- ser.idnum = series_id
-
- if msg:
- tout.info(msg)
- if dry_run:
- tout.info('Dry run completed')
-
- def decrement(self, series, dry_run=False):
- """Decrement a series to the previous version and delete the branch
-
- Args:
- series (str): Name of series to use, or None to use current branch
- dry_run (bool): True to do a dry run
- """
- ser = self._parse_series(series)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
-
- max_vers = self._series_max_version(ser.idnum)
- if max_vers < 2:
- raise ValueError(f"Series '{ser.name}' only has one version")
-
- tout.info(f"Removing series '{ser.name}' v{max_vers}")
-
- new_max = max_vers - 1
-
- repo = pygit2.Repository(self.gitdir)
- if not dry_run:
- name = self._get_branch_name(ser.name, new_max)
- branch = repo.lookup_branch(name)
- try:
- repo.checkout(branch)
- except pygit2.errors.GitError:
- tout.warning(f"Failed to checkout branch {name}")
- raise
-
- del_name = f'{ser.name}{max_vers}'
- del_branch = repo.lookup_branch(del_name)
- branch_oid = del_branch.peel(pygit2.enums.ObjectType.COMMIT).oid
- del_branch.delete()
- print(f"Deleted branch '{del_name}' {oid(branch_oid)}")
-
- self.db.ser_ver_remove(ser.idnum, max_vers)
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- def increment(self, series_name, dry_run=False):
- """Increment a series to the next version and create a new branch
-
- Args:
- series_name (str): Name of series to use, or None to use current
- branch
- dry_run (bool): True to do a dry run
- """
- ser = self._parse_series(series_name)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
-
- max_vers = self._series_max_version(ser.idnum)
-
- branch_name = self._get_branch_name(ser.name, max_vers)
- on_branch = gitutil.get_branch(self.gitdir) == branch_name
- svid = self.get_series_svid(ser.idnum, max_vers)
- pwc = self.get_pcommit_dict(svid)
- count = len(pwc.values())
- series = patchstream.get_metadata(branch_name, 0, count,
- git_dir=self.gitdir)
- tout.info(f"Increment '{ser.name}' v{max_vers}: {count} patches")
-
- # Create a new branch
- vers = max_vers + 1
- new_name = self._join_name_version(ser.name, vers)
-
- self.update_series(branch_name, series, max_vers, new_name, dry_run,
- add_vers=vers, switch=on_branch)
-
- old_svid = self.get_series_svid(ser.idnum, max_vers)
- pcd = self.get_pcommit_dict(old_svid)
-
- svid = self.db.ser_ver_add(ser.idnum, vers)
- self.db.pcommit_add_list(svid, pcd.values())
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- # repo.head.set_target(amended)
- tout.info(f'Added new branch {new_name}')
- if dry_run:
- tout.info('Dry run completed')
-
- def link_set(self, series_name, version, link, update_commit):
- """Add / update a series-links link for a series
-
- Args:
- series_name (str): Name of series to use, or None to use current
- branch
- version (int): Version number, or None to detect from name
- link (str): Patchwork link-string for the series
- update_commit (bool): True to update the current commit with the
- link
- """
- ser, version = self._parse_series_and_version(series_name, version)
- self._ensure_version(ser, version)
-
- self._set_link(ser.idnum, ser.name, version, link, update_commit)
- self.commit()
- tout.info(f"Setting link for series '{ser.name}' v{version} to {link}")
-
- def link_get(self, series, version):
- """Get the patchwork link for a version of a series
-
- Args:
- series (str): Name of series to use, or None to use current branch
- version (int): Version number or None for current
-
- Return:
- str: Patchwork link as a string, e.g. '12325'
- """
- ser, version = self._parse_series_and_version(series, version)
- self._ensure_version(ser, version)
- return self.db.ser_ver_get_link(ser.idnum, version)
-
- def link_search(self, pwork, series, version):
- """Search patch for the link for a series
-
- Returns either the single match, or None, in which case the second part
- of the tuple is filled in
-
- Args:
- pwork (Patchwork): Patchwork object to use
- series (str): Series name to search for, or None for current series
- that is checked out
- version (int): Version to search for, or None for current version
- detected from branch name
-
- Returns:
- tuple:
- int: ID of the series found, or None
- list of possible matches, or None, each a dict:
- 'id': series ID
- 'name': series name
- str: series name
- int: series version
- str: series description
- """
- _, ser, version, _, _, _, _, _ = self._get_patches(series, version)
-
- if not ser.desc:
- raise ValueError(f"Series '{ser.name}' has an empty description")
-
- pws, options = self.loop.run_until_complete(pwork.find_series(
- ser, version))
- return pws, options, ser.name, version, ser.desc
-
- def link_auto(self, pwork, series, version, update_commit, wait_s=0):
- """Automatically find a series link by looking in patchwork
-
- Args:
- pwork (Patchwork): Patchwork object to use
- series (str): Series name to search for, or None for current series
- that is checked out
- version (int): Version to search for, or None for current version
- detected from branch name
- update_commit (bool): True to update the current commit with the
- link
- wait_s (int): Number of seconds to wait for the autolink to succeed
- """
- start = self.get_time()
- stop = start + wait_s
- sleep_time = 5
- while True:
- pws, options, name, version, desc = self.link_search(
- pwork, series, version)
- if pws:
- if wait_s:
- tout.info('Link completed after '
- f'{self.get_time() - start} seconds')
- break
-
- print(f"Possible matches for '{name}' v{version} desc '{desc}':")
- print(' Link Version Description')
- for opt in options:
- print(f"{opt['id']:6} {opt['version']:7} {opt['name']}")
- if not wait_s or self.get_time() > stop:
- delay = f' after {wait_s} seconds' if wait_s else ''
- raise ValueError(f"Cannot find series '{desc}{delay}'")
-
- self.sleep(sleep_time)
-
- self.link_set(name, version, pws, update_commit)
-
- def link_auto_all(self, pwork, update_commit, link_all_versions,
- replace_existing, dry_run, show_summary=True):
- """Automatically find a series link by looking in patchwork
-
- Args:
- pwork (Patchwork): Patchwork object to use
- update_commit (bool): True to update the current commit with the
- link
- link_all_versions (bool): True to sync all versions of a series,
- False to sync only the latest version
- replace_existing (bool): True to sync a series even if it already
- has a link
- dry_run (bool): True to do a dry run
- show_summary (bool): True to show a summary of how things went
-
- Return:
- OrderedDict of summary info:
- key (int): ser_ver ID
- value (AUTOLINK): result of autolinking on this ser_ver
- """
- sdict = self.db.series_get_dict_by_id()
- all_ser_vers = self._get_autolink_dict(sdict, link_all_versions)
-
- # Get rid of things without a description
- valid = {}
- state = {}
- no_desc = 0
- not_found = 0
- updated = 0
- failed = 0
- already = 0
- for svid, (ser_id, name, version, link, desc) in all_ser_vers.items():
- if link and not replace_existing:
- state[svid] = f'already:{link}'
- already += 1
- elif desc:
- valid[svid] = ser_id, version, link, desc
- else:
- no_desc += 1
- state[svid] = 'missing description'
-
- results, requests = self.loop.run_until_complete(
- pwork.find_series_list(valid))
-
- for svid, ser_id, link, _ in results:
- if link:
- version = all_ser_vers[svid][2]
- if self._set_link(ser_id, sdict[ser_id].name, version,
- link, update_commit, dry_run=dry_run):
- updated += 1
- state[svid] = f'linked:{link}'
- else:
- failed += 1
- state[svid] = 'failed'
- else:
- not_found += 1
- state[svid] = 'not found'
-
- # Create a summary sorted by name and version
- summary = OrderedDict()
- for svid in sorted(all_ser_vers, key=lambda k: all_ser_vers[k][1:2]):
- _, name, version, link, ser = all_ser_vers[svid]
- summary[svid] = AUTOLINK(name, version, link, ser.desc,
- state[svid])
-
- if show_summary:
- msg = f'{updated} series linked'
- if already:
- msg += f', {already} already linked'
- if not_found:
- msg += f', {not_found} not found'
- if no_desc:
- msg += f', {no_desc} missing description'
- if failed:
- msg += f', {failed} updated failed'
- tout.info(msg + f' ({requests} requests)')
-
- tout.info('')
- tout.info(f"{'Name':15} Version {'Description':40} Result")
- border = f"{'-' * 15} ------- {'-' * 40} {'-' * 15}"
- tout.info(border)
- for name, version, link, desc, state in summary.values():
- bright = True
- if state.startswith('already'):
- col = self.col.GREEN
- bright = False
- elif state.startswith('linked'):
- col = self.col.MAGENTA
- else:
- col = self.col.RED
- col_state = self.col.build(col, state, bright)
- tout.info(f"{name:16.16} {version:7} {desc or '':40.40} "
- f'{col_state}')
- tout.info(border)
- if dry_run:
- tout.info('Dry run completed')
-
- return summary
-
- def series_list(self):
- """List all series
-
- Lines all series along with their description, number of patches
- accepted and the available versions
- """
- sdict = self.db.series_get_dict()
- print(f"{'Name':15} {'Description':40} Accepted Versions")
- border = f"{'-' * 15} {'-' * 40} -------- {'-' * 15}"
- print(border)
- for name in sorted(sdict):
- ser = sdict[name]
- versions = self._get_version_list(ser.idnum)
- stat = self._series_get_version_stats(
- ser.idnum, self._series_max_version(ser.idnum))[0]
-
- vlist = ' '.join([str(ver) for ver in sorted(versions)])
-
- print(f'{name:16.16} {ser.desc:41.41} {stat.rjust(8)} {vlist}')
- print(border)
-
- def list_patches(self, series, version, show_commit=False,
- show_patch=False):
- """List patches in a series
-
- Args:
- series (str): Name of series to use, or None to use current branch
- version (int): Version number, or None to detect from name
- show_commit (bool): True to show the commit and diffstate
- show_patch (bool): True to show the patch
- """
- branch, series, version, pwc, name, _, cover_id, num_comments = (
- self._get_patches(series, version))
- with terminal.pager():
- state_totals = defaultdict(int)
- self._list_patches(branch, pwc, series, name, cover_id,
- num_comments, show_commit, show_patch, True,
- state_totals)
-
- def mark(self, in_name, allow_marked=False, dry_run=False):
- """Add Change-Id tags to a series
-
- Args:
- in_name (str): Name of the series to unmark
- allow_marked (bool): Allow commits to be (already) marked
- dry_run (bool): True to do a dry run, restoring the original tree
- afterwards
-
- Return:
- pygit.oid: oid of the new branch
- """
- name, ser, _, _ = self.prep_series(in_name)
- tout.info(f"Marking series '{name}': allow_marked {allow_marked}")
-
- if not allow_marked:
- bad = []
- for cmt in ser.commits:
- if cmt.change_id:
- bad.append(cmt)
- if bad:
- print(f'{len(bad)} commit(s) already have marks')
- for cmt in bad:
- print(f' - {oid(cmt.hash)} {cmt.subject}')
- raise ValueError(
- f'Marked commits {len(bad)}/{len(ser.commits)}')
- new_oid = self._mark_series(in_name, ser, dry_run=dry_run)
-
- if dry_run:
- tout.info('Dry run completed')
- return new_oid
-
- def unmark(self, name, allow_unmarked=False, dry_run=False):
- """Remove Change-Id tags from a series
-
- Args:
- name (str): Name of the series to unmark
- allow_unmarked (bool): Allow commits to be (already) unmarked
- dry_run (bool): True to do a dry run, restoring the original tree
- afterwards
-
- Return:
- pygit.oid: oid of the new branch
- """
- name, ser, _, _ = self.prep_series(name)
- tout.info(
- f"Unmarking series '{name}': allow_unmarked {allow_unmarked}")
-
- if not allow_unmarked:
- bad = []
- for cmt in ser.commits:
- if not cmt.change_id:
- bad.append(cmt)
- if bad:
- print(f'{len(bad)} commit(s) are missing marks')
- for cmt in bad:
- print(f' - {oid(cmt.hash)} {cmt.subject}')
- raise ValueError(
- f'Unmarked commits {len(bad)}/{len(ser.commits)}')
- vals = None
- for vals in self.process_series(name, ser, dry_run=dry_run):
- if cser_helper.CHANGE_ID_TAG in vals.msg:
- lines = vals.msg.splitlines()
- updated = [line for line in lines
- if not line.startswith(cser_helper.CHANGE_ID_TAG)]
- vals.msg = '\n'.join(updated)
-
- tout.detail(" - removing mark")
- vals.info = 'unmarked'
- else:
- vals.info = 'no mark'
-
- if dry_run:
- tout.info('Dry run completed')
- return vals.oid
-
- def open(self, pwork, name, version):
- """Open the patchwork page for a series
-
- Args:
- pwork (Patchwork): Patchwork object to use
- name (str): Name of series to open
- version (str): Version number to open
- """
- ser, version = self._parse_series_and_version(name, version)
- link = self.link_get(ser.name, version)
- pwork.url = 'https://patchwork.ozlabs.org'
- url = self.loop.run_until_complete(pwork.get_series_url(link))
- print(f'Opening {url}')
-
- # With Firefox, GTK produces lots of warnings, so suppress them
- # Gtk-Message: 06:48:20.692: Failed to load module "xapp-gtk3-module"
- # Gtk-Message: 06:48:20.692: Not loading module "atk-bridge": The
- # functionality is provided by GTK natively. Please try to not load it.
- # Gtk-Message: 06:48:20.692: Failed to load module "appmenu-gtk-module"
- # Gtk-Message: 06:48:20.692: Failed to load module "appmenu-gtk-module"
- # [262145, Main Thread] WARNING: GTK+ module /snap/firefox/5987/
- # gnome-platform/usr/lib/gtk-2.0/modules/libcanberra-gtk-module.so
- # cannot be loaded.
- # GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same
- # process # is not supported.: 'glib warning', file /build/firefox/
- # parts/firefox/build/toolkit/xre/nsSigHandlers.cpp:201
- #
- # (firefox_firefox:262145): Gtk-WARNING **: 06:48:20.728: GTK+ module
- # /snap/firefox/5987/gnome-platform/usr/lib/gtk-2.0/modules/
- # libcanberra-gtk-module.so cannot be loaded.
- # GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same
- # process is not supported.
- # Gtk-Message: 06:48:20.728: Failed to load module
- # "canberra-gtk-module"
- # [262145, Main Thread] WARNING: GTK+ module /snap/firefox/5987/
- # gnome-platform/usr/lib/gtk-2.0/modules/libcanberra-gtk-module.so
- # cannot be loaded.
- # GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same
- # process is not supported.: 'glib warning', file /build/firefox/
- # parts/firefox/build/toolkit/xre/nsSigHandlers.cpp:201
- #
- # (firefox_firefox:262145): Gtk-WARNING **: 06:48:20.729: GTK+ module
- # /snap/firefox/5987/gnome-platform/usr/lib/gtk-2.0/modules/
- # libcanberra-gtk-module.so cannot be loaded.
- # GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same
- # process is not supported.
- # Gtk-Message: 06:48:20.729: Failed to load module
- # "canberra-gtk-module"
- # ATTENTION: default value of option mesa_glthread overridden by
- # environment.
- cros_subprocess.Popen(['xdg-open', url])
-
- def progress(self, series, show_all_versions, list_patches):
- """Show progress information for all versions in a series
-
- Args:
- series (str): Name of series to use, or None to show progress for
- all series
- show_all_versions (bool): True to show all versions of a series,
- False to show only the final version
- list_patches (bool): True to list all patches for each series,
- False to just show the series summary on a single line
- """
- with terminal.pager():
- state_totals = defaultdict(int)
- if series is not None:
- _, _, need_scan = self._progress_one(
- self._parse_series(series), show_all_versions,
- list_patches, state_totals)
- if need_scan:
- tout.warning(
- 'Inconsistent commit-subject: Please use '
- "'patman series -s <branch> scan' to resolve this")
- return
-
- total_patches = 0
- total_series = 0
- sdict = self.db.series_get_dict()
- border = None
- total_need_scan = 0
- if not list_patches:
- print(self.col.build(
- self.col.MAGENTA,
- f"{'Name':16} {'Description':41} Count {'Status'}"))
- border = f"{'-' * 15} {'-' * 40} ----- {'-' * 15}"
- print(border)
- for name in sorted(sdict):
- ser = sdict[name]
- num_series, num_patches, need_scan = self._progress_one(
- ser, show_all_versions, list_patches, state_totals)
- total_need_scan += need_scan
- if list_patches:
- print()
- total_series += num_series
- total_patches += num_patches
- if not list_patches:
- print(border)
- total = f'{total_series} series'
- out = ''
- for state, freq in state_totals.items():
- out += ' ' + self._build_col(state, f'{freq}:')[0]
- if total_need_scan:
- out = '*' + out[1:]
-
- print(f"{total:15} {'':40} {total_patches:5} {out}")
- if total_need_scan:
- tout.info(
- f'Series marked * ({total_need_scan}) have commit '
- 'subjects which mismatch their patches and need to be '
- 'scanned')
-
- def project_set(self, pwork, name, quiet=False):
- """Set the name of the project
-
- Args:
- pwork (Patchwork): Patchwork object to use
- name (str): Name of the project to use in patchwork
- quiet (bool): True to skip writing the message
- """
- res = self.loop.run_until_complete(pwork.get_projects())
- proj_id = None
- link_name = None
- for proj in res:
- if proj['name'] == name:
- proj_id = proj['id']
- link_name = proj['link_name']
- if not proj_id:
- raise ValueError(f"Unknown project name '{name}'")
- self.db.settings_update(name, proj_id, link_name)
- self.commit()
- if not quiet:
- tout.info(f"Project '{name}' patchwork-ID {proj_id} "
- f'link-name {link_name}')
-
- def project_get(self):
- """Get the details of the project
-
- Returns:
- tuple or None if there are no settings:
- name (str): Project name, e.g. 'U-Boot'
- proj_id (int): Patchworks project ID for this project
- link_name (str): Patchwork's link-name for the project
- """
- return self.db.settings_get()
-
- def remove(self, name, dry_run=False):
- """Remove a series from the database
-
- Args:
- name (str): Name of series to remove, or None to use current one
- dry_run (bool): True to do a dry run
- """
- ser = self._parse_series(name)
- name = ser.name
- if not ser.idnum:
- raise ValueError(f"No such series '{name}'")
-
- self.db.ser_ver_remove(ser.idnum, None)
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- self.commit()
- tout.info(f"Removed series '{name}'")
- if dry_run:
- tout.info('Dry run completed')
-
- def rename(self, series, name, dry_run=False):
- """Rename a series
-
- Renames a series and changes the name of any branches which match
- versions present in the database
-
- Args:
- series (str): Name of series to use, or None to use current branch
- name (str): new name to use (must not include version number)
- dry_run (bool): True to do a dry run
- """
- old_ser, _ = self._parse_series_and_version(series, None)
- if not old_ser.idnum:
- raise ValueError(f"Series '{old_ser.name}' not found in database")
- if old_ser.name != series:
- raise ValueError(f"Invalid series name '{series}': "
- 'did you use the branch name?')
- chk, _ = cser_helper.split_name_version(name)
- if chk != name:
- raise ValueError(
- f"Invalid series name '{name}': did you use the branch name?")
- if chk == old_ser.name:
- raise ValueError(
- f"Cannot rename series '{old_ser.name}' to itself")
- if self.get_series_by_name(name):
- raise ValueError(f"Cannot rename: series '{name}' already exists")
-
- versions = self._get_version_list(old_ser.idnum)
- missing = []
- exists = []
- todo = {}
- for ver in versions:
- ok = True
- old_branch = self._get_branch_name(old_ser.name, ver)
- if not gitutil.check_branch(old_branch, self.gitdir):
- missing.append(old_branch)
- ok = False
-
- branch = self._get_branch_name(name, ver)
- if gitutil.check_branch(branch, self.gitdir):
- exists.append(branch)
- ok = False
-
- if ok:
- todo[ver] = [old_branch, branch]
-
- if missing or exists:
- msg = 'Cannot rename'
- if missing:
- msg += f": branches missing: {', '.join(missing)}"
- if exists:
- msg += f": branches exist: {', '.join(exists)}"
- raise ValueError(msg)
-
- for old_branch, branch in todo.values():
- tout.info(f"Renaming branch '{old_branch}' to '{branch}'")
- if not dry_run:
- gitutil.rename_branch(old_branch, branch, self.gitdir)
-
- # Change the series name; nothing needs to change in ser_ver
- self.db.series_set_name(old_ser.idnum, name)
-
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- tout.info(f"Renamed series '{series}' to '{name}'")
- if dry_run:
- tout.info('Dry run completed')
-
- def scan(self, branch_name, mark=False, allow_unmarked=False, end=None,
- dry_run=False):
- """Scan a branch and make updates to the database if it has changed
-
- Args:
- branch_name (str): Name of branch to sync, or None for current one
- mark (str): True to mark each commit with a change ID
- allow_unmarked (str): True to not require each commit to be marked
- end (str): Add only commits up to but exclu
- dry_run (bool): True to do a dry run
- """
- def _show_item(oper, seq, subject):
- col = None
- if oper == '+':
- col = self.col.GREEN
- elif oper == '-':
- col = self.col.RED
- out = self.col.build(col, subject) if col else subject
- tout.info(f'{oper} {seq:3} {out}')
-
- name, ser, version, msg = self.prep_series(branch_name, end)
- svid = self.get_ser_ver(ser.idnum, version).idnum
- pcdict = self.get_pcommit_dict(svid)
-
- tout.info(
- f"Syncing series '{name}' v{version}: mark {mark} "
- f'allow_unmarked {allow_unmarked}')
- if msg:
- tout.info(msg)
-
- ser = self._handle_mark(name, ser, version, mark, allow_unmarked,
- False, dry_run)
-
- # First check for new patches that are not in the database
- to_add = dict(enumerate(ser.commits))
- for pcm in pcdict.values():
- tout.debug(f'pcm {pcm.subject}')
- i = self._find_matched_commit(to_add, pcm)
- if i is not None:
- del to_add[i]
-
- # Now check for patches in the database that are not in the branch
- to_remove = dict(enumerate(pcdict.values()))
- for cmt in ser.commits:
- tout.debug(f'cmt {cmt.subject}')
- i = self._find_matched_patch(to_remove, cmt)
- if i is not None:
- del to_remove[i]
-
- for seq, cmt in enumerate(ser.commits):
- if seq in to_remove:
- _show_item('-', seq, to_remove[seq].subject)
- del to_remove[seq]
- if seq in to_add:
- _show_item('+', seq, to_add[seq].subject)
- del to_add[seq]
- else:
- _show_item(' ', seq, cmt.subject)
- seq = len(ser.commits)
- for cmt in to_add.items():
- _show_item('+', seq, cmt.subject)
- seq += 1
- for seq, pcm in to_remove.items():
- _show_item('+', seq, pcm.subject)
-
- self.db.pcommit_delete(svid)
- self._add_series_commits(ser, svid)
- if not dry_run:
- self.commit()
- else:
- self.rollback()
- tout.info('Dry run completed')
-
- def send(self, pwork, name, autolink, autolink_wait, args):
- """Send out a series
-
- Args:
- pwork (Patchwork): Patchwork object to use
- name (str): Series name to search for, or None for current series
- that is checked out
- autolink (bool): True to auto-link the series after sending
- args (argparse.Namespace): 'send' arguments provided
- autolink_wait (int): Number of seconds to wait for the autolink to
- succeed
- """
- ser, version = self._parse_series_and_version(name, None)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
-
- args.branch = self._get_branch_name(ser.name, version)
- likely_sent = send.send(args, git_dir=self.gitdir, cwd=self.topdir)
-
- if likely_sent and autolink:
- print(f'Autolinking with Patchwork ({autolink_wait} seconds)')
- self.link_auto(pwork, name, version, True, wait_s=autolink_wait)
-
- def archive(self, series):
- """Archive a series
-
- Args:
- series (str): Name of series to use, or None to use current branch
- """
- ser = self._parse_series(series, include_archived=True)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
-
- svlist = self.db.ser_ver_get_for_series(ser.idnum)
-
- # Figure out the tags we will create
- tag_info = {}
- now = self.get_now()
- now_str = now.strftime('%d%b%y').lower()
- for svi in svlist:
- name = self._get_branch_name(ser.name, svi.version)
- if not gitutil.check_branch(name, git_dir=self.gitdir):
- raise ValueError(f"No branch named '{name}'")
- tag_info[svi.version] = [svi.idnum, name, f'{name}-{now_str}']
-
- # Create the tags
- repo = pygit2.Repository(self.gitdir)
- for _, (idnum, name, tag_name) in tag_info.items():
- commit = repo.revparse_single(name)
- repo.create_tag(tag_name, commit.hex,
- pygit2.enums.ObjectType.COMMIT,
- commit.author, commit.message)
-
- # Update the database
- for idnum, name, tag_name in tag_info.values():
- self.db.ser_ver_set_archive_tag(idnum, tag_name)
-
- # Delete the branches
- for idnum, name, tag_name in tag_info.values():
- # Detach HEAD from the branch if pointing to this branch
- commit = repo.revparse_single(name)
- if repo.head.target == commit.oid:
- repo.set_head(commit.oid)
-
- repo.branches.delete(name)
-
- self.db.series_set_archived(ser.idnum, True)
- self.commit()
-
- def unarchive(self, series):
- """Unarchive a series
-
- Args:
- series (str): Name of series to use, or None to use current branch
- """
- ser = self._parse_series(series, include_archived=True)
- if not ser.idnum:
- raise ValueError(f"Series '{ser.name}' not found in database")
- self.db.series_set_archived(ser.idnum, False)
-
- svlist = self.db.ser_ver_get_for_series(ser.idnum)
-
- # Collect the tags
- repo = pygit2.Repository(self.gitdir)
- tag_info = {}
- for svi in svlist:
- name = self._get_branch_name(ser.name, svi.version)
- target = repo.revparse_single(svi.archive_tag)
- tag_info[svi.idnum] = name, svi.archive_tag, target
-
- # Make sure the branches don't exist
- for name, tag_name, tag in tag_info.values():
- if name in repo.branches:
- raise ValueError(
- f"Cannot restore branch '{name}': already exists")
-
- # Recreate the branches
- for name, tag_name, tag in tag_info.values():
- target = repo.get(tag.target)
- repo.branches.create(name, target)
-
- # Delete the tags
- for name, tag_name, tag in tag_info.values():
- repo.references.delete(f'refs/tags/{tag_name}')
-
- # Update the database
- for idnum, (name, tag_name, tag) in tag_info.items():
- self.db.ser_ver_set_archive_tag(idnum, None)
-
- self.commit()
-
- def status(self, pwork, series, version, show_comments,
- show_cover_comments=False):
- """Show the series status from patchwork
-
- Args:
- pwork (Patchwork): Patchwork object to use
- series (str): Name of series to use, or None to use current branch
- version (int): Version number, or None to detect from name
- show_comments (bool): Show all comments on each patch
- show_cover_comments (bool): Show all comments on the cover letter
- """
- branch, series, version, _, _, link, _, _ = self._get_patches(
- series, version)
- if not link:
- raise ValueError(
- f"Series '{series.name}' v{version} has no patchwork link: "
- f"Try 'patman series -s {branch} autolink'")
- status.check_and_show_status(
- series, link, branch, None, False, show_comments,
- show_cover_comments, pwork, self.gitdir)
-
- def summary(self, series):
- """Show summary information for all series
-
- Args:
- series (str): Name of series to use
- """
- print(f"{'Name':17} Status Description")
- print(f"{'-' * 17} {'-' * 6} {'-' * 30}")
- if series is not None:
- self._summary_one(self._parse_series(series))
- return
-
- sdict = self.db.series_get_dict()
- for ser in sdict.values():
- self._summary_one(ser)
-
- def gather(self, pwork, series, version, show_comments,
- show_cover_comments, gather_tags, dry_run=False):
- """Gather any new tags from Patchwork, optionally showing comments
-
- Args:
- pwork (Patchwork): Patchwork object to use
- series (str): Name of series to use, or None to use current branch
- version (int): Version number, or None to detect from name
- show_comments (bool): True to show the comments on each patch
- show_cover_comments (bool): True to show the comments on the cover
- letter
- gather_tags (bool): True to gather review/test tags
- dry_run (bool): True to do a dry run (database is not updated)
- """
- ser, version = self._parse_series_and_version(series, version)
- self._ensure_version(ser, version)
- svid, link = self._get_series_svid_link(ser.idnum, version)
- if not link:
- raise ValueError(
- "No patchwork link is available: use 'patman series autolink'")
- tout.info(
- f"Updating series '{ser.name}' version {version} "
- f"from link '{link}'")
-
- loop = asyncio.get_event_loop()
- with pwork.collect_stats() as stats:
- cover, patches = loop.run_until_complete(self._gather(
- pwork, link, show_cover_comments))
-
- with terminal.pager():
- updated, updated_cover = self._sync_one(
- svid, ser.name, version, show_comments, show_cover_comments,
- gather_tags, cover, patches, dry_run)
- tout.info(f"{updated} patch{'es' if updated != 1 else ''}"
- f"{' and cover letter' if updated_cover else ''} "
- f'updated ({stats.request_count} requests)')
-
- if not dry_run:
- self.commit()
- else:
- self.rollback()
- tout.info('Dry run completed')
-
- def gather_all(self, pwork, show_comments, show_cover_comments,
- sync_all_versions, gather_tags, dry_run=False):
- to_fetch, missing = self._get_fetch_dict(sync_all_versions)
-
- loop = asyncio.get_event_loop()
- result, requests = loop.run_until_complete(self._do_series_sync_all(
- pwork, to_fetch))
-
- with terminal.pager():
- tot_updated = 0
- tot_cover = 0
- add_newline = False
- for (svid, sync), (cover, patches) in zip(to_fetch.items(),
- result):
- if add_newline:
- tout.info('')
- tout.info(f"Syncing '{sync.series_name}' v{sync.version}")
- updated, updated_cover = self._sync_one(
- svid, sync.series_name, sync.version, show_comments,
- show_cover_comments, gather_tags, cover, patches, dry_run)
- tot_updated += updated
- tot_cover += updated_cover
- add_newline = gather_tags
-
- tout.info('')
- tout.info(
- f"{tot_updated} patch{'es' if tot_updated != 1 else ''} and "
- f"{tot_cover} cover letter{'s' if tot_cover != 1 else ''} "
- f'updated, {missing} missing '
- f"link{'s' if missing != 1 else ''} ({requests} requests)")
- if not dry_run:
- self.commit()
- else:
- self.rollback()
- tout.info('Dry run completed')
-
- def upstream_add(self, name, url):
- """Add a new upstream tree
-
- Args:
- name (str): Name of the tree
- url (str): URL for the tree
- """
- self.db.upstream_add(name, url)
- self.commit()
-
- def upstream_list(self):
- """List the upstream repos
-
- Shows a list of the repos, obtained from the database
- """
- udict = self.get_upstream_dict()
-
- for name, items in udict.items():
- url, is_default = items
- default = 'default' if is_default else ''
- print(f'{name:15.15} {default:8} {url}')
-
- def upstream_set_default(self, name):
- """Set the default upstream target
-
- Args:
- name (str): Name of the upstream remote to set as default, or None
- for none
- """
- self.db.upstream_set_default(name)
- self.commit()
-
- def upstream_get_default(self):
- """Get the default upstream target
-
- Return:
- str: Name of the upstream remote to set as default, or None if none
- """
- return self.db.upstream_get_default()
-
- def upstream_delete(self, name):
- """Delete an upstream target
-
- Args:
- name (str): Name of the upstream remote to delete
- """
- self.db.upstream_delete(name)
- self.commit()
-
- def version_remove(self, name, version, dry_run=False):
- """Remove a version of a series from the database
-
- Args:
- name (str): Name of series to remove, or None to use current one
- version (int): Version number to remove
- dry_run (bool): True to do a dry run
- """
- ser, version = self._parse_series_and_version(name, version)
- name = ser.name
-
- versions = self._ensure_version(ser, version)
-
- if versions == [version]:
- raise ValueError(
- f"Series '{ser.name}' only has one version: remove the series")
-
- self.db.ser_ver_remove(ser.idnum, version)
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- tout.info(f"Removed version {version} from series '{name}'")
- if dry_run:
- tout.info('Dry run completed')
-
- def version_change(self, name, version, new_version, dry_run=False):
- """Change a version of a series to be a different version
-
- Args:
- name (str): Name of series to remove, or None to use current one
- version (int): Version number to change
- new_version (int): New version
- dry_run (bool): True to do a dry run
- """
- ser, version = self._parse_series_and_version(name, version)
- name = ser.name
-
- versions = self._ensure_version(ser, version)
- vstr = list(map(str, versions))
- if version not in versions:
- raise ValueError(
- f"Series '{ser.name}' does not have v{version}: "
- f"{' '.join(vstr)}")
-
- if not new_version:
- raise ValueError('Please provide a new version number')
-
- if new_version in versions:
- raise ValueError(
- f"Series '{ser.name}' already has a v{new_version}: "
- f"{' '.join(vstr)}")
-
- new_name = self._join_name_version(ser.name, new_version)
-
- svid = self.get_series_svid(ser.idnum, version)
- pwc = self.get_pcommit_dict(svid)
- count = len(pwc.values())
- series = patchstream.get_metadata(name, 0, count, git_dir=self.gitdir)
-
- self.update_series(name, series, version, new_name, dry_run,
- add_vers=new_version, switch=True)
- self.db.ser_ver_set_version(svid, new_version)
-
- if not dry_run:
- self.commit()
- else:
- self.rollback()
-
- tout.info(f"Changed version {version} in series '{ser.name}' "
- f"to {new_version} named '{new_name}'")
- if dry_run:
- tout.info('Dry run completed')
diff --git a/tools/patman/database.py b/tools/patman/database.py
deleted file mode 100644
index 9c25b04a720..00000000000
--- a/tools/patman/database.py
+++ /dev/null
@@ -1,823 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2025 Simon Glass <sjg at chromium.org>
-#
-"""Handles the patman database
-
-This uses sqlite3 with a local file.
-
-To adjsut the schema, increment LATEST, create a migrate_to_v<x>() function
-and write some code in migrate_to() to call it.
-"""
-
-from collections import namedtuple, OrderedDict
-import os
-import sqlite3
-
-from u_boot_pylib import tools
-from u_boot_pylib import tout
-from patman.series import Series
-
-# Schema version (version 0 means there is no database yet)
-LATEST = 4
-
-# Information about a series/version record
-SerVer = namedtuple(
- 'SER_VER',
- 'idnum,series_id,version,link,cover_id,cover_num_comments,name,'
- 'archive_tag')
-
-# Record from the pcommit table:
-# idnum (int): record ID
-# seq (int): Patch sequence in series (0 is first)
-# subject (str): patch subject
-# svid (int): ID of series/version record in ser_ver table
-# change_id (str): Change-ID value
-# state (str): Current status in patchwork
-# patch_id (int): Patchwork's patch ID for this patch
-# num_comments (int): Number of comments attached to the commit
-Pcommit = namedtuple(
- 'PCOMMIT',
- 'idnum,seq,subject,svid,change_id,state,patch_id,num_comments')
-
-
-class Database:
- """Database of information used by patman"""
-
- # dict of databases:
- # key: filename
- # value: Database object
- instances = {}
-
- def __init__(self, db_path):
- """Set up a new database object
-
- Args:
- db_path (str): Path to the database
- """
- if db_path in Database.instances:
- # Two connections to the database can cause:
- # sqlite3.OperationalError: database is locked
- raise ValueError(f"There is already a database for '{db_path}'")
- self.con = None
- self.cur = None
- self.db_path = db_path
- self.is_open = False
- Database.instances[db_path] = self
-
- @staticmethod
- def get_instance(db_path):
- """Get the database instance for a path
-
- This is provides to ensure that different callers can obtain the
- same database object when accessing the same database file.
-
- Args:
- db_path (str): Path to the database
-
- Return:
- Database: Database instance, which is created if necessary
- """
- db = Database.instances.get(db_path)
- if db:
- return db, False
- return Database(db_path), True
-
- def start(self):
- """Open the database read for use, migrate to latest schema"""
- self.open_it()
- self.migrate_to(LATEST)
-
- def open_it(self):
- """Open the database, creating it if necessary"""
- if self.is_open:
- raise ValueError('Already open')
- if not os.path.exists(self.db_path):
- tout.warning(f'Creating new database {self.db_path}')
- self.con = sqlite3.connect(self.db_path)
- self.cur = self.con.cursor()
- self.is_open = True
-
- def close(self):
- """Close the database"""
- if not self.is_open:
- raise ValueError('Already closed')
- self.con.close()
- self.cur = None
- self.con = None
- self.is_open = False
-
- def create_v1(self):
- """Create a database with the v1 schema"""
- self.cur.execute(
- 'CREATE TABLE series (id INTEGER PRIMARY KEY AUTOINCREMENT,'
- 'name UNIQUE, desc, archived BIT)')
-
- # Provides a series_id/version pair, which is used to refer to a
- # particular series version sent to patchwork. This stores the link
- # to patchwork
- self.cur.execute(
- 'CREATE TABLE ser_ver (id INTEGER PRIMARY KEY AUTOINCREMENT,'
- 'series_id INTEGER, version INTEGER, link,'
- 'FOREIGN KEY (series_id) REFERENCES series (id))')
-
- self.cur.execute(
- 'CREATE TABLE upstream (name UNIQUE, url, is_default BIT)')
-
- # change_id is the Change-Id
- # patch_id is the ID of the patch on the patchwork server
- self.cur.execute(
- 'CREATE TABLE pcommit (id INTEGER PRIMARY KEY AUTOINCREMENT,'
- 'svid INTEGER, seq INTEGER, subject, patch_id INTEGER, '
- 'change_id, state, num_comments INTEGER, '
- 'FOREIGN KEY (svid) REFERENCES ser_ver (id))')
-
- self.cur.execute(
- 'CREATE TABLE settings (name UNIQUE, proj_id INT, link_name)')
-
- def _migrate_to_v2(self):
- """Add a schema_version table"""
- self.cur.execute('CREATE TABLE schema_version (version INTEGER)')
-
- def _migrate_to_v3(self):
- """Store the number of cover-letter comments in the schema"""
- self.cur.execute('ALTER TABLE ser_ver ADD COLUMN cover_id')
- self.cur.execute('ALTER TABLE ser_ver ADD COLUMN cover_num_comments '
- 'INTEGER')
- self.cur.execute('ALTER TABLE ser_ver ADD COLUMN name')
-
- def _migrate_to_v4(self):
- """Add an archive tag for each ser_ver"""
- self.cur.execute('ALTER TABLE ser_ver ADD COLUMN archive_tag')
-
- def migrate_to(self, dest_version):
- """Migrate the database to the selected version
-
- Args:
- dest_version (int): Version to migrate to
- """
- while True:
- version = self.get_schema_version()
- if version == dest_version:
- break
-
- self.close()
- tools.write_file(f'{self.db_path}old.v{version}',
- tools.read_file(self.db_path))
-
- version += 1
- tout.info(f'Update database to v{version}')
- self.open_it()
- if version == 1:
- self.create_v1()
- elif version == 2:
- self._migrate_to_v2()
- elif version == 3:
- self._migrate_to_v3()
- elif version == 4:
- self._migrate_to_v4()
-
- # Save the new version if we have a schema_version table
- if version > 1:
- self.cur.execute('DELETE FROM schema_version')
- self.cur.execute(
- 'INSERT INTO schema_version (version) VALUES (?)',
- (version,))
- self.commit()
-
- def get_schema_version(self):
- """Get the version of the database's schema
-
- Return:
- int: Database version, 0 means there is no data; anything less than
- LATEST means the schema is out of date and must be updated
- """
- # If there is no database at all, assume v0
- version = 0
- try:
- self.cur.execute('SELECT name FROM series')
- except sqlite3.OperationalError:
- return 0
-
- # If there is no schema, assume v1
- try:
- self.cur.execute('SELECT version FROM schema_version')
- version = self.cur.fetchone()[0]
- except sqlite3.OperationalError:
- return 1
- return version
-
- def execute(self, query, parameters=()):
- """Execute a database query
-
- Args:
- query (str): Query string
- parameters (list of values): Parameters to pass
-
- Return:
-
- """
- return self.cur.execute(query, parameters)
-
- def commit(self):
- """Commit changes to the database"""
- self.con.commit()
-
- def rollback(self):
- """Roll back changes to the database"""
- self.con.rollback()
-
- def lastrowid(self):
- """Get the last row-ID reported by the database
-
- Return:
- int: Value for lastrowid
- """
- return self.cur.lastrowid
-
- def rowcount(self):
- """Get the row-count reported by the database
-
- Return:
- int: Value for rowcount
- """
- return self.cur.rowcount
-
- def _get_series_list(self, include_archived):
- """Get a list of Series objects from the database
-
- Args:
- include_archived (bool): True to include archives series
-
- Return:
- list of Series
- """
- res = self.execute(
- 'SELECT id, name, desc FROM series ' +
- ('WHERE archived = 0' if not include_archived else ''))
- return [Series.from_fields(idnum=idnum, name=name, desc=desc)
- for idnum, name, desc in res.fetchall()]
-
- # series functions
-
- def series_get_dict_by_id(self, include_archived=False):
- """Get a dict of Series objects from the database
-
- Args:
- include_archived (bool): True to include archives series
-
- Return:
- OrderedDict:
- key: series ID
- value: Series with idnum, name and desc filled out
- """
- sdict = OrderedDict()
- for ser in self._get_series_list(include_archived):
- sdict[ser.idnum] = ser
- return sdict
-
- def series_find_by_name(self, name, include_archived=False):
- """Find a series and return its details
-
- Args:
- name (str): Name to search for
- include_archived (bool): True to include archives series
-
- Returns:
- idnum, or None if not found
- """
- res = self.execute(
- 'SELECT id FROM series WHERE name = ?' +
- ('AND archived = 0' if not include_archived else ''), (name,))
- recs = res.fetchall()
-
- # This shouldn't happen
- assert len(recs) <= 1, 'Expected one match, but multiple found'
-
- if len(recs) != 1:
- return None
- return recs[0][0]
-
- def series_get_info(self, idnum):
- """Get information for a series from the database
-
- Args:
- idnum (int): Series ID to look up
-
- Return: tuple:
- str: Series name
- str: Series description
-
- Raises:
- ValueError: Series is not found
- """
- res = self.execute('SELECT name, desc FROM series WHERE id = ?',
- (idnum,))
- recs = res.fetchall()
- if len(recs) != 1:
- raise ValueError(f'No series found (id {idnum} len {len(recs)})')
- return recs[0]
-
- def series_get_dict(self, include_archived=False):
- """Get a dict of Series objects from the database
-
- Args:
- include_archived (bool): True to include archives series
-
- Return:
- OrderedDict:
- key: series name
- value: Series with idnum, name and desc filled out
- """
- sdict = OrderedDict()
- for ser in self._get_series_list(include_archived):
- sdict[ser.name] = ser
- return sdict
-
- def series_get_version_list(self, series_idnum):
- """Get a list of the versions available for a series
-
- Args:
- series_idnum (int): ID of series to look up
-
- Return:
- str: List of versions, which may be empty if the series is in the
- process of being added
- """
- res = self.execute('SELECT version FROM ser_ver WHERE series_id = ?',
- (series_idnum,))
- return [x[0] for x in res.fetchall()]
-
- def series_get_max_version(self, series_idnum):
- """Get the highest version number available for a series
-
- Args:
- series_idnum (int): ID of series to look up
-
- Return:
- int: Maximum version number
- """
- res = self.execute(
- 'SELECT MAX(version) FROM ser_ver WHERE series_id = ?',
- (series_idnum,))
- return res.fetchall()[0][0]
-
- def series_get_all_max_versions(self):
- """Find the latest version of all series
-
- Return: list of:
- int: ser_ver ID
- int: series ID
- int: Maximum version
- """
- res = self.execute(
- 'SELECT id, series_id, MAX(version) FROM ser_ver '
- 'GROUP BY series_id')
- return res.fetchall()
-
- def series_add(self, name, desc):
- """Add a new series record
-
- The new record is set to not archived
-
- Args:
- name (str): Series name
- desc (str): Series description
-
- Return:
- int: ID num of the new series record
- """
- self.execute(
- 'INSERT INTO series (name, desc, archived) '
- f"VALUES ('{name}', '{desc}', 0)")
- return self.lastrowid()
-
- def series_remove(self, idnum):
- """Remove a series from the database
-
- The series must exist
-
- Args:
- idnum (int): ID num of series to remove
- """
- self.execute('DELETE FROM series WHERE id = ?', (idnum,))
- assert self.rowcount() == 1
-
- def series_remove_by_name(self, name):
- """Remove a series from the database
-
- Args:
- name (str): Name of series to remove
-
- Raises:
- ValueError: Series does not exist (database is rolled back)
- """
- self.execute('DELETE FROM series WHERE name = ?', (name,))
- if self.rowcount() != 1:
- self.rollback()
- raise ValueError(f"No such series '{name}'")
-
- def series_set_archived(self, series_idnum, archived):
- """Update archive flag for a series
-
- Args:
- series_idnum (int): ID num of the series
- archived (bool): Whether to mark the series as archived or
- unarchived
- """
- self.execute(
- 'UPDATE series SET archived = ? WHERE id = ?',
- (archived, series_idnum))
-
- def series_set_name(self, series_idnum, name):
- """Update name for a series
-
- Args:
- series_idnum (int): ID num of the series
- name (str): new name to use
- """
- self.execute(
- 'UPDATE series SET name = ? WHERE id = ?', (name, series_idnum))
-
- # ser_ver functions
-
- def ser_ver_get_link(self, series_idnum, version):
- """Get the link for a series version
-
- Args:
- series_idnum (int): ID num of the series
- version (int): Version number to search for
-
- Return:
- str: Patchwork link as a string, e.g. '12325', or None if none
-
- Raises:
- ValueError: Multiple matches are found
- """
- res = self.execute(
- 'SELECT link FROM ser_ver WHERE '
- f"series_id = {series_idnum} AND version = '{version}'")
- recs = res.fetchall()
- if not recs:
- return None
- if len(recs) > 1:
- raise ValueError('Expected one match, but multiple matches found')
- return recs[0][0]
-
- def ser_ver_set_link(self, series_idnum, version, link):
- """Set the link for a series version
-
- Args:
- series_idnum (int): ID num of the series
- version (int): Version number to search for
- link (str): Patchwork link for the ser_ver
-
- Return:
- bool: True if the record was found and updated, else False
- """
- if link is None:
- link = ''
- self.execute(
- 'UPDATE ser_ver SET link = ? WHERE series_id = ? AND version = ?',
- (str(link), series_idnum, version))
- return self.rowcount() != 0
-
- def ser_ver_set_info(self, info):
- """Set the info for a series version
-
- Args:
- info (SER_VER): Info to set. Only two options are supported:
- 1: svid,cover_id,cover_num_comments,name
- 2: svid,name
-
- Return:
- bool: True if the record was found and updated, else False
- """
- assert info.idnum is not None
- if info.cover_id:
- assert info.series_id is None
- self.execute(
- 'UPDATE ser_ver SET cover_id = ?, cover_num_comments = ?, '
- 'name = ? WHERE id = ?',
- (info.cover_id, info.cover_num_comments, info.name,
- info.idnum))
- else:
- assert not info.cover_id
- assert not info.cover_num_comments
- assert not info.series_id
- assert not info.version
- assert not info.link
- self.execute('UPDATE ser_ver SET name = ? WHERE id = ?',
- (info.name, info.idnum))
-
- return self.rowcount() != 0
-
- def ser_ver_set_version(self, svid, version):
- """Sets the version for a ser_ver record
-
- Args:
- svid (int): Record ID to update
- version (int): Version number to add
-
- Raises:
- ValueError: svid was not found
- """
- self.execute(
- 'UPDATE ser_ver SET version = ? WHERE id = ?', (version, svid))
- if self.rowcount() != 1:
- raise ValueError(f'No ser_ver updated (svid {svid})')
-
- def ser_ver_set_archive_tag(self, svid, tag):
- """Sets the archive tag for a ser_ver record
-
- Args:
- svid (int): Record ID to update
- tag (tag): Tag to add
-
- Raises:
- ValueError: svid was not found
- """
- self.execute(
- 'UPDATE ser_ver SET archive_tag = ? WHERE id = ?', (tag, svid))
- if self.rowcount() != 1:
- raise ValueError(f'No ser_ver updated (svid {svid})')
-
- def ser_ver_add(self, series_idnum, version, link=None):
- """Add a new ser_ver record
-
- Args:
- series_idnum (int): ID num of the series which is getting a new
- version
- version (int): Version number to add
- link (str): Patchwork link, or None if not known
-
- Return:
- int: ID num of the new ser_ver record
- """
- self.execute(
- 'INSERT INTO ser_ver (series_id, version, link) VALUES (?, ?, ?)',
- (series_idnum, version, link))
- return self.lastrowid()
-
- def ser_ver_get_for_series(self, series_idnum, version=None):
- """Get a list of ser_ver records for a given series ID
-
- Args:
- series_idnum (int): ID num of the series to search
- version (int): Version number to search for, or None for all
-
- Return:
- SER_VER: Requested information
-
- Raises:
- ValueError: There is no matching idnum/version
- """
- base = ('SELECT id, series_id, version, link, cover_id, '
- 'cover_num_comments, name, archive_tag FROM ser_ver '
- 'WHERE series_id = ?')
- if version:
- res = self.execute(base + ' AND version = ?',
- (series_idnum, version))
- else:
- res = self.execute(base, (series_idnum,))
- recs = res.fetchall()
- if not recs:
- raise ValueError(
- f'No matching series for id {series_idnum} version {version}')
- if version:
- return SerVer(*recs[0])
- return [SerVer(*x) for x in recs]
-
- def ser_ver_get_ids_for_series(self, series_idnum, version=None):
- """Get a list of ser_ver records for a given series ID
-
- Args:
- series_idnum (int): ID num of the series to search
- version (int): Version number to search for, or None for all
-
- Return:
- list of int: List of svids for the matching records
- """
- if version:
- res = self.execute(
- 'SELECT id FROM ser_ver WHERE series_id = ? AND version = ?',
- (series_idnum, version))
- else:
- res = self.execute(
- 'SELECT id FROM ser_ver WHERE series_id = ?', (series_idnum,))
- return list(res.fetchall()[0])
-
- def ser_ver_get_list(self):
- """Get a list of patchwork entries from the database
-
- Return:
- list of SER_VER
- """
- res = self.execute(
- 'SELECT id, series_id, version, link, cover_id, '
- 'cover_num_comments, name, archive_tag FROM ser_ver')
- items = res.fetchall()
- return [SerVer(*x) for x in items]
-
- def ser_ver_remove(self, series_idnum, version=None, remove_pcommits=True,
- remove_series=True):
- """Delete a ser_ver record
-
- Removes the record which has the given series ID num and version
-
- Args:
- series_idnum (int): ID num of the series
- version (int): Version number, or None to remove all versions
- remove_pcommits (bool): True to remove associated pcommits too
- remove_series (bool): True to remove the series if versions is None
- """
- if remove_pcommits:
- # Figure out svids to delete
- svids = self.ser_ver_get_ids_for_series(series_idnum, version)
-
- self.pcommit_delete_list(svids)
-
- if version:
- self.execute(
- 'DELETE FROM ser_ver WHERE series_id = ? AND version = ?',
- (series_idnum, version))
- else:
- self.execute(
- 'DELETE FROM ser_ver WHERE series_id = ?',
- (series_idnum,))
- if not version and remove_series:
- self.series_remove(series_idnum)
-
- # pcommit functions
-
- def pcommit_get_list(self, find_svid=None):
- """Get a dict of pcommits entries from the database
-
- Args:
- find_svid (int): If not None, finds the records associated with a
- particular series and version; otherwise returns all records
-
- Return:
- list of PCOMMIT: pcommit records
- """
- query = ('SELECT id, seq, subject, svid, change_id, state, patch_id, '
- 'num_comments FROM pcommit')
- if find_svid is not None:
- query += f' WHERE svid = {find_svid}'
- res = self.execute(query)
- return [Pcommit(*rec) for rec in res.fetchall()]
-
- def pcommit_add_list(self, svid, pcommits):
- """Add records to the pcommit table
-
- Args:
- svid (int): ser_ver ID num
- pcommits (list of PCOMMIT): Only seq, subject, change_id are
- uses; svid comes from the argument passed in and the others
- are assumed to be obtained from patchwork later
- """
- for pcm in pcommits:
- self.execute(
- 'INSERT INTO pcommit (svid, seq, subject, change_id) VALUES '
- '(?, ?, ?, ?)', (svid, pcm.seq, pcm.subject, pcm.change_id))
-
- def pcommit_delete(self, svid):
- """Delete pcommit records for a given ser_ver ID
-
- Args_:
- svid (int): ser_ver ID num of records to delete
- """
- self.execute('DELETE FROM pcommit WHERE svid = ?', (svid,))
-
- def pcommit_delete_list(self, svid_list):
- """Delete pcommit records for a given set of ser_ver IDs
-
- Args_:
- svid (list int): ser_ver ID nums of records to delete
- """
- vals = ', '.join([str(x) for x in svid_list])
- self.execute('DELETE FROM pcommit WHERE svid IN (?)', (vals,))
-
- def pcommit_update(self, pcm):
- """Update a pcommit record
-
- Args:
- pcm (PCOMMIT): Information to write; only the idnum, state,
- patch_id and num_comments are used
-
- Return:
- True if the data was written
- """
- self.execute(
- 'UPDATE pcommit SET '
- 'patch_id = ?, state = ?, num_comments = ? WHERE id = ?',
- (pcm.patch_id, pcm.state, pcm.num_comments, pcm.idnum))
- return self.rowcount() > 0
-
- # upstream functions
-
- def upstream_add(self, name, url):
- """Add a new upstream record
-
- Args:
- name (str): Name of the tree
- url (str): URL for the tree
-
- Raises:
- ValueError if the name already exists in the database
- """
- try:
- self.execute(
- 'INSERT INTO upstream (name, url) VALUES (?, ?)', (name, url))
- except sqlite3.IntegrityError as exc:
- if 'UNIQUE constraint failed: upstream.name' in str(exc):
- raise ValueError(f"Upstream '{name}' already exists") from exc
-
- def upstream_set_default(self, name):
- """Mark (only) the given upstream as the default
-
- Args:
- name (str): Name of the upstream remote to set as default, or None
-
- Raises:
- ValueError if more than one name matches (should not happen);
- database is rolled back
- """
- self.execute("UPDATE upstream SET is_default = 0")
- if name is not None:
- self.execute(
- 'UPDATE upstream SET is_default = 1 WHERE name = ?', (name,))
- if self.rowcount() != 1:
- self.rollback()
- raise ValueError(f"No such upstream '{name}'")
-
- def upstream_get_default(self):
- """Get the name of the default upstream
-
- Return:
- str: Default-upstream name, or None if there is no default
- """
- res = self.execute(
- "SELECT name FROM upstream WHERE is_default = 1")
- recs = res.fetchall()
- if len(recs) != 1:
- return None
- return recs[0][0]
-
- def upstream_delete(self, name):
- """Delete an upstream target
-
- Args:
- name (str): Name of the upstream remote to delete
-
- Raises:
- ValueError: Upstream does not exist (database is rolled back)
- """
- self.execute(f"DELETE FROM upstream WHERE name = '{name}'")
- if self.rowcount() != 1:
- self.rollback()
- raise ValueError(f"No such upstream '{name}'")
-
- def upstream_get_dict(self):
- """Get a list of upstream entries from the database
-
- Return:
- OrderedDict:
- key (str): upstream name
- value (str): url
- """
- res = self.execute('SELECT name, url, is_default FROM upstream')
- udict = OrderedDict()
- for name, url, is_default in res.fetchall():
- udict[name] = url, is_default
- return udict
-
- # settings functions
-
- def settings_update(self, name, proj_id, link_name):
- """Set the patchwork settings of the project
-
- Args:
- name (str): Name of the project to use in patchwork
- proj_id (int): Project ID for the project
- link_name (str): Link name for the project
- """
- self.execute('DELETE FROM settings')
- self.execute(
- 'INSERT INTO settings (name, proj_id, link_name) '
- 'VALUES (?, ?, ?)', (name, proj_id, link_name))
-
- def settings_get(self):
- """Get the patchwork settings of the project
-
- Returns:
- tuple or None if there are no settings:
- name (str): Project name, e.g. 'U-Boot'
- proj_id (int): Patchworks project ID for this project
- link_name (str): Patchwork's link-name for the project
- """
- res = self.execute("SELECT name, proj_id, link_name FROM settings")
- recs = res.fetchall()
- if len(recs) != 1:
- return None
- return recs[0]
diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py
deleted file mode 100644
index d485648e467..00000000000
--- a/tools/patman/patchwork.py
+++ /dev/null
@@ -1,852 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2025 Simon Glass <sjg at chromium.org>
-#
-"""Provides a basic API for the patchwork server
-"""
-
-import asyncio
-import re
-
-import aiohttp
-from collections import namedtuple
-
-from u_boot_pylib import terminal
-
-# Information passed to series_get_states()
-# link (str): Patchwork link for series
-# series_id (int): Series ID in database
-# series_name (str): Series name
-# version (int): Version number of series
-# show_comments (bool): True to show comments
-# show_cover_comments (bool): True to show cover-letter comments
-STATE_REQ = namedtuple(
- 'state_req',
- 'link,series_id,series_name,version,show_comments,show_cover_comments')
-
-# Responses from series_get_states()
-# int: ser_ver ID number
-# COVER: Cover-letter info
-# list of Patch: Information on each patch in the series
-# list of dict: patches, see get_series()['patches']
-STATE_RESP = namedtuple('state_resp', 'svid,cover,patches,patch_list')
-
-# Information about a cover-letter on patchwork
-# id (int): Patchwork ID of cover letter
-# state (str): Current state, e.g. 'accepted'
-# num_comments (int): Number of comments
-# name (str): Series name
-# comments (list of dict): Comments
-COVER = namedtuple('cover', 'id,num_comments,name,comments')
-
-# Number of retries
-RETRIES = 3
-
-# Max concurrent request
-MAX_CONCURRENT = 50
-
-# Patches which are part of a multi-patch series are shown with a prefix like
-# [prefix, version, sequence], for example '[RFC, v2, 3/5]'. All but the last
-# part is optional. This decodes the string into groups. For single patches
-# the [] part is not present:
-# Groups: (ignore, ignore, ignore, prefix, version, sequence, subject)
-RE_PATCH = re.compile(r'(\[(((.*),)?(.*),)?(.*)\]\s)?(.*)$')
-
-# This decodes the sequence string into a patch number and patch count
-RE_SEQ = re.compile(r'(\d+)/(\d+)')
-
-
-class Patch(dict):
- """Models a patch in patchwork
-
- This class records information obtained from patchwork
-
- Some of this information comes from the 'Patch' column:
-
- [RFC,v2,1/3] dm: Driver and uclass changes for tiny-dm
-
- This shows the prefix, version, seq, count and subject.
-
- The other properties come from other columns in the display.
-
- Properties:
- pid (str): ID of the patch (typically an integer)
- seq (int): Sequence number within series (1=first) parsed from sequence
- string
- count (int): Number of patches in series, parsed from sequence string
- raw_subject (str): Entire subject line, e.g.
- "[1/2,v2] efi_loader: Sort header file ordering"
- prefix (str): Prefix string or None (e.g. 'RFC')
- version (str): Version string or None (e.g. 'v2')
- raw_subject (str): Raw patch subject
- subject (str): Patch subject with [..] part removed (same as commit
- subject)
- data (dict or None): Patch data:
- """
- def __init__(self, pid, state=None, data=None, comments=None,
- series_data=None):
- super().__init__()
- self.id = pid # Use 'id' to match what the Rest API provides
- self.seq = None
- self.count = None
- self.prefix = None
- self.version = None
- self.raw_subject = None
- self.subject = None
- self.state = state
- self.data = data
- self.comments = comments
- self.series_data = series_data
- self.name = None
-
- # These make us more like a dictionary
- def __setattr__(self, name, value):
- self[name] = value
-
- def __getattr__(self, name):
- return self[name]
-
- def __hash__(self):
- return hash(frozenset(self.items()))
-
- def __str__(self):
- return self.raw_subject
-
- def parse_subject(self, raw_subject):
- """Parse the subject of a patch into its component parts
-
- See RE_PATCH for details. The parsed info is placed into seq, count,
- prefix, version, subject
-
- Args:
- raw_subject (str): Subject string to parse
-
- Raises:
- ValueError: the subject cannot be parsed
- """
- self.raw_subject = raw_subject.strip()
- mat = RE_PATCH.search(raw_subject.strip())
- if not mat:
- raise ValueError(f"Cannot parse subject '{raw_subject}'")
- self.prefix, self.version, seq_info, self.subject = mat.groups()[3:]
- mat_seq = RE_SEQ.match(seq_info) if seq_info else False
- if mat_seq is None:
- self.version = seq_info
- seq_info = None
- if self.version and not self.version.startswith('v'):
- self.prefix = self.version
- self.version = None
- if seq_info:
- if mat_seq:
- self.seq = int(mat_seq.group(1))
- self.count = int(mat_seq.group(2))
- else:
- self.seq = 1
- self.count = 1
-
-
-class Review:
- """Represents a single review email collected in Patchwork
-
- Patches can attract multiple reviews. Each consists of an author/date and
- a variable number of 'snippets', which are groups of quoted and unquoted
- text.
- """
- def __init__(self, meta, snippets):
- """Create new Review object
-
- Args:
- meta (str): Text containing review author and date
- snippets (list): List of snippets in th review, each a list of text
- lines
- """
- self.meta = ' : '.join([line for line in meta.splitlines() if line])
- self.snippets = snippets
-
-
-class Patchwork:
- """Class to handle communication with patchwork
- """
- def __init__(self, url, show_progress=True, single_thread=False):
- """Set up a new patchwork handler
-
- Args:
- url (str): URL of patchwork server, e.g.
- 'https://patchwork.ozlabs.org'
- """
- self.url = url
- self.fake_request = None
- self.proj_id = None
- self.link_name = None
- self._show_progress = show_progress
- self.semaphore = asyncio.Semaphore(
- 1 if single_thread else MAX_CONCURRENT)
- self.request_count = 0
-
- async def _request(self, client, subpath):
- """Call the patchwork API and return the result as JSON
-
- Args:
- client (aiohttp.ClientSession): Session to use
- subpath (str): URL subpath to use
-
- Returns:
- dict: Json result
-
- Raises:
- ValueError: the URL could not be read
- """
- # print('subpath', subpath)
- self.request_count += 1
- if self.fake_request:
- return self.fake_request(subpath)
-
- full_url = f'{self.url}/api/1.2/{subpath}'
- async with self.semaphore:
- # print('full_url', full_url)
- for i in range(RETRIES + 1):
- try:
- async with client.get(full_url) as response:
- if response.status != 200:
- raise ValueError(
- f"Could not read URL '{full_url}'")
- result = await response.json()
- # print('- done', full_url)
- return result
- break
- except aiohttp.client_exceptions.ServerDisconnectedError:
- if i == RETRIES:
- raise
-
- @staticmethod
- def for_testing(func):
- """Get an instance to use for testing
-
- Args:
- func (function): Function to call to handle requests. The function
- is passed a URL and is expected to return a dict with the
- resulting data
-
- Returns:
- Patchwork: testing instance
- """
- pwork = Patchwork(None, show_progress=False)
- pwork.fake_request = func
- return pwork
-
- class _Stats:
- def __init__(self, parent):
- self.parent = parent
- self.request_count = 0
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.request_count = self.parent.request_count
-
- def collect_stats(self):
- """Context manager to count requests across a range of patchwork calls
-
- Usage:
- pwork = Patchwork(...)
- with pwork.count_requests() as counter:
- pwork.something()
- print(f'{counter.count} requests')
- """
- self.request_count = 0
- return self._Stats(self)
-
- async def get_projects(self):
- """Get a list of projects on the server
-
- Returns:
- list of dict, one for each project
- 'name' (str): Project name, e.g. 'U-Boot'
- 'id' (int): Project ID, e.g. 9
- 'link_name' (str): Project's link-name, e.g. 'uboot'
- """
- async with aiohttp.ClientSession() as client:
- return await self._request(client, 'projects/')
-
- async def _query_series(self, client, desc):
- """Query series by name
-
- Args:
- client (aiohttp.ClientSession): Session to use
- desc: String to search for
-
- Return:
- list of series matches, each a dict, see get_series()
- """
- query = desc.replace(' ', '+')
- return await self._request(
- client, f'series/?project={self.proj_id}&q={query}')
-
- async def _find_series(self, client, svid, ser_id, version, ser):
- """Find a series on the server
-
- Args:
- client (aiohttp.ClientSession): Session to use
- svid (int): ser_ver ID
- ser_id (int): series ID
- version (int): Version number to search for
- ser (Series): Contains description (cover-letter title)
-
- Returns:
- tuple:
- int: ser_ver ID (as passed in)
- int: series ID (as passed in)
- str: Series link, or None if not found
- list of dict, or None if found
- each dict is the server result from a possible series
- """
- desc = ser.desc
- name_found = []
-
- # Do a series query on the description
- res = await self._query_series(client, desc)
- for pws in res:
- if pws['name'] == desc:
- if int(pws['version']) == version:
- return svid, ser_id, pws['id'], None
- name_found.append(pws)
-
- # When there is no cover letter, patchwork uses the first patch as the
- # series name
- cmt = ser.commits[0]
-
- res = await self._query_series(client, cmt.subject)
- for pws in res:
- patch = Patch(0)
- patch.parse_subject(pws['name'])
- if patch.subject == cmt.subject:
- if int(pws['version']) == version:
- return svid, ser_id, pws['id'], None
- name_found.append(pws)
-
- return svid, ser_id, None, name_found or res
-
- async def find_series(self, ser, version):
- """Find a series based on its description and version
-
- Args:
- ser (Series): Contains description (cover-letter title)
- version (int): Version number
-
- Return: tuple:
- tuple:
- str: Series ID, or None if not found
- list of dict, or None if found
- each dict is the server result from a possible series
- int: number of server requests done
- """
- async with aiohttp.ClientSession() as client:
- # We don't know the svid and it isn't needed, so use -1
- _, _, link, options = await self._find_series(client, -1, -1,
- version, ser)
- return link, options
-
- async def find_series_list(self, to_find):
- """Find the link for each series in a list
-
- Args:
- to_find (dict of svids to sync):
- key (int): ser_ver ID
- value (tuple):
- int: Series ID
- int: Series version
- str: Series link
- str: Series description
-
- Return: tuple:
- list of tuple, one for each item in to_find:
- int: ser_ver_ID
- int: series ID
- int: Series version
- str: Series link, or None if not found
- list of dict, or None if found
- each dict is the server result from a possible series
- int: number of server requests done
- """
- self.request_count = 0
- async with aiohttp.ClientSession() as client:
- tasks = [asyncio.create_task(
- self._find_series(client, svid, ser_id, version, desc))
- for svid, (ser_id, version, link, desc) in to_find.items()]
- results = await asyncio.gather(*tasks)
-
- return results, self.request_count
-
- def project_set(self, project_id, link_name):
- """Set the project ID
-
- The patchwork server has multiple projects. This allows the ID and
- link_name of the relevant project to be selected
-
- This function is used for testing
-
- Args:
- project_id (int): Project ID to use, e.g. 6
- link_name (str): Name to use for project URL links, e.g. 'uboot'
- """
- self.proj_id = project_id
- self.link_name = link_name
-
- async def get_series(self, client, link):
- """Read information about a series
-
- Args:
- client (aiohttp.ClientSession): Session to use
- link (str): Patchwork series ID
-
- Returns: dict containing patchwork's series information
- id (int): series ID unique across patchwork instance, e.g. 3
- url (str): Full URL, e.g.
- 'https://patchwork.ozlabs.org/api/1.2/series/3/'
- web_url (str): Full URL, e.g.
- 'https://patchwork.ozlabs.org/project/uboot/list/?series=3
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- name (str): Series name, e.g. '[U-Boot] moveconfig: fix error'
- date (str): Date, e.g. '2017-08-27T08:00:51'
- submitter (dict): id, url, name, email, e.g.:
- "id": 6125,
- "url": "https://patchwork.ozlabs.org/api/1.2/people/6125/",
- "name": "Chris Packham",
- "email": "judge.packham at gmail.com"
- version (int): Version number
- total (int): Total number of patches based on subject
- received_total (int): Total patches received by patchwork
- received_all (bool): True if all patches were received
- mbox (str): URL of mailbox, e.g.
- 'https://patchwork.ozlabs.org/series/3/mbox/'
- cover_letter (dict) or None, e.g.:
- "id": 806215,
- "url": "https://patchwork.ozlabs.org/api/1.2/covers/806215/",
- "web_url": "https://patchwork.ozlabs.org/project/uboot/cover/
- 20170827094411.8583-1-judge.packham at gmail.com/",
- "msgid": "<20170827094411.8583-1-judge.packham at gmail.com>",
- "list_archive_url": null,
- "date": "2017-08-27T09:44:07",
- "name": "[U-Boot,v2,0/4] usb: net: Migrate USB Ethernet",
- "mbox": "https://patchwork.ozlabs.org/project/uboot/cover/
- 20170827094411.8583-1-judge.packham at gmail.com/mbox/"
- patches (list of dict), each e.g.:
- "id": 806202,
- "url": "https://patchwork.ozlabs.org/api/1.2/patches/806202/",
- "web_url": "https://patchwork.ozlabs.org/project/uboot/patch/
- 20170827080051.816-1-judge.packham at gmail.com/",
- "msgid": "<20170827080051.816-1-judge.packham at gmail.com>",
- "list_archive_url": null,
- "date": "2017-08-27T08:00:51",
- "name": "[U-Boot] moveconfig: fix error message do_autoconf()",
- "mbox": "https://patchwork.ozlabs.org/project/uboot/patch/
- 20170827080051.816-1-judge.packham at gmail.com/mbox/"
- """
- return await self._request(client, f'series/{link}/')
-
- async def get_patch(self, client, patch_id):
- """Read information about a patch
-
- Args:
- client (aiohttp.ClientSession): Session to use
- patch_id (str): Patchwork patch ID
-
- Returns: dict containing patchwork's patch information
- "id": 185,
- "url": "https://patchwork.ozlabs.org/api/1.2/patches/185/",
- "web_url": "https://patchwork.ozlabs.org/project/cbe-oss-dev/patch/
- 200809050416.27831.adetsch at br.ibm.com/",
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- "msgid": "<200809050416.27831.adetsch at br.ibm.com>",
- "list_archive_url": null,
- "date": "2008-09-05T07:16:27",
- "name": "powerpc/spufs: Fix possible scheduling of a context",
- "commit_ref": "b2e601d14deb2083e2a537b47869ab3895d23a28",
- "pull_url": null,
- "state": "accepted",
- "archived": false,
- "hash": "bc1c0b80d7cff66c0d1e5f3f8f4d10eb36176f0d",
- "submitter": {
- "id": 93,
- "url": "https://patchwork.ozlabs.org/api/1.2/people/93/",
- "name": "Andre Detsch",
- "email": "adetsch at br.ibm.com"
- },
- "delegate": {
- "id": 1,
- "url": "https://patchwork.ozlabs.org/api/1.2/users/1/",
- "username": "jk",
- "first_name": "Jeremy",
- "last_name": "Kerr",
- "email": "jk at ozlabs.org"
- },
- "mbox": "https://patchwork.ozlabs.org/project/cbe-oss-dev/patch/
- 200809050416.27831.adetsch at br.ibm.com/mbox/",
- "series": [],
- "comments": "https://patchwork.ozlabs.org/api/patches/185/
- comments/",
- "check": "pending",
- "checks": "https://patchwork.ozlabs.org/api/patches/185/checks/",
- "tags": {},
- "related": [],
- "headers": {...}
- "content": "We currently have a race when scheduling a context
- after we have found a runnable context in spusched_tick, the
- context may have been scheduled by spu_activate().
-
- This may result in a panic if we try to unschedule a context
- been freed in the meantime.
-
- This change exits spu_schedule() if the context has already
- scheduled, so we don't end up scheduling it twice.
-
- Signed-off-by: Andre Detsch <adetsch at br.ibm.com>",
- "diff": '''Index: spufs/arch/powerpc/platforms/cell/spufs/sched.c
- =======================================================
- --- spufs.orig/arch/powerpc/platforms/cell/spufs/sched.c
- +++ spufs/arch/powerpc/platforms/cell/spufs/sched.c
- @@ -727,7 +727,8 @@ static void spu_schedule(struct spu *spu
- \t/* not a candidate for interruptible because it's called
- \t from the scheduler thread or from spu_deactivate */
- \tmutex_lock(&ctx->state_mutex);
- -\t__spu_schedule(spu, ctx);
- +\tif (ctx->state == SPU_STATE_SAVED)
- +\t\t__spu_schedule(spu, ctx);
- \tspu_release(ctx);
- }
- '''
- "prefixes": ["3/3", ...]
- """
- return await self._request(client, f'patches/{patch_id}/')
-
- async def _get_patch_comments(self, client, patch_id):
- """Read comments about a patch
-
- Args:
- client (aiohttp.ClientSession): Session to use
- patch_id (str): Patchwork patch ID
-
- Returns: list of dict: list of comments:
- id (int): series ID unique across patchwork instance, e.g. 3331924
- web_url (str): Full URL, e.g.
- 'https://patchwork.ozlabs.org/comment/3331924/'
- msgid (str): Message ID, e.g.
- '<d2526c98-8198-4b8b-ab10-20bda0151da1 at gmx.de>'
- list_archive_url: (unknown?)
- date (str): Date, e.g. '2024-06-20T13:38:03'
- subject (str): email subject, e.g. 'Re: [PATCH 3/5] buildman:
- Support building within a Python venv'
- date (str): Date, e.g. '2017-08-27T08:00:51'
- submitter (dict): id, url, name, email, e.g.:
- "id": 61270,
- "url": "https://patchwork.ozlabs.org/api/people/61270/",
- "name": "Heinrich Schuchardt",
- "email": "xypron.glpk at gmx.de"
- content (str): Content of email, e.g. 'On 20.06.24 15:19,
- Simon Glass wrote:
- >...'
- headers: dict: email headers, see get_cover() for an example
- """
- return await self._request(client, f'patches/{patch_id}/comments/')
-
- async def get_cover(self, client, cover_id):
- """Read information about a cover letter
-
- Args:
- client (aiohttp.ClientSession): Session to use
- cover_id (int): Patchwork cover-letter ID
-
- Returns: dict containing patchwork's cover-letter information:
- id (int): series ID unique across patchwork instance, e.g. 3
- url (str): Full URL, e.g. https://patchwork.ozlabs.org/project/uboot/list/?series=3
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/api/1.2/covers/2054866/'
- web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/20250304130947.109799-1-sjg@chromium.org/'
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- msgid (str): Message ID, e.g. '20250304130947.109799-1-sjg at chromium.org>'
- list_archive_url (?)
- date (str): Date, e.g. '2017-08-27T08:00:51'
- name (str): Series name, e.g. '[U-Boot] moveconfig: fix error'
- submitter (dict): id, url, name, email, e.g.:
- "id": 6170,
- "url": "https://patchwork.ozlabs.org/api/1.2/people/6170/",
- "name": "Simon Glass",
- "email": "sjg at chromium.org"
- mbox (str): URL to mailbox, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/20250304130947.109799-1-sjg@chromium.org/mbox/'
- series (list of dict) each e.g.:
- "id": 446956,
- "url": "https://patchwork.ozlabs.org/api/1.2/series/446956/",
- "web_url": "https://patchwork.ozlabs.org/project/uboot/list/?series=446956",
- "date": "2025-03-04T13:09:37",
- "name": "binman: Check code-coverage requirements",
- "version": 1,
- "mbox": "https://patchwork.ozlabs.org/series/446956/mbox/"
- comments: Web URL to comments: 'https://patchwork.ozlabs.org/api/covers/2054866/comments/'
- headers: dict: e.g.:
- "Return-Path": "<u-boot-bounces at lists.denx.de>",
- "X-Original-To": "incoming at patchwork.ozlabs.org",
- "Delivered-To": "patchwork-incoming at legolas.ozlabs.org",
- "Authentication-Results": [
- "legolas.ozlabs.org;
-\tdkim=pass (1024-bit key;
- unprotected) header.d=chromium.org header.i=@chromium.org header.a=rsa-sha256
- header.s=google header.b=dG8yqtoK;
-\tdkim-atps=neutral",
- "legolas.ozlabs.org;
- spf=pass (sender SPF authorized) smtp.mailfrom=lists.denx.de
- (client-ip=85.214.62.61; helo=phobos.denx.de;
- envelope-from=u-boot-bounces at lists.denx.de; receiver=patchwork.ozlabs.org)",
- "phobos.denx.de;
- dmarc=pass (p=none dis=none) header.from=chromium.org",
- "phobos.denx.de;
- spf=pass smtp.mailfrom=u-boot-bounces at lists.denx.de",
- "phobos.denx.de;
-\tdkim=pass (1024-bit key;
- unprotected) header.d=chromium.org header.i=@chromium.org
- header.b=\"dG8yqtoK\";
-\tdkim-atps=neutral",
- "phobos.denx.de;
- dmarc=pass (p=none dis=none) header.from=chromium.org",
- "phobos.denx.de;
- spf=pass smtp.mailfrom=sjg at chromium.org"
- ],
- "Received": [
- "from phobos.denx.de (phobos.denx.de [85.214.62.61])
-\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
-\t key-exchange X25519 server-signature ECDSA (secp384r1))
-\t(No client certificate requested)
-\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4Z6bd50jLhz1yD0
-\tfor <incoming at patchwork.ozlabs.org>; Wed, 5 Mar 2025 00:10:00 +1100 (AEDT)",
- "from h2850616.stratoserver.net (localhost [IPv6:::1])
-\tby phobos.denx.de (Postfix) with ESMTP id 434E88144A;
-\tTue, 4 Mar 2025 14:09:58 +0100 (CET)",
- "by phobos.denx.de (Postfix, from userid 109)
- id 8CBF98144A; Tue, 4 Mar 2025 14:09:57 +0100 (CET)",
- "from mail-io1-xd2e.google.com (mail-io1-xd2e.google.com
- [IPv6:2607:f8b0:4864:20::d2e])
- (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits))
- (No client certificate requested)
- by phobos.denx.de (Postfix) with ESMTPS id 48AE281426
- for <u-boot at lists.denx.de>; Tue, 4 Mar 2025 14:09:55 +0100 (CET)",
- "by mail-io1-xd2e.google.com with SMTP id
- ca18e2360f4ac-85ae33109f6so128326139f.2
- for <u-boot at lists.denx.de>; Tue, 04 Mar 2025 05:09:55 -0800 (PST)",
- "from chromium.org (c-73-203-119-151.hsd1.co.comcast.net.
- [73.203.119.151]) by smtp.gmail.com with ESMTPSA id
- ca18e2360f4ac-858753cd304sm287383839f.33.2025.03.04.05.09.49
- (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
- Tue, 04 Mar 2025 05:09:50 -0800 (PST)"
- ],
- "X-Spam-Checker-Version": "SpamAssassin 3.4.2 (2018-09-13) on phobos.denx.de",
- "X-Spam-Level": "",
- "X-Spam-Status": "No, score=-2.1 required=5.0 tests=BAYES_00,DKIMWL_WL_HIGH,
- DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,
- RCVD_IN_DNSWL_BLOCKED,SPF_HELO_NONE,SPF_PASS autolearn=ham
- autolearn_force=no version=3.4.2",
- "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;
- d=chromium.org; s=google; t=1741093792; x=1741698592; darn=lists.denx.de;
- h=content-transfer-encoding:mime-version:message-id:date:subject:cc
- :to:from:from:to:cc:subject:date:message-id:reply-to;
- bh=B2zsLws430/BEZfatNjeaNnrcxmYUstVjp1pSXgNQjc=;
- b=dG8yqtoKpSy15RHagnPcppzR8KbFCRXa2OBwXfwGoyN6M15tOJsUu2tpCdBFYiL5Mk
- hQz5iDLV8p0Bs+fP4XtNEx7KeYfTZhiqcRFvdCLwYtGray/IHtOZaNoHLajrstic/OgE
- 01ymu6gOEboU32eQ8uC8pdCYQ4UCkfKJwmiiU=",
- "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;
- d=1e100.net; s=20230601; t=1741093792; x=1741698592;
- h=content-transfer-encoding:mime-version:message-id:date:subject:cc
- :to:from:x-gm-message-state:from:to:cc:subject:date:message-id
- :reply-to;
- bh=B2zsLws430/BEZfatNjeaNnrcxmYUstVjp1pSXgNQjc=;
- b=eihzJf4i9gin9usvz4hnAvvbLV9/yB7hGPpwwW/amgnPUyWCeQstgvGL7WDLYYnukH
- 161p4mt7+cCj7Hao/jSPvVZeuKiBNPkS4YCuP3QjXfdk2ziQ9IjloVmGarWZUOlYJ5iQ
- dZnxypUkuFfLcEDSwUmRO1dvLi3nH8PDlae3yT2H87LeHaxhXWdzHxQdPc86rkYyCqCr
- qBC2CTS31jqSuiaI+7qB3glvbJbSEXkunz0iDewTJDvZfmuloxTipWUjRJ1mg9UJcZt5
- 9xIuTq1n9aYf1RcQlrEOQhdBAQ0/IJgvmZtzPZi9L+ppBva1ER/xm06nMA7GEUtyGwun
- c6pA==",
- "X-Gm-Message-State": "AOJu0Yybx3b1+yClf/IfIbQd9u8sxzK9ixPP2HimXF/dGZfSiS7Cb+O5
- WrAkvtp7m3KPM/Mpv0sSZ5qrfTnKnb3WZyv6Oe5Q1iUjAftGNwbSxob5eJ/0y3cgrTdzE4sIWPE
- =",
- "X-Gm-Gg": "ASbGncu5gtgpXEPGrpbTRJulqFrFj1YPAAmKk4MiXA8/3J1A+25F0Uug2KeFUrZEjkG
- KMdPg/C7e2emIvfM+Jl+mKv0ITBvhbyNCyY1q2U1s1cayZF05coZ9ewzGxXJGiEqLMG69uBmmIi
- rBEvCnkXS+HVZobDQMtOsezpc+Ju8JRA7+y1R0WIlutl1mQARct6p0zTkuZp75QyB6dm/d0KYgd
- iux/t/f0HC2CxstQlTlJYzKL6UJgkB5/UorY1lW/0NDRS6P1iemPQ7I3EPLJO8tM5ZrpJE7qgNP
- xy0jXbUv44c48qJ1VszfY5USB8fRG7nwUYxNu6N1PXv9xWbl+z2xL68qNYUrFlHsB8ILTXAyzyr
- Cdj+Sxg==",
- "X-Google-Smtp-Source": "
- AGHT+IFeVk5D4YEfJgPxOfg3ikO6Q7IhaDzABGkAPI6HA0ubK85OPhUHK08gV7enBQ8OdoE/ttqEjw==",
- "X-Received": "by 2002:a05:6602:640f:b0:855:63c8:abb5 with SMTP id
- ca18e2360f4ac-85881fdba3amr1839428939f.13.1741093792636;
- Tue, 04 Mar 2025 05:09:52 -0800 (PST)",
- "From": "Simon Glass <sjg at chromium.org>",
- "To": "U-Boot Mailing List <u-boot at lists.denx.de>",
- "Cc": "Simon Glass <sjg at chromium.org>, Alexander Kochetkov <al.kochet at gmail.com>,
- Alper Nebi Yasak <alpernebiyasak at gmail.com>,
- Brandon Maier <brandon.maier at collins.com>,
- Jerome Forissier <jerome.forissier at linaro.org>,
- Jiaxun Yang <jiaxun.yang at flygoat.com>,
- Neha Malcom Francis <n-francis at ti.com>,
- Patrick Rudolph <patrick.rudolph at 9elements.com>,
- Paul HENRYS <paul.henrys_ext at softathome.com>, Peng Fan <peng.fan at nxp.com>,
- Philippe Reynes <philippe.reynes at softathome.com>,
- Stefan Herbrechtsmeier <stefan.herbrechtsmeier at weidmueller.com>,
- Tom Rini <trini at konsulko.com>",
- "Subject": "[PATCH 0/7] binman: Check code-coverage requirements",
- "Date": "Tue, 4 Mar 2025 06:09:37 -0700",
- "Message-ID": "<20250304130947.109799-1-sjg at chromium.org>",
- "X-Mailer": "git-send-email 2.43.0",
- "MIME-Version": "1.0",
- "Content-Transfer-Encoding": "8bit",
- "X-BeenThere": "u-boot at lists.denx.de",
- "X-Mailman-Version": "2.1.39",
- "Precedence": "list",
- "List-Id": "U-Boot discussion <u-boot.lists.denx.de>",
- "List-Unsubscribe": "<https://lists.denx.de/options/u-boot>,
- <mailto:u-boot-request at lists.denx.de?subject=unsubscribe>",
- "List-Archive": "<https://lists.denx.de/pipermail/u-boot/>",
- "List-Post": "<mailto:u-boot at lists.denx.de>",
- "List-Help": "<mailto:u-boot-request at lists.denx.de?subject=help>",
- "List-Subscribe": "<https://lists.denx.de/listinfo/u-boot>,
- <mailto:u-boot-request at lists.denx.de?subject=subscribe>",
- "Errors-To": "u-boot-bounces at lists.denx.de",
- "Sender": "\"U-Boot\" <u-boot-bounces at lists.denx.de>",
- "X-Virus-Scanned": "clamav-milter 0.103.8 at phobos.denx.de",
- "X-Virus-Status": "Clean"
- content (str): Email content, e.g. 'This series adds a cover-coverage check to CI for Binman. The iMX8 tests
-are still not completed,...'
- """
- async with aiohttp.ClientSession() as client:
- return await self._request(client, f'covers/{cover_id}/')
-
- async def get_cover_comments(self, client, cover_id):
- """Read comments about a cover letter
-
- Args:
- client (aiohttp.ClientSession): Session to use
- cover_id (str): Patchwork cover-letter ID
-
- Returns: list of dict: list of comments, each:
- id (int): series ID unique across patchwork instance, e.g. 3472068
- web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/comment/3472068/'
- list_archive_url: (unknown?)
-
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/api/1.2/covers/2054866/'
- web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/20250304130947.109799-1-sjg@chromium.org/'
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- date (str): Date, e.g. '2025-03-04T13:16:15'
- subject (str): 'Re: [PATCH 0/7] binman: Check code-coverage requirements'
- submitter (dict): id, url, name, email, e.g.:
- "id": 6170,
- "url": "https://patchwork.ozlabs.org/api/people/6170/",
- "name": "Simon Glass",
- "email": "sjg at chromium.org"
- content (str): Email content, e.g. 'Hi,
-
-On Tue, 4 Mar 2025 at 06:09, Simon Glass <sjg at chromium.org> wrote:
->
-> This '...
- headers: dict: email headers, see get_cover() for an example
- """
- return await self._request(client, f'covers/{cover_id}/comments/')
-
- async def get_series_url(self, link):
- """Get the URL for a series
-
- Args:
- link (str): Patchwork series ID
-
- Returns:
- str: URL for the series page
- """
- return f'{self.url}/project/{self.link_name}/list/?series={link}&state=*&archive=both'
-
- async def _get_patch_status(self, client, patch_id):
- """Get the patch status
-
- Args:
- client (aiohttp.ClientSession): Session to use
- patch_id (int): Patch ID to look up in patchwork
-
- Return:
- PATCH: Patch information
-
- Requests:
- 1 for patch, 1 for patch comments
- """
- data = await self.get_patch(client, patch_id)
- state = data['state']
- comment_data = await self._get_patch_comments(client, patch_id)
-
- return Patch(patch_id, state, data, comment_data)
-
- async def get_series_cover(self, client, data):
- """Get the cover information (including comments)
-
- Args:
- client (aiohttp.ClientSession): Session to use
- data (dict): Return value from self.get_series()
-
- Returns:
- COVER object, or None if no cover letter
- """
- # Patchwork should always provide this, but use get() so that we don't
- # have to provide it in our fake patchwork _fake_patchwork_cser()
- cover = data.get('cover_letter')
- cover_id = None
- if cover:
- cover_id = cover['id']
- info = await self.get_cover_comments(client, cover_id)
- cover = COVER(cover_id, len(info), cover['name'], info)
- return cover
-
- async def series_get_state(self, client, link, read_comments,
- read_cover_comments):
- """Sync the series information against patchwork, to find patch status
-
- Args:
- client (aiohttp.ClientSession): Session to use
- link (str): Patchwork series ID
- read_comments (bool): True to read the comments on the patches
- read_cover_comments (bool): True to read the comments on the cover
- letter
-
- Return: tuple:
- COVER object, or None if none or not read_cover_comments
- list of PATCH objects
- """
- data = await self.get_series(client, link)
- patch_list = list(data['patches'])
-
- count = len(patch_list)
- patches = []
- if read_comments:
- # Returns a list of Patch objects
- tasks = [self._get_patch_status(client, patch_list[i]['id'])
- for i in range(count)]
-
- patch_status = await asyncio.gather(*tasks)
- for patch_data, status in zip(patch_list, patch_status):
- status.series_data = patch_data
- patches.append(status)
- else:
- for i in range(count):
- info = patch_list[i]
- pat = Patch(info['id'], series_data=info)
- pat.raw_subject = info['name']
- patches.append(pat)
- if self._show_progress:
- terminal.print_clear()
-
- if read_cover_comments:
- cover = await self.get_series_cover(client, data)
- else:
- cover = None
-
- return cover, patches
diff --git a/tools/patman/project.py b/tools/patman/project.py
deleted file mode 100644
index e633401e9d6..00000000000
--- a/tools/patman/project.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-# Copyright (c) 2012 The Chromium OS Authors.
-#
-
-import os.path
-
-from u_boot_pylib import gitutil
-
-def detect_project():
- """Autodetect the name of the current project.
-
- This looks for signature files/directories that are unlikely to exist except
- in the given project.
-
- Returns:
- The name of the project, like "linux" or "u-boot". Returns "unknown"
- if we can't detect the project.
- """
- top_level = gitutil.get_top_level()
-
- if (not top_level or
- os.path.exists(os.path.join(top_level, "include", "u-boot"))):
- return "u-boot"
- elif os.path.exists(os.path.join(top_level, "kernel")):
- return "linux"
-
- return "unknown"
diff --git a/tools/patman/send.py b/tools/patman/send.py
deleted file mode 100644
index 08a916aff1a..00000000000
--- a/tools/patman/send.py
+++ /dev/null
@@ -1,197 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2025 Google LLC
-#
-"""Handles the 'send' subcommand
-"""
-
-import os
-import sys
-
-from patman import checkpatch
-from patman import patchstream
-from patman import settings
-from u_boot_pylib import gitutil
-from u_boot_pylib import terminal
-
-
-def check_patches(series, patch_files, run_checkpatch, verbose, use_tree, cwd):
- """Run some checks on a set of patches
-
- This santiy-checks the patman tags like Series-version and runs the patches
- through checkpatch
-
- Args:
- series (Series): Series object for this series (set of patches)
- patch_files (list): List of patch filenames, each a string, e.g.
- ['0001_xxx.patch', '0002_yyy.patch']
- run_checkpatch (bool): True to run checkpatch.pl
- verbose (bool): True to print out every line of the checkpatch output as
- it is parsed
- use_tree (bool): If False we'll pass '--no-tree' to checkpatch.
- cwd (str): Path to use for patch files (None to use current dir)
-
- Returns:
- bool: True if the patches had no errors, False if they did
- """
- # Do a few checks on the series
- series.DoChecks()
-
- # Check the patches
- if run_checkpatch:
- ok = checkpatch.check_patches(verbose, patch_files, use_tree, cwd)
- else:
- ok = True
- return ok
-
-
-def email_patches(col, series, cover_fname, patch_files, process_tags, its_a_go,
- ignore_bad_tags, add_maintainers, get_maintainer_script, limit,
- dry_run, in_reply_to, thread, smtp_server, cwd=None):
- """Email patches to the recipients
-
- This emails out the patches and cover letter using 'git send-email'. Each
- patch is copied to recipients identified by the patch tag and output from
- the get_maintainer.pl script. The cover letter is copied to all recipients
- of any patch.
-
- To make this work a CC file is created holding the recipients for each patch
- and the cover letter. See the main program 'cc_cmd' for this logic.
-
- Args:
- col (terminal.Color): Colour output object
- series (Series): Series object for this series (set of patches)
- cover_fname (str): Filename of the cover letter as a string (None if
- none)
- patch_files (list): List of patch filenames, each a string, e.g.
- ['0001_xxx.patch', '0002_yyy.patch']
- process_tags (bool): True to process subject tags in each patch, e.g.
- for 'dm: spi: Add SPI support' this would be 'dm' and 'spi'. The
- tags are looked up in the configured sendemail.aliasesfile and also
- in ~/.patman (see README)
- its_a_go (bool): True if we are going to actually send the patches,
- False if the patches have errors and will not be sent unless
- @ignore_errors
- ignore_bad_tags (bool): True to just print a warning for unknown tags,
- False to halt with an error
- add_maintainers (bool): Run the get_maintainer.pl script for each patch
- get_maintainer_script (str): The script used to retrieve which
- maintainers to cc
- limit (int): Limit on the number of people that can be cc'd on a single
- patch or the cover letter (None if no limit)
- dry_run (bool): Don't actually email the patches, just print out what
- would be sent
- in_reply_to (str): If not None we'll pass this to git as --in-reply-to.
- Should be a message ID that this is in reply to.
- thread (bool): True to add --thread to git send-email (make all patches
- reply to cover-letter or first patch in series)
- smtp_server (str): SMTP server to use to send patches (None for default)
- cwd (str): Path to use for patch files (None to use current dir)
-
- Return:
- Git command that was/would be run
- """
- cc_file = series.MakeCcFile(process_tags, cover_fname, not ignore_bad_tags,
- add_maintainers, limit, get_maintainer_script,
- settings.alias, cwd)
-
- # Email the patches out (giving the user time to check / cancel)
- cmd = ''
- if its_a_go:
- cmd = gitutil.email_patches(
- series, cover_fname, patch_files, dry_run, not ignore_bad_tags,
- cc_file, alias=settings.alias, in_reply_to=in_reply_to,
- thread=thread, smtp_server=smtp_server, cwd=cwd)
- else:
- print(col.build(col.RED, "Not sending emails due to errors/warnings"))
-
- # For a dry run, just show our actions as a sanity check
- if dry_run:
- series.ShowActions(patch_files, cmd, process_tags, settings.alias)
- if not its_a_go:
- print(col.build(col.RED, "Email would not be sent"))
-
- os.remove(cc_file)
- return cmd
-
-
-def prepare_patches(col, branch, count, start, end, ignore_binary, signoff,
- keep_change_id=False, git_dir=None, cwd=None):
- """Figure out what patches to generate, then generate them
-
- The patch files are written to the current directory, e.g. 0001_xxx.patch
- 0002_yyy.patch
-
- Args:
- col (terminal.Color): Colour output object
- branch (str): Branch to create patches from (None = current)
- count (int): Number of patches to produce, or -1 to produce patches for
- the current branch back to the upstream commit
- start (int): Start patch to use (0=first / top of branch)
- end (int): End patch to use (0=last one in series, 1=one before that,
- etc.)
- ignore_binary (bool): Don't generate patches for binary files
- keep_change_id (bool): Preserve the Change-Id tag.
- git_dir (str): Path to git repository (None to use default)
- cwd (str): Path to use for git operations (None to use current dir)
-
- Returns:
- Tuple:
- Series object for this series (set of patches)
- Filename of the cover letter as a string (None if none)
- patch_files: List of patch filenames, each a string, e.g.
- ['0001_xxx.patch', '0002_yyy.patch']
- """
- if count == -1:
- # Work out how many patches to send if we can
- count = (gitutil.count_commits_to_branch(branch, git_dir=git_dir) -
- start)
-
- if not count:
- msg = 'No commits found to process - please use -c flag, or run:\n' \
- ' git branch --set-upstream-to remote/branch'
- sys.exit(col.build(col.RED, msg))
-
- # Read the metadata from the commits
- to_do = count - end
- series = patchstream.get_metadata(branch, start, to_do, git_dir)
- cover_fname, patch_files = gitutil.create_patches(
- branch, start, to_do, ignore_binary, series, signoff, git_dir=git_dir,
- cwd=cwd)
-
- # Fix up the patch files to our liking, and insert the cover letter
- patchstream.fix_patches(series, patch_files, keep_change_id,
- insert_base_commit=not cover_fname, cwd=cwd)
- if cover_fname and series.get('cover'):
- patchstream.insert_cover_letter(cover_fname, series, to_do, cwd=cwd)
- return series, cover_fname, patch_files
-
-
-def send(args, git_dir=None, cwd=None):
- """Create, check and send patches by email
-
- Args:
- args (argparse.Namespace): Arguments to patman
- cwd (str): Path to use for git operations
-
- Return:
- bool: True if the patches were likely sent, else False
- """
- col = terminal.Color()
- series, cover_fname, patch_files = prepare_patches(
- col, args.branch, args.count, args.start, args.end,
- args.ignore_binary, args.add_signoff,
- keep_change_id=args.keep_change_id, git_dir=git_dir, cwd=cwd)
- ok = check_patches(series, patch_files, args.check_patch,
- args.verbose, args.check_patch_use_tree, cwd)
-
- ok = ok and gitutil.check_suppress_cc_config()
-
- its_a_go = ok or args.ignore_errors
- cmd = email_patches(
- col, series, cover_fname, patch_files, args.process_tags,
- its_a_go, args.ignore_bad_tags, args.add_maintainers,
- args.get_maintainer_script, args.limit, args.dry_run,
- args.in_reply_to, args.thread, args.smtp_server, cwd=cwd)
-
- return cmd and its_a_go and not args.dry_run
diff --git a/tools/patman/status.py b/tools/patman/status.py
deleted file mode 100644
index 967fef3ad6e..00000000000
--- a/tools/patman/status.py
+++ /dev/null
@@ -1,405 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2020 Google LLC
-#
-"""Talks to the patchwork service to figure out what patches have been reviewed
-and commented on. Provides a way to display review tags and comments.
-Allows creation of a new branch based on the old but with the review tags
-collected from patchwork.
-"""
-
-import asyncio
-from collections import defaultdict
-import concurrent.futures
-from itertools import repeat
-
-import aiohttp
-import pygit2
-
-from u_boot_pylib import terminal
-from u_boot_pylib import tout
-from patman import patchstream
-from patman import patchwork
-
-
-def process_reviews(content, comment_data, base_rtags):
- """Process and return review data
-
- Args:
- content (str): Content text of the patch itself - see pwork.get_patch()
- comment_data (list of dict): Comments for the patch - see
- pwork._get_patch_comments()
- base_rtags (dict): base review tags (before any comments)
- key: Response tag (e.g. 'Reviewed-by')
- value: Set of people who gave that response, each a name/email
- string
-
- Return: tuple:
- dict: new review tags (noticed since the base_rtags)
- key: Response tag (e.g. 'Reviewed-by')
- value: Set of people who gave that response, each a name/email
- string
- list of patchwork.Review: reviews received on the patch
- """
- pstrm = patchstream.PatchStream.process_text(content, True)
- rtags = defaultdict(set)
- for response, people in pstrm.commit.rtags.items():
- rtags[response].update(people)
-
- reviews = []
- for comment in comment_data:
- pstrm = patchstream.PatchStream.process_text(comment['content'], True)
- if pstrm.snippets:
- submitter = comment['submitter']
- person = f"{submitter['name']} <{submitter['email']}>"
- reviews.append(patchwork.Review(person, pstrm.snippets))
- for response, people in pstrm.commit.rtags.items():
- rtags[response].update(people)
-
- # Find the tags that are not in the commit
- new_rtags = defaultdict(set)
- for tag, people in rtags.items():
- for who in people:
- is_new = (tag not in base_rtags or
- who not in base_rtags[tag])
- if is_new:
- new_rtags[tag].add(who)
- return new_rtags, reviews
-
-
-def compare_with_series(series, patches):
- """Compare a list of patches with a series it came from
-
- This prints any problems as warnings
-
- Args:
- series (Series): Series to compare against
- patches (list of Patch): list of Patch objects to compare with
-
- Returns:
- tuple
- dict:
- key: Commit number (0...n-1)
- value: Patch object for that commit
- dict:
- key: Patch number (0...n-1)
- value: Commit object for that patch
- """
- # Check the names match
- warnings = []
- patch_for_commit = {}
- all_patches = set(patches)
- for seq, cmt in enumerate(series.commits):
- pmatch = [p for p in all_patches if p.subject == cmt.subject]
- if len(pmatch) == 1:
- patch_for_commit[seq] = pmatch[0]
- all_patches.remove(pmatch[0])
- elif len(pmatch) > 1:
- warnings.append("Multiple patches match commit %d ('%s'):\n %s" %
- (seq + 1, cmt.subject,
- '\n '.join([p.subject for p in pmatch])))
- else:
- warnings.append("Cannot find patch for commit %d ('%s')" %
- (seq + 1, cmt.subject))
-
- # Check the names match
- commit_for_patch = {}
- all_commits = set(series.commits)
- for seq, patch in enumerate(patches):
- cmatch = [c for c in all_commits if c.subject == patch.subject]
- if len(cmatch) == 1:
- commit_for_patch[seq] = cmatch[0]
- all_commits.remove(cmatch[0])
- elif len(cmatch) > 1:
- warnings.append("Multiple commits match patch %d ('%s'):\n %s" %
- (seq + 1, patch.subject,
- '\n '.join([c.subject for c in cmatch])))
- else:
- warnings.append("Cannot find commit for patch %d ('%s')" %
- (seq + 1, patch.subject))
-
- return patch_for_commit, commit_for_patch, warnings
-
-
-def show_responses(col, rtags, indent, is_new):
- """Show rtags collected
-
- Args:
- col (terminal.Colour): Colour object to use
- rtags (dict): review tags to show
- key: Response tag (e.g. 'Reviewed-by')
- value: Set of people who gave that response, each a name/email string
- indent (str): Indentation string to write before each line
- is_new (bool): True if this output should be highlighted
-
- Returns:
- int: Number of review tags displayed
- """
- count = 0
- for tag in sorted(rtags.keys()):
- people = rtags[tag]
- for who in sorted(people):
- terminal.tprint(indent + '%s %s: ' % ('+' if is_new else ' ', tag),
- newline=False, colour=col.GREEN, bright=is_new,
- col=col)
- terminal.tprint(who, colour=col.WHITE, bright=is_new, col=col)
- count += 1
- return count
-
-def create_branch(series, new_rtag_list, branch, dest_branch, overwrite,
- repo=None):
- """Create a new branch with review tags added
-
- Args:
- series (Series): Series object for the existing branch
- new_rtag_list (list): List of review tags to add, one for each commit,
- each a dict:
- key: Response tag (e.g. 'Reviewed-by')
- value: Set of people who gave that response, each a name/email
- string
- branch (str): Existing branch to update
- dest_branch (str): Name of new branch to create
- overwrite (bool): True to force overwriting dest_branch if it exists
- repo (pygit2.Repository): Repo to use (use None unless testing)
-
- Returns:
- int: Total number of review tags added across all commits
-
- Raises:
- ValueError: if the destination branch name is the same as the original
- branch, or it already exists and @overwrite is False
- """
- if branch == dest_branch:
- raise ValueError(
- 'Destination branch must not be the same as the original branch')
- if not repo:
- repo = pygit2.Repository('.')
- count = len(series.commits)
- new_br = repo.branches.get(dest_branch)
- if new_br:
- if not overwrite:
- raise ValueError("Branch '%s' already exists (-f to overwrite)" %
- dest_branch)
- new_br.delete()
- if not branch:
- branch = 'HEAD'
- target = repo.revparse_single('%s~%d' % (branch, count))
- repo.branches.local.create(dest_branch, target)
-
- num_added = 0
- for seq in range(count):
- parent = repo.branches.get(dest_branch)
- cherry = repo.revparse_single('%s~%d' % (branch, count - seq - 1))
-
- repo.merge_base(cherry.oid, parent.target)
- base_tree = cherry.parents[0].tree
-
- index = repo.merge_trees(base_tree, parent, cherry)
- tree_id = index.write_tree(repo)
-
- lines = []
- if new_rtag_list[seq]:
- for tag, people in new_rtag_list[seq].items():
- for who in people:
- lines.append('%s: %s' % (tag, who))
- num_added += 1
- message = patchstream.insert_tags(cherry.message.rstrip(),
- sorted(lines))
-
- repo.create_commit(
- parent.name, cherry.author, cherry.committer, message, tree_id,
- [parent.target])
- return num_added
-
-
-def check_patch_count(num_commits, num_patches):
- """Check the number of commits and patches agree
-
- Args:
- num_commits (int): Number of commits
- num_patches (int): Number of patches
- """
- if num_patches != num_commits:
- tout.warning(f'Warning: Patchwork reports {num_patches} patches, '
- f'series has {num_commits}')
-
-
-def do_show_status(series, cover, patches, show_comments, show_cover_comments,
- col, warnings_on_stderr=True):
- """Check the status of a series on Patchwork
-
- This finds review tags and comments for a series in Patchwork, displaying
- them to show what is new compared to the local series.
-
- Args:
- series (Series): Series object for the existing branch
- cover (COVER): Cover letter info, or None if none
- patches (list of Patch): Patches sorted by sequence number
- show_comments (bool): True to show the comments on each patch
- show_cover_comments (bool): True to show the comments on the
- letter
- col (terminal.Colour): Colour object
-
- Return: tuple:
- int: Number of new review tags to add
- list: List of review tags to add, one item for each commit, each a
- dict:
- key: Response tag (e.g. 'Reviewed-by')
- value: Set of people who gave that response, each a name/email
- string
- """
- compare = []
- for pw_patch in patches:
- patch = patchwork.Patch(pw_patch.id)
- patch.parse_subject(pw_patch.series_data['name'])
- compare.append(patch)
-
- count = len(series.commits)
- new_rtag_list = [None] * count
- review_list = [None] * count
-
- with terminal.pager():
- patch_for_commit, _, warnings = compare_with_series(series, compare)
- for warn in warnings:
- tout.do_output(tout.WARNING if warnings_on_stderr else tout.INFO,
- warn)
-
- for seq, pw_patch in enumerate(patches):
- compare[seq].patch = pw_patch
-
- for i in range(count):
- pat = patch_for_commit.get(i)
- if pat:
- patch_data = pat.patch.data
- comment_data = pat.patch.comments
- new_rtag_list[i], review_list[i] = process_reviews(
- patch_data['content'], comment_data,
- series.commits[i].rtags)
- num_to_add = _do_show_status(
- series, cover, patch_for_commit, show_comments,
- show_cover_comments, new_rtag_list, review_list, col)
-
- return num_to_add, new_rtag_list
-
-
-def _do_show_status(series, cover, patch_for_commit, show_comments,
- show_cover_comments, new_rtag_list, review_list, col):
- if cover and show_cover_comments:
- terminal.tprint(f'Cov {cover.name}', colour=col.BLACK, col=col,
- bright=False, back=col.YELLOW)
- for seq, comment in enumerate(cover.comments):
- submitter = comment['submitter']
- person = '%s <%s>' % (submitter['name'], submitter['email'])
- terminal.tprint(f"From: {person}: {comment['date']}",
- colour=col.RED, col=col)
- print(comment['content'])
- print()
-
- num_to_add = 0
- for seq, cmt in enumerate(series.commits):
- patch = patch_for_commit.get(seq)
- if not patch:
- continue
- terminal.tprint('%3d %s' % (patch.seq, patch.subject[:50]),
- colour=col.YELLOW, col=col)
- cmt = series.commits[seq]
- base_rtags = cmt.rtags
- new_rtags = new_rtag_list[seq]
-
- indent = ' ' * 2
- show_responses(col, base_rtags, indent, False)
- num_to_add += show_responses(col, new_rtags, indent, True)
- if show_comments:
- for review in review_list[seq]:
- terminal.tprint('Review: %s' % review.meta, colour=col.RED,
- col=col)
- for snippet in review.snippets:
- for line in snippet:
- quoted = line.startswith('>')
- terminal.tprint(
- f' {line}',
- colour=col.MAGENTA if quoted else None, col=col)
- terminal.tprint()
- return num_to_add
-
-
-def show_status(series, branch, dest_branch, force, cover, patches,
- show_comments, show_cover_comments, test_repo=None):
- """Check the status of a series on Patchwork
-
- This finds review tags and comments for a series in Patchwork, displaying
- them to show what is new compared to the local series.
-
- Args:
- client (aiohttp.ClientSession): Session to use
- series (Series): Series object for the existing branch
- branch (str): Existing branch to update, or None
- dest_branch (str): Name of new branch to create, or None
- force (bool): True to force overwriting dest_branch if it exists
- cover (COVER): Cover letter info, or None if none
- patches (list of Patch): Patches sorted by sequence number
- show_comments (bool): True to show the comments on each patch
- show_cover_comments (bool): True to show the comments on the letter
- test_repo (pygit2.Repository): Repo to use (use None unless testing)
- """
- col = terminal.Color()
- check_patch_count(len(series.commits), len(patches))
- num_to_add, new_rtag_list = do_show_status(
- series, cover, patches, show_comments, show_cover_comments, col)
-
- if not dest_branch and num_to_add:
- msg = ' (use -d to write them to a new branch)'
- else:
- msg = ''
- terminal.tprint(
- f"{num_to_add} new response{'s' if num_to_add != 1 else ''} "
- f'available in patchwork{msg}')
-
- if dest_branch:
- num_added = create_branch(series, new_rtag_list, branch,
- dest_branch, force, test_repo)
- terminal.tprint(
- f"{num_added} response{'s' if num_added != 1 else ''} added "
- f"from patchwork into new branch '{dest_branch}'")
-
-
-async def check_status(link, pwork, read_comments=False,
- read_cover_comments=False):
- """Set up an HTTP session and get the required state
-
- Args:
- link (str): Patch series ID number
- pwork (Patchwork): Patchwork object to use for reading
- read_comments (bool): True to read comments and state for each patch
-
- Return: tuple:
- COVER object, or None if none or not read_cover_comments
- list of PATCH objects
- """
- async with aiohttp.ClientSession() as client:
- return await pwork.series_get_state(client, link, read_comments,
- read_cover_comments)
-
-
-def check_and_show_status(series, link, branch, dest_branch, force,
- show_comments, show_cover_comments, pwork,
- test_repo=None):
- """Read the series status from patchwork and show it to the user
-
- Args:
- series (Series): Series object for the existing branch
- link (str): Patch series ID number
- branch (str): Existing branch to update, or None
- dest_branch (str): Name of new branch to create, or None
- force (bool): True to force overwriting dest_branch if it exists
- show_comments (bool): True to show the comments on each patch
- show_cover_comments (bool): True to show the comments on the letter
- pwork (Patchwork): Patchwork object to use for reading
- test_repo (pygit2.Repository): Repo to use (use None unless testing)
- """
- loop = asyncio.get_event_loop()
- cover, patches = loop.run_until_complete(check_status(
- link, pwork, True, show_cover_comments))
-
- show_status(series, branch, dest_branch, force, cover, patches,
- show_comments, show_cover_comments, test_repo=test_repo)
--
2.43.0
More information about the U-Boot
mailing list