diff options
Diffstat (limited to 'tools')
29 files changed, 23 insertions, 13359 deletions
diff --git a/tools/binman/pyproject.toml b/tools/binman/pyproject.toml index 2f17588e9df..fd43a71b02b 100644 --- a/tools/binman/pyproject.toml +++ b/tools/binman/pyproject.toml @@ -26,4 +26,4 @@ classifiers = [ binman = "binman.main:start_binman" [tool.setuptools.package-data] -patman = ["*.rst"] +binman = ["*.rst"] diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index 73bf6cdd2c5..a91a1060cc4 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -362,7 +362,6 @@ RUN wget -O /tmp/pytest-requirements.txt https://source.denx.de/u-boot/u-boot/-/ RUN wget -O /tmp/sphinx-requirements.txt https://source.denx.de/u-boot/u-boot/-/raw/master/doc/sphinx/requirements.txt RUN wget -O /tmp/binman-requirements.txt https://source.denx.de/u-boot/u-boot/-/raw/master/tools/binman/requirements.txt RUN wget -O /tmp/buildman-requirements.txt https://source.denx.de/u-boot/u-boot/-/raw/master/tools/buildman/requirements.txt -RUN wget -O /tmp/patman-requirements.txt https://source.denx.de/u-boot/u-boot/-/raw/master/tools/patman/requirements.txt RUN wget -O /tmp/u_boot_pylib-requirements.txt https://source.denx.de/u-boot/u-boot/-/raw/master/tools/u_boot_pylib/requirements.txt RUN python3 -m venv /tmp/venv && \ . /tmp/venv/bin/activate && \ @@ -370,7 +369,6 @@ RUN python3 -m venv /tmp/venv && \ -r /tmp/sphinx-requirements.txt \ -r /tmp/binman-requirements.txt \ -r /tmp/buildman-requirements.txt \ - -r /tmp/patman-requirements.txt \ -r /tmp/u_boot_pylib-requirements.txt \ setuptools pytest-azurepipelines && \ deactivate && \ diff --git a/tools/patman/README.rst b/tools/patman/README.rst deleted file mode 120000 index 76368b95980..00000000000 --- a/tools/patman/README.rst +++ /dev/null @@ -1 +0,0 @@ -patman.rst
\ No newline at end of file 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/__main__.py b/tools/patman/__main__.py index edfb1b5927c..a497f6ced7e 100755 --- a/tools/patman/__main__.py +++ b/tools/patman/__main__.py @@ -4,56 +4,30 @@ # Copyright (c) 2011 The Chromium OS Authors. # -"""See README for more information""" +"""Stub for the former in-tree patman tool -import os -import sys - -# Allow 'from patman import xxx to work' -# pylint: disable=C0413 -our_path = os.path.dirname(os.path.realpath(__file__)) -sys.path.append(os.path.join(our_path, '..')) - -# Our modules -from u_boot_pylib import test_util -from u_boot_pylib import tout -from patman import cmdline -from patman import control - - -def run_patman(): - """Run patamn +patman is now maintained as a standalone 'patch-manager' package, rather +than living in the U-Boot tree. This stub just tells the user how to get +it. +""" - This is the main program. It collects arguments and runs either the tests or - the control module. - """ - args = cmdline.parse_args() - - if not args.debug: - sys.tracebacklimit = 0 - - tout.init(tout.INFO if args.verbose else tout.WARNING) - - # Run our reasonably good tests - if args.cmd == 'test': - # pylint: disable=C0415 - from patman import func_test - from patman import test_checkpatch - from patman import test_cseries +import sys - to_run = args.testname if args.testname not in [None, 'test'] else None - result = test_util.run_test_suites( - 'patman', False, args.verbose, args.no_capture, - args.test_preserve_dirs, None, to_run, None, - [test_checkpatch.TestPatch, func_test.TestFunctional, 'settings', - test_cseries.TestCseries]) - sys.exit(0 if result.wasSuccessful() else 1) - # Process commits, produce patches files, check them, email them - else: - exit_code = control.do_patman(args) - sys.exit(exit_code) +def main(): + """Print instructions for installing the patch-manager package""" + print( + 'patman is no longer part of U-Boot. It is now maintained as a\n' + "separate package called 'patch-manager'.\n" + '\n' + 'Install it with:\n' + '\n' + ' pip install patch-manager\n' + '\n' + 'Documentation: https://deinde.dev/patman\n', + file=sys.stderr) + return 1 -if __name__ == "__main__": - sys.exit(run_patman()) +if __name__ == '__main__': + sys.exit(main()) 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 <[email protected]> -# -"""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 <[email protected]> -# -"""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/func_test.py b/tools/patman/func_test.py deleted file mode 100644 index d029181765c..00000000000 --- a/tools/patman/func_test.py +++ /dev/null @@ -1,1342 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: GPL-2.0+ -# -# Copyright 2017 Google, Inc -# - -"""Functional tests for checking that patman behaves correctly""" - -import asyncio -import contextlib -import os -import pathlib -import re -import shutil -import sys -import unittest - -import pygit2 - -from u_boot_pylib import command -from u_boot_pylib import gitutil -from u_boot_pylib import terminal -from u_boot_pylib import tools - -from patman.commit import Commit -from patman import control -from patman import patchstream -from patman.patchstream import PatchStream -from patman import patchwork -from patman import send -from patman.series import Series -from patman import status -from patman.test_common import TestCommon - -PATMAN_DIR = pathlib.Path(__file__).parent -TEST_DATA_DIR = PATMAN_DIR / 'test/' - - -def directory_excursion(directory): - """Change directory to `directory` for a limited to the context block.""" - current = os.getcwd() - try: - os.chdir(directory) - yield - finally: - os.chdir(current) - - -class TestFunctional(unittest.TestCase, TestCommon): - """Functional tests for checking that patman behaves correctly""" - fred = 'Fred Bloggs <[email protected]>' - joe = 'Joe Bloggs <[email protected]>' - mary = 'Mary Bloggs <[email protected]>' - commits = None - patches = None - - def setUp(self): - TestCommon.setUp(self) - self.repo = None - self._patman_pathname = sys.argv[0] - self._patman_dir = os.path.dirname(os.path.realpath(sys.argv[0])) - - def tearDown(self): - TestCommon.tearDown(self) - - @staticmethod - def _get_path(fname): - """Get the path to a test file - - Args: - fname (str): Filename to obtain - - Returns: - str: Full path to file in the test directory - """ - return TEST_DATA_DIR / fname - - @classmethod - def _get_text(cls, fname): - """Read a file as text - - Args: - fname (str): Filename to read - - Returns: - str: Contents of file - """ - return open(cls._get_path(fname), encoding='utf-8').read() - - @classmethod - def _get_patch_name(cls, subject): - """Get the filename of a patch given its subject - - Args: - subject (str): Patch subject - - Returns: - str: Filename for that patch - """ - fname = re.sub('[ :]', '-', subject) - return fname.replace('--', '-') - - def _create_patches_for_test(self, series): - """Create patch files for use by tests - - This copies patch files from the test directory as needed by the series - - Args: - series (Series): Series containing commits to convert - - Returns: - tuple: - str: Cover-letter filename, or None if none - fname_list: list of str, each a patch filename - """ - cover_fname = None - fname_list = [] - for i, commit in enumerate(series.commits): - clean_subject = self._get_patch_name(commit.subject) - src_fname = '%04d-%s.patch' % (i + 1, clean_subject[:52]) - fname = os.path.join(self.tmpdir, src_fname) - shutil.copy(self._get_path(src_fname), fname) - fname_list.append(fname) - if series.get('cover'): - src_fname = '0000-cover-letter.patch' - cover_fname = os.path.join(self.tmpdir, src_fname) - fname = os.path.join(self.tmpdir, src_fname) - shutil.copy(self._get_path(src_fname), fname) - - return cover_fname, fname_list - - def test_basic(self): - """Tests the basic flow of patman - - This creates a series from some hard-coded patches build from a simple - tree with the following metadata in the top commit: - - Series-to: u-boot - Series-prefix: RFC - Series-postfix: some-branch - Series-cc: Stefan Brüns <[email protected]> - Cover-letter-cc: Lord Mëlchett <[email protected]> - Series-version: 3 - Patch-cc: fred - Series-process-log: sort, uniq - Series-changes: 4 - - Some changes - - Multi - line - change - - Commit-changes: 2 - - Changes only for this commit - - Cover-changes: 4 - - Some notes for the cover letter - - Cover-letter: - test: A test patch series - This is a test of how the cover - letter - works - END - - and this in the first commit: - - Commit-changes: 2 - - second revision change - - Series-notes: - some notes - about some things - from the first commit - END - - Commit-notes: - Some notes about - the first commit - END - - with the following commands: - - git log -n2 --reverse >/path/to/tools/patman/test/test01.txt - git format-patch --subject-prefix RFC --cover-letter HEAD~2 - mv 00* /path/to/tools/patman/test - - It checks these aspects: - - git log can be processed by patchstream - - emailing patches uses the correct command - - CC file has information on each commit - - cover letter has the expected text and subject - - each patch has the correct subject - - dry-run information prints out correctly - - unicode is handled correctly - - Series-to, Series-cc, Series-prefix, Series-postfix, Cover-letter - - Cover-letter-cc, Series-version, Series-changes, Series-notes - - Commit-notes - """ - process_tags = True - ignore_bad_tags = False - stefan = (b'Stefan Br\xc3\xbcns <[email protected]>' - .decode('utf-8')) - rick = 'Richard III <[email protected]>' - mel = b'Lord M\xc3\xablchett <[email protected]>'.decode('utf-8') - add_maintainers = [stefan, rick] - dry_run = True - in_reply_to = mel - count = 2 - alias = { - 'fdt': ['simon'], - 'u-boot': ['[email protected]'], - 'simon': [self.leb], - 'fred': [self.fred], - 'joe': [self.joe], - } - - text = self._get_text('test01.txt') - series = patchstream.get_metadata_for_test(text) - series.base_commit = Commit('1a44532') - series.branch = 'mybranch' - cover_fname, args = self._create_patches_for_test(series) - get_maintainer_script = str(pathlib.Path(__file__).parent.parent.parent - / 'get_maintainer.pl') + ' --norolestats' - with terminal.capture() as out: - patchstream.fix_patches(series, args) - if cover_fname and series.get('cover'): - patchstream.insert_cover_letter(cover_fname, series, count) - series.DoChecks() - cc_file = series.MakeCcFile(process_tags, cover_fname, - not ignore_bad_tags, add_maintainers, - None, get_maintainer_script, alias) - cmd = gitutil.email_patches( - series, cover_fname, args, dry_run, not ignore_bad_tags, - cc_file, alias, in_reply_to=in_reply_to, thread=None) - series.ShowActions(args, cmd, process_tags, alias) - cc_lines = tools.read_file(cc_file, binary=False).splitlines() - os.remove(cc_file) - - itr = iter(out[0].getvalue().splitlines()) - self.assertEqual('Cleaned %s patches' % len(series.commits), - next(itr)) - self.assertEqual('Change log missing for v2', next(itr)) - self.assertEqual('Change log missing for v3', next(itr)) - self.assertEqual('Change log for unknown version v4', next(itr)) - self.assertEqual("Alias 'pci' not found", next(itr)) - while next(itr) != 'Cc processing complete': - pass - self.assertIn('Dry run', next(itr)) - self.assertEqual('', next(itr)) - self.assertIn('Send a total of %d patches' % count, next(itr)) - prev = next(itr) - for i in range(len(series.commits)): - self.assertEqual(' %s' % args[i], prev) - while True: - prev = next(itr) - if 'Cc:' not in prev: - break - self.assertEqual('To: [email protected]', prev) - self.assertEqual('Cc: %s' % stefan, next(itr)) - self.assertEqual('Version: 3', next(itr)) - self.assertEqual('Prefix:\t RFC', next(itr)) - self.assertEqual('Postfix:\t some-branch', next(itr)) - self.assertEqual('Cover: 4 lines', next(itr)) - self.assertEqual(' Cc: %s' % self.fred, next(itr)) - self.assertEqual(' Cc: %s' % self.joe, next(itr)) - self.assertEqual(' Cc: %s' % self.leb, - next(itr)) - self.assertEqual(' Cc: %s' % mel, next(itr)) - self.assertEqual(' Cc: %s' % rick, next(itr)) - expected = ('Git command: git send-email --annotate ' - '--in-reply-to="%s" --to [email protected] ' - '--cc "%s" --cc-cmd "%s send --cc-cmd %s" %s %s' - % (in_reply_to, stefan, sys.argv[0], cc_file, cover_fname, - ' '.join(args))) - self.assertEqual(expected, next(itr)) - - self.assertEqual(('%s %s\0%s' % (args[0], rick, stefan)), cc_lines[0]) - self.assertEqual( - '%s %s\0%s\0%s\0%s\0%s' % (args[1], self.fred, self.joe, self.leb, - rick, stefan), - cc_lines[1]) - - expected = ''' -This is a test of how the cover -letter -works - -some notes -about some things -from the first commit - -Changes in v4: -- Multi - line - change -- Some changes -- Some notes for the cover letter -- fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base() - -Simon Glass (2): - pci: Correct cast for sandbox - fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base() - - cmd/pci.c | 3 ++- - fs/fat/fat.c | 1 + - lib/efi_loader/efi_memory.c | 1 + - lib/fdtdec.c | 3 ++- - 4 files changed, 6 insertions(+), 2 deletions(-) - ---\x20 -2.7.4 - -base-commit: 1a44532 -branch: mybranch -''' - lines = tools.read_file(cover_fname, binary=False).splitlines() - self.assertEqual( - 'Subject: [RFC PATCH some-branch v3 0/2] test: A test patch series', - lines[3]) - self.assertEqual(expected.splitlines(), lines[7:]) - - for i, fname in enumerate(args): - lines = tools.read_file(fname, binary=False).splitlines() - subject = [line for line in lines if line.startswith('Subject')] - self.assertEqual('Subject: [RFC %d/%d]' % (i + 1, count), - subject[0][:18]) - - # Check that we got our commit notes - start = 0 - expected = '' - - if i == 0: - start = 17 - expected = '''--- -Some notes about -the first commit - -(no changes since v2) - -Changes in v2: -- second revision change''' - elif i == 1: - start = 17 - expected = '''--- - -Changes in v4: -- Multi - line - change -- New -- Some changes - -Changes in v2: -- Changes only for this commit''' - - if expected: - expected = expected.splitlines() - self.assertEqual(expected, lines[start:(start+len(expected))]) - - def test_base_commit(self): - """Test adding a base commit with no cover letter""" - orig_text = self._get_text('test01.txt') - pos = orig_text.index( - 'commit 5ab48490f03051875ab13d288a4bf32b507d76fd') - text = orig_text[:pos] - series = patchstream.get_metadata_for_test(text) - series.base_commit = Commit('1a44532') - series.branch = 'mybranch' - cover_fname, args = self._create_patches_for_test(series) - self.assertFalse(cover_fname) - with terminal.capture() as out: - patchstream.fix_patches(series, args, insert_base_commit=True) - self.assertEqual('Cleaned 1 patch\n', out[0].getvalue()) - lines = tools.read_file(args[0], binary=False).splitlines() - pos = lines.index('-- ') - - # We expect these lines at the end: - # -- (with trailing space) - # 2.7.4 - # (empty) - # base-commit: xxx - # branch: xxx - self.assertEqual('base-commit: 1a44532', lines[pos + 3]) - self.assertEqual('branch: mybranch', lines[pos + 4]) - - def test_branch(self): - """Test creating patches from a branch""" - repo = self.make_git_tree() - target = repo.lookup_reference('refs/heads/first') - # pylint doesn't seem to find this - # pylint: disable=E1101 - self.repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE) - control.setup() - orig_dir = os.getcwd() - try: - os.chdir(self.tmpdir) - - # Check that it can detect the current branch - self.assertEqual(2, gitutil.count_commits_to_branch(None)) - col = terminal.Color() - with terminal.capture() as _: - _, cover_fname, patch_files = send.prepare_patches( - col, branch=None, count=-1, start=0, end=0, - ignore_binary=False, signoff=True) - self.assertIsNone(cover_fname) - self.assertEqual(2, len(patch_files)) - - # Check that it can detect a different branch - self.assertEqual(3, gitutil.count_commits_to_branch('second')) - with terminal.capture() as _: - _, cover_fname, patch_files = send.prepare_patches( - col, branch='second', count=-1, start=0, end=0, - ignore_binary=False, signoff=True) - self.assertIsNotNone(cover_fname) - self.assertEqual(3, len(patch_files)) - - cover = tools.read_file(cover_fname, binary=False) - lines = cover.splitlines()[-2:] - base = repo.lookup_reference('refs/heads/base').target - self.assertEqual(f'base-commit: {base}', lines[0]) - self.assertEqual('branch: second', lines[1]) - - # Make sure that the base-commit is not present when it is in the - # cover letter - for fname in patch_files: - self.assertNotIn(b'base-commit:', tools.read_file(fname)) - - # Check that it can skip patches at the end - with terminal.capture() as _: - _, cover_fname, patch_files = send.prepare_patches( - col, branch='second', count=-1, start=0, end=1, - ignore_binary=False, signoff=True) - self.assertIsNotNone(cover_fname) - self.assertEqual(2, len(patch_files)) - - cover = tools.read_file(cover_fname, binary=False) - lines = cover.splitlines()[-2:] - base2 = repo.lookup_reference('refs/heads/second') - ref = base2.peel(pygit2.GIT_OBJ_COMMIT).parents[0].parents[0].id - self.assertEqual(f'base-commit: {ref}', lines[0]) - self.assertEqual('branch: second', lines[1]) - finally: - os.chdir(orig_dir) - - def test_custom_get_maintainer_script(self): - """Validate that a custom get_maintainer script gets used.""" - self.make_git_tree() - with directory_excursion(self.tmpdir): - # Setup git. - os.environ['GIT_CONFIG_GLOBAL'] = '/dev/null' - os.environ['GIT_CONFIG_SYSTEM'] = '/dev/null' - tools.run('git', 'config', 'user.name', 'Dummy') - tools.run('git', 'config', 'user.email', '[email protected]') - tools.run('git', 'branch', 'upstream') - tools.run('git', 'branch', '--set-upstream-to=upstream') - - # Setup patman configuration. - tools.write_file('.patman', '[settings]\n' - 'get_maintainer_script: dummy-script.sh\n' - 'check_patch: False\n' - 'add_maintainers: True\n', binary=False) - tools.write_file('dummy-script.sh', - '#!/usr/bin/env python3\n' - 'print("[email protected]")\n', binary=False) - os.chmod('dummy-script.sh', 0x555) - tools.run('git', 'add', '.') - tools.run('git', 'commit', '-m', 'new commit') - - # Finally, do the test - with terminal.capture(): - output = tools.run(PATMAN_DIR / 'patman', '--dry-run') - # Assert the email address is part of the dry-run - # output. - self.assertIn('[email protected]', output) - - def test_tags(self): - """Test collection of tags in a patchstream""" - text = '''This is a patch - -Signed-off-by: Terminator -Reviewed-by: %s -Reviewed-by: %s -Tested-by: %s -''' % (self.joe, self.mary, self.leb) - pstrm = PatchStream.process_text(text) - self.assertEqual(pstrm.commit.rtags, { - 'Reviewed-by': {self.joe, self.mary}, - 'Tested-by': {self.leb}}) - - def test_invalid_tag(self): - """Test invalid tag in a patchstream""" - text = '''This is a patch - -Serie-version: 2 -''' - with self.assertRaises(ValueError) as exc: - PatchStream.process_text(text) - self.assertEqual("Line 3: Invalid tag = 'Serie-version: 2'", - str(exc.exception)) - - def test_missing_end(self): - """Test a missing END tag""" - text = '''This is a patch - -Cover-letter: -This is the title -missing END after this line -Signed-off-by: Fred -''' - pstrm = PatchStream.process_text(text) - self.assertEqual(["Missing 'END' in section 'cover'"], - pstrm.commit.warn) - - def test_missing_blank_line(self): - """Test a missing blank line after a tag""" - text = '''This is a patch - -Series-changes: 2 -- First line of changes -- Missing blank line after this line -Signed-off-by: Fred -''' - pstrm = PatchStream.process_text(text) - self.assertEqual(["Missing 'blank line' in section 'Series-changes'"], - pstrm.commit.warn) - - def test_invalid_commit_tag(self): - """Test an invalid Commit-xxx tag""" - text = '''This is a patch - -Commit-fred: testing -''' - pstrm = PatchStream.process_text(text) - self.assertEqual(["Line 3: Ignoring Commit-fred"], pstrm.commit.warn) - - def test_self_test(self): - """Test a tested by tag by this user""" - test_line = 'Tested-by: %[email protected]' % os.getenv('USER') - text = '''This is a patch - -%s -''' % test_line - pstrm = PatchStream.process_text(text) - self.assertEqual(["Ignoring '%s'" % test_line], pstrm.commit.warn) - - def test_space_before_tab(self): - """Test a space before a tab""" - text = '''This is a patch - -+ \tSomething -''' - pstrm = PatchStream.process_text(text) - self.assertEqual(["Line 3/0 has space before tab"], pstrm.commit.warn) - - def test_lines_after_test(self): - """Test detecting lines after TEST= line""" - text = '''This is a patch - -TEST=sometest -more lines -here -''' - pstrm = PatchStream.process_text(text) - self.assertEqual(["Found 2 lines after TEST="], pstrm.commit.warn) - - def test_blank_line_at_end(self): - """Test detecting a blank line at the end of a file""" - text = '''This is a patch - -diff --git a/lib/fdtdec.c b/lib/fdtdec.c -index c072e54..942244f 100644 ---- a/lib/fdtdec.c -+++ b/lib/fdtdec.c -@@ -1200,7 +1200,8 @@ int fdtdec_setup_mem_size_base(void) - \t} - - \tgd->ram_size = (phys_size_t)(res.end - res.start + 1); -- debug("%s: Initial DRAM size %llx\n", __func__, (u64)gd->ram_size); -+ debug("%s: Initial DRAM size %llx\n", __func__, -+ (unsigned long long)gd->ram_size); -+ -diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c - --- -2.7.4 - - ''' - pstrm = PatchStream.process_text(text) - self.assertEqual( - ["Found possible blank line(s) at end of file 'lib/fdtdec.c'"], - pstrm.commit.warn) - - def test_no_upstream(self): - """Test CountCommitsToBranch when there is no upstream""" - repo = self.make_git_tree() - target = repo.lookup_reference('refs/heads/base') - # pylint doesn't seem to find this - # pylint: disable=E1101 - self.repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE) - - # Check that it can detect the current branch - orig_dir = os.getcwd() - try: - os.chdir(self.gitdir) - with self.assertRaises(ValueError) as exc: - gitutil.count_commits_to_branch(None) - self.assertIn( - "Failed to determine upstream: fatal: no upstream configured for branch 'base'", - str(exc.exception)) - finally: - os.chdir(orig_dir) - - def run_patman(self, *args): - """Run patman using the provided arguments - - This runs the patman executable from scratch, as opposed to calling - the control.do_patman() function. - - Args: - args (list of str): Arguments to pass (excluding argv[0]) - - Return: - CommandResult: Result of execution - """ - all_args = [self._patman_pathname] + list(args) - return command.run_one(*all_args, capture=True, capture_stderr=True) - - def test_full_help(self): - """Test getting full help""" - command.TEST_RESULT = None - result = self.run_patman('-H') - help_file = os.path.join(self._patman_dir, 'README.rst') - # Remove possible extraneous strings - extra = '::::::::::::::\n' + help_file + '\n::::::::::::::\n' - gothelp = result.stdout.replace(extra, '') - self.assertEqual(len(gothelp), os.path.getsize(help_file)) - self.assertEqual(0, len(result.stderr)) - self.assertEqual(0, result.return_code) - - def test_help(self): - """Test getting help with commands and arguments""" - command.TEST_RESULT = None - result = self.run_patman('-h') - self.assertTrue(len(result.stdout) > 1000) - self.assertEqual(0, len(result.stderr)) - self.assertEqual(0, result.return_code) - - @staticmethod - def _fake_patchwork(subpath): - """Fake Patchwork server for the function below - - This handles accessing a series, providing a list consisting of a - single patch - - Args: - subpath (str): URL subpath to use - """ - re_series = re.match(r'series/(\d*)/$', subpath) - if re_series: - series_num = re_series.group(1) - if series_num == '1234': - return {'patches': [ - {'id': '1', 'name': 'Some patch'}]} - raise ValueError('Fake Patchwork does not understand: %s' % subpath) - - def test_status_mismatch(self): - """Test Patchwork patches not matching the series""" - pwork = patchwork.Patchwork.for_testing(self._fake_patchwork) - with terminal.capture() as (_, err): - loop = asyncio.get_event_loop() - _, patches = loop.run_until_complete(status.check_status(1234, - pwork)) - status.check_patch_count(0, len(patches)) - self.assertIn('Warning: Patchwork reports 1 patches, series has 0', - err.getvalue()) - - def test_status_read_patch(self): - """Test handling a single patch in Patchwork""" - pwork = patchwork.Patchwork.for_testing(self._fake_patchwork) - loop = asyncio.get_event_loop() - _, patches = loop.run_until_complete(status.check_status(1234, pwork)) - self.assertEqual(1, len(patches)) - patch = patches[0] - self.assertEqual('1', patch.id) - self.assertEqual('Some patch', patch.raw_subject) - - def test_parse_subject(self): - """Test parsing of the patch subject""" - patch = patchwork.Patch('1') - - # Simple patch not in a series - patch.parse_subject('Testing') - self.assertEqual('Testing', patch.raw_subject) - self.assertEqual('Testing', patch.subject) - self.assertEqual(1, patch.seq) - self.assertEqual(1, patch.count) - self.assertEqual(None, patch.prefix) - self.assertEqual(None, patch.version) - - # First patch in a series - patch.parse_subject('[1/2] Testing') - self.assertEqual('[1/2] Testing', patch.raw_subject) - self.assertEqual('Testing', patch.subject) - self.assertEqual(1, patch.seq) - self.assertEqual(2, patch.count) - self.assertEqual(None, patch.prefix) - self.assertEqual(None, patch.version) - - # Second patch in a series - patch.parse_subject('[2/2] Testing') - self.assertEqual('Testing', patch.subject) - self.assertEqual(2, patch.seq) - self.assertEqual(2, patch.count) - self.assertEqual(None, patch.prefix) - self.assertEqual(None, patch.version) - - # With PATCH prefix - patch.parse_subject('[PATCH,2/5] Testing') - self.assertEqual('Testing', patch.subject) - self.assertEqual(2, patch.seq) - self.assertEqual(5, patch.count) - self.assertEqual('PATCH', patch.prefix) - self.assertEqual(None, patch.version) - - # RFC patch - patch.parse_subject('[RFC,3/7] Testing') - self.assertEqual('Testing', patch.subject) - self.assertEqual(3, patch.seq) - self.assertEqual(7, patch.count) - self.assertEqual('RFC', patch.prefix) - self.assertEqual(None, patch.version) - - # Version patch - patch.parse_subject('[v2,3/7] Testing') - self.assertEqual('Testing', patch.subject) - self.assertEqual(3, patch.seq) - self.assertEqual(7, patch.count) - self.assertEqual(None, patch.prefix) - self.assertEqual('v2', patch.version) - - # All fields - patch.parse_subject('[RESEND,v2,3/7] Testing') - self.assertEqual('Testing', patch.subject) - self.assertEqual(3, patch.seq) - self.assertEqual(7, patch.count) - self.assertEqual('RESEND', patch.prefix) - self.assertEqual('v2', patch.version) - - # RFC only - patch.parse_subject('[RESEND] Testing') - self.assertEqual('Testing', patch.subject) - self.assertEqual(1, patch.seq) - self.assertEqual(1, patch.count) - self.assertEqual('RESEND', patch.prefix) - self.assertEqual(None, patch.version) - - def test_compare_series(self): - """Test operation of compare_with_series()""" - commit1 = Commit('abcd') - commit1.subject = 'Subject 1' - commit2 = Commit('ef12') - commit2.subject = 'Subject 2' - commit3 = Commit('3456') - commit3.subject = 'Subject 2' - - patch1 = patchwork.Patch('1') - patch1.subject = 'Subject 1' - patch2 = patchwork.Patch('2') - patch2.subject = 'Subject 2' - patch3 = patchwork.Patch('3') - patch3.subject = 'Subject 2' - - series = Series() - series.commits = [commit1] - patches = [patch1] - patch_for_commit, commit_for_patch, warnings = ( - status.compare_with_series(series, patches)) - self.assertEqual(1, len(patch_for_commit)) - self.assertEqual(patch1, patch_for_commit[0]) - self.assertEqual(1, len(commit_for_patch)) - self.assertEqual(commit1, commit_for_patch[0]) - - series.commits = [commit1] - patches = [patch1, patch2] - patch_for_commit, commit_for_patch, warnings = ( - status.compare_with_series(series, patches)) - self.assertEqual(1, len(patch_for_commit)) - self.assertEqual(patch1, patch_for_commit[0]) - self.assertEqual(1, len(commit_for_patch)) - self.assertEqual(commit1, commit_for_patch[0]) - self.assertEqual(["Cannot find commit for patch 2 ('Subject 2')"], - warnings) - - series.commits = [commit1, commit2] - patches = [patch1] - patch_for_commit, commit_for_patch, warnings = ( - status.compare_with_series(series, patches)) - self.assertEqual(1, len(patch_for_commit)) - self.assertEqual(patch1, patch_for_commit[0]) - self.assertEqual(1, len(commit_for_patch)) - self.assertEqual(commit1, commit_for_patch[0]) - self.assertEqual(["Cannot find patch for commit 2 ('Subject 2')"], - warnings) - - series.commits = [commit1, commit2, commit3] - patches = [patch1, patch2] - patch_for_commit, commit_for_patch, warnings = ( - status.compare_with_series(series, patches)) - self.assertEqual(2, len(patch_for_commit)) - self.assertEqual(patch1, patch_for_commit[0]) - self.assertEqual(patch2, patch_for_commit[1]) - self.assertEqual(1, len(commit_for_patch)) - self.assertEqual(commit1, commit_for_patch[0]) - self.assertEqual(["Cannot find patch for commit 3 ('Subject 2')", - "Multiple commits match patch 2 ('Subject 2'):\n" - ' Subject 2\n Subject 2'], - warnings) - - series.commits = [commit1, commit2] - patches = [patch1, patch2, patch3] - patch_for_commit, commit_for_patch, warnings = ( - status.compare_with_series(series, patches)) - self.assertEqual(1, len(patch_for_commit)) - self.assertEqual(patch1, patch_for_commit[0]) - self.assertEqual(2, len(commit_for_patch)) - self.assertEqual(commit1, commit_for_patch[0]) - self.assertEqual(["Multiple patches match commit 2 ('Subject 2'):\n" - ' Subject 2\n Subject 2', - "Cannot find commit for patch 3 ('Subject 2')"], - warnings) - - def _fake_patchwork2(self, subpath): - """Fake Patchwork server for the function below - - This handles accessing series, patches and comments, providing the data - in self.patches to the caller - - Args: - subpath (str): URL subpath to use - """ - re_series = re.match(r'series/(\d*)/$', subpath) - re_patch = re.match(r'patches/(\d*)/$', subpath) - re_comments = re.match(r'patches/(\d*)/comments/$', subpath) - if re_series: - series_num = re_series.group(1) - if series_num == '1234': - return {'patches': self.patches} - elif re_patch: - patch_num = int(re_patch.group(1)) - patch = self.patches[patch_num - 1] - return patch - elif re_comments: - patch_num = int(re_comments.group(1)) - patch = self.patches[patch_num - 1] - return patch.comments - raise ValueError('Fake Patchwork does not understand: %s' % subpath) - - def test_find_new_responses(self): - """Test operation of find_new_responses()""" - commit1 = Commit('abcd') - commit1.subject = 'Subject 1' - commit2 = Commit('ef12') - commit2.subject = 'Subject 2' - - patch1 = patchwork.Patch('1') - patch1.parse_subject('[1/2] Subject 1') - patch1.name = patch1.raw_subject - patch1.content = 'This is my patch content' - comment1a = {'content': 'Reviewed-by: %s\n' % self.joe} - - patch1.comments = [comment1a] - - patch2 = patchwork.Patch('2') - patch2.parse_subject('[2/2] Subject 2') - patch2.name = patch2.raw_subject - patch2.content = 'Some other patch content' - comment2a = { - 'content': 'Reviewed-by: %s\nTested-by: %s\n' % - (self.mary, self.leb)} - comment2b = {'content': 'Reviewed-by: %s' % self.fred} - patch2.comments = [comment2a, comment2b] - - # This test works by setting up commits and patch for use by the fake - # Rest API function _fake_patchwork2(). It calls various functions in - # the status module after setting up tags in the commits, checking that - # things behaves as expected - self.commits = [commit1, commit2] - self.patches = [patch1, patch2] - - # Check that the tags are picked up on the first patch - new_rtags, _ = status.process_reviews(patch1.content, patch1.comments, - commit1.rtags) - self.assertEqual(new_rtags, {'Reviewed-by': {self.joe}}) - - # Now the second patch - new_rtags, _ = status.process_reviews(patch2.content, patch2.comments, - commit2.rtags) - self.assertEqual(new_rtags, { - 'Reviewed-by': {self.mary, self.fred}, - 'Tested-by': {self.leb}}) - - # Now add some tags to the commit, which means they should not appear as - # 'new' tags when scanning comments - commit1.rtags = {'Reviewed-by': {self.joe}} - new_rtags, _ = status.process_reviews(patch1.content, patch1.comments, - commit1.rtags) - self.assertEqual(new_rtags, {}) - - # For the second commit, add Ed and Fred, so only Mary should be left - commit2.rtags = { - 'Tested-by': {self.leb}, - 'Reviewed-by': {self.fred}} - new_rtags, _ = status.process_reviews(patch2.content, patch2.comments, - commit2.rtags) - self.assertEqual(new_rtags, {'Reviewed-by': {self.mary}}) - - # Check that the output patches expectations: - # 1 Subject 1 - # Reviewed-by: Joe Bloggs <[email protected]> - # 2 Subject 2 - # Tested-by: Lord Edmund Blackaddër <[email protected]> - # Reviewed-by: Fred Bloggs <[email protected]> - # + Reviewed-by: Mary Bloggs <[email protected]> - # 1 new response available in patchwork - - series = Series() - series.commits = [commit1, commit2] - terminal.set_print_test_mode() - pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2) - status.check_and_show_status(series, '1234', None, None, False, False, - False, pwork) - itr = iter(terminal.get_print_test_lines()) - col = terminal.Color() - self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.YELLOW), - next(itr)) - self.assertEqual( - terminal.PrintLine(' Reviewed-by: ', col.GREEN, newline=False, - bright=False), - next(itr)) - self.assertEqual(terminal.PrintLine(self.joe, col.WHITE, bright=False), - next(itr)) - - self.assertEqual(terminal.PrintLine(' 2 Subject 2', col.YELLOW), - next(itr)) - self.assertEqual( - terminal.PrintLine(' Reviewed-by: ', col.GREEN, newline=False, - bright=False), - next(itr)) - self.assertEqual(terminal.PrintLine(self.fred, col.WHITE, - bright=False), next(itr)) - self.assertEqual( - terminal.PrintLine(' Tested-by: ', col.GREEN, newline=False, - bright=False), - next(itr)) - self.assertEqual(terminal.PrintLine(self.leb, col.WHITE, bright=False), - next(itr)) - self.assertEqual( - terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False), - next(itr)) - self.assertEqual(terminal.PrintLine(self.mary, col.WHITE), - next(itr)) - self.assertEqual(terminal.PrintLine( - '1 new response available in patchwork (use -d to write them to a new branch)', - None), next(itr)) - - def _fake_patchwork3(self, subpath): - """Fake Patchwork server for the function below - - This handles accessing series, patches and comments, providing the data - in self.patches to the caller - - Args: - subpath (str): URL subpath to use - """ - re_series = re.match(r'series/(\d*)/$', subpath) - re_patch = re.match(r'patches/(\d*)/$', subpath) - re_comments = re.match(r'patches/(\d*)/comments/$', subpath) - if re_series: - series_num = re_series.group(1) - if series_num == '1234': - return {'patches': self.patches} - elif re_patch: - patch_num = int(re_patch.group(1)) - patch = self.patches[patch_num - 1] - return patch - elif re_comments: - patch_num = int(re_comments.group(1)) - patch = self.patches[patch_num - 1] - return patch.comments - raise ValueError('Fake Patchwork does not understand: %s' % subpath) - - def test_create_branch(self): - """Test operation of create_branch()""" - repo = self.make_git_tree() - branch = 'first' - dest_branch = 'first2' - count = 2 - gitdir = self.gitdir - - # Set up the test git tree. We use branch 'first' which has two commits - # in it - series = patchstream.get_metadata_for_list(branch, gitdir, count) - self.assertEqual(2, len(series.commits)) - - patch1 = patchwork.Patch('1') - patch1.parse_subject('[1/2] %s' % series.commits[0].subject) - patch1.name = patch1.raw_subject - patch1.content = 'This is my patch content' - comment1a = {'content': 'Reviewed-by: %s\n' % self.joe} - - patch1.comments = [comment1a] - - patch2 = patchwork.Patch('2') - patch2.parse_subject('[2/2] %s' % series.commits[1].subject) - patch2.name = patch2.raw_subject - patch2.content = 'Some other patch content' - comment2a = { - 'content': 'Reviewed-by: %s\nTested-by: %s\n' % - (self.mary, self.leb)} - comment2b = { - 'content': 'Reviewed-by: %s' % self.fred} - patch2.comments = [comment2a, comment2b] - - # This test works by setting up patches for use by the fake Rest API - # function _fake_patchwork3(). The fake patch comments above should - # result in new review tags that are collected and added to the commits - # created in the destination branch. - self.patches = [patch1, patch2] - count = 2 - - # Expected output: - # 1 i2c: I2C things - # + Reviewed-by: Joe Bloggs <[email protected]> - # 2 spi: SPI fixes - # + Reviewed-by: Fred Bloggs <[email protected]> - # + Reviewed-by: Mary Bloggs <[email protected]> - # + Tested-by: Lord Edmund Blackaddër <[email protected]> - # 4 new responses available in patchwork - # 4 responses added from patchwork into new branch 'first2' - # <unittest.result.TestResult run=8 errors=0 failures=0> - - terminal.set_print_test_mode() - pwork = patchwork.Patchwork.for_testing(self._fake_patchwork3) - status.check_and_show_status( - series, '1234', branch, dest_branch, False, False, False, pwork, - repo) - lines = terminal.get_print_test_lines() - self.assertEqual(12, len(lines)) - self.assertEqual( - "4 responses added from patchwork into new branch 'first2'", - lines[11].text) - - # Check that the destination branch has the new tags - new_series = patchstream.get_metadata_for_list(dest_branch, gitdir, - count) - self.assertEqual( - {'Reviewed-by': {self.joe}}, - new_series.commits[0].rtags) - self.assertEqual( - {'Tested-by': {self.leb}, - 'Reviewed-by': {self.fred, self.mary}}, - new_series.commits[1].rtags) - - # Now check the actual test of the first commit message. We expect to - # see the new tags immediately below the old ones. - stdout = patchstream.get_list(dest_branch, count=count, git_dir=gitdir) - itr = iter([line.strip() for line in stdout.splitlines() - if '-by:' in line]) - - # First patch should have the review tag - self.assertEqual('Reviewed-by: %s' % self.joe, next(itr)) - - # Second patch should have the sign-off then the tested-by and two - # reviewed-by tags - self.assertEqual('Signed-off-by: %s' % self.leb, next(itr)) - self.assertEqual('Reviewed-by: %s' % self.fred, next(itr)) - self.assertEqual('Reviewed-by: %s' % self.mary, next(itr)) - self.assertEqual('Tested-by: %s' % self.leb, next(itr)) - - def test_parse_snippets(self): - """Test parsing of review snippets""" - text = '''Hi Fred, - -This is a comment from someone. - -Something else - -On some recent date, Fred wrote: -> This is why I wrote the patch -> so here it is - -Now a comment about the commit message -A little more to say - -Even more - -> diff --git a/file.c b/file.c -> Some more code -> Code line 2 -> Code line 3 -> Code line 4 -> Code line 5 -> Code line 6 -> Code line 7 -> Code line 8 -> Code line 9 - -And another comment - -> @@ -153,8 +143,13 @@ def check_patch(fname, show_types=False): -> further down on the file -> and more code -> +Addition here -> +Another addition here -> codey -> more codey - -and another thing in same file - -> @@ -253,8 +243,13 @@ -> with no function context - -one more thing - -> diff --git a/tools/patman/main.py b/tools/patman/main.py -> +line of code -now a very long comment in a different file -line2 -line3 -line4 -line5 -line6 -line7 -line8 -''' - pstrm = PatchStream.process_text(text, True) - self.assertEqual([], pstrm.commit.warn) - - # We expect to the filename and up to 5 lines of code context before - # each comment. The 'On xxx wrote:' bit should be removed. - self.assertEqual( - [['Hi Fred,', - 'This is a comment from someone.', - 'Something else'], - ['> This is why I wrote the patch', - '> so here it is', - 'Now a comment about the commit message', - 'A little more to say', 'Even more'], - ['> File: file.c', '> Code line 5', '> Code line 6', - '> Code line 7', '> Code line 8', '> Code line 9', - 'And another comment'], - ['> File: file.c', - '> Line: 153 / 143: def check_patch(fname, show_types=False):', - '> and more code', '> +Addition here', - '> +Another addition here', '> codey', '> more codey', - 'and another thing in same file'], - ['> File: file.c', '> Line: 253 / 243', - '> with no function context', 'one more thing'], - ['> File: tools/patman/main.py', '> +line of code', - 'now a very long comment in a different file', - 'line2', 'line3', 'line4', 'line5', 'line6', 'line7', 'line8']], - pstrm.snippets) - - def test_review_snippets(self): - """Test showing of review snippets""" - def _to_submitter(who): - m_who = re.match('(.*) <(.*)>', who) - return { - 'name': m_who.group(1), - 'email': m_who.group(2) - } - - commit1 = Commit('abcd') - commit1.subject = 'Subject 1' - commit2 = Commit('ef12') - commit2.subject = 'Subject 2' - - patch1 = patchwork.Patch('1') - patch1.parse_subject('[1/2] Subject 1') - patch1.name = patch1.raw_subject - patch1.content = 'This is my patch content' - comment1a = {'submitter': _to_submitter(self.joe), - 'content': '''Hi Fred, - -On some date Fred wrote: - -> diff --git a/file.c b/file.c -> Some code -> and more code - -Here is my comment above the above... - - -Reviewed-by: %s -''' % self.joe} - - patch1.comments = [comment1a] - - patch2 = patchwork.Patch('2') - patch2.parse_subject('[2/2] Subject 2') - patch2.name = patch2.raw_subject - patch2.content = 'Some other patch content' - comment2a = { - 'content': 'Reviewed-by: %s\nTested-by: %s\n' % - (self.mary, self.leb)} - comment2b = {'submitter': _to_submitter(self.fred), - 'content': '''Hi Fred, - -On some date Fred wrote: - -> diff --git a/tools/patman/commit.py b/tools/patman/commit.py -> @@ -41,6 +41,9 @@ class Commit: -> self.rtags = collections.defaultdict(set) -> self.warn = [] -> -> + def __str__(self): -> + return self.subject -> + -> def add_change(self, version, info): -> """Add a new change line to the change list for a version. -> -A comment - -Reviewed-by: %s -''' % self.fred} - patch2.comments = [comment2a, comment2b] - - # This test works by setting up commits and patch for use by the fake - # Rest API function _fake_patchwork2(). It calls various functions in - # the status module after setting up tags in the commits, checking that - # things behaves as expected - self.commits = [commit1, commit2] - self.patches = [patch1, patch2] - - # Check that the output patches expectations: - # 1 Subject 1 - # Reviewed-by: Joe Bloggs <[email protected]> - # 2 Subject 2 - # Tested-by: Lord Edmund Blackaddër <[email protected]> - # Reviewed-by: Fred Bloggs <[email protected]> - # + Reviewed-by: Mary Bloggs <[email protected]> - # 1 new response available in patchwork - - series = Series() - series.commits = [commit1, commit2] - terminal.set_print_test_mode() - pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2) - status.check_and_show_status( - series, '1234', None, None, False, True, False, pwork) - itr = iter(terminal.get_print_test_lines()) - col = terminal.Color() - self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.YELLOW), - next(itr)) - self.assertEqual( - terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False), - next(itr)) - self.assertEqual(terminal.PrintLine(self.joe, col.WHITE), next(itr)) - - self.assertEqual(terminal.PrintLine('Review: %s' % self.joe, col.RED), - next(itr)) - self.assertEqual(terminal.PrintLine(' Hi Fred,', None), next(itr)) - self.assertEqual(terminal.PrintLine('', None), next(itr)) - self.assertEqual(terminal.PrintLine(' > File: file.c', col.MAGENTA), - next(itr)) - self.assertEqual(terminal.PrintLine(' > Some code', col.MAGENTA), - next(itr)) - self.assertEqual(terminal.PrintLine(' > and more code', - col.MAGENTA), - next(itr)) - self.assertEqual(terminal.PrintLine( - ' Here is my comment above the above...', None), next(itr)) - self.assertEqual(terminal.PrintLine('', None), next(itr)) - - self.assertEqual(terminal.PrintLine(' 2 Subject 2', col.YELLOW), - next(itr)) - self.assertEqual( - terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False), - next(itr)) - self.assertEqual(terminal.PrintLine(self.fred, col.WHITE), - next(itr)) - self.assertEqual( - terminal.PrintLine(' + Reviewed-by: ', col.GREEN, newline=False), - next(itr)) - self.assertEqual(terminal.PrintLine(self.mary, col.WHITE), - next(itr)) - self.assertEqual( - terminal.PrintLine(' + Tested-by: ', col.GREEN, newline=False), - next(itr)) - self.assertEqual(terminal.PrintLine(self.leb, col.WHITE), - next(itr)) - - self.assertEqual(terminal.PrintLine('Review: %s' % self.fred, col.RED), - next(itr)) - self.assertEqual(terminal.PrintLine(' Hi Fred,', None), next(itr)) - self.assertEqual(terminal.PrintLine('', None), next(itr)) - self.assertEqual(terminal.PrintLine( - ' > File: tools/patman/commit.py', col.MAGENTA), next(itr)) - self.assertEqual(terminal.PrintLine( - ' > Line: 41 / 41: class Commit:', col.MAGENTA), next(itr)) - self.assertEqual(terminal.PrintLine( - ' > + return self.subject', col.MAGENTA), next(itr)) - self.assertEqual(terminal.PrintLine( - ' > +', col.MAGENTA), next(itr)) - self.assertEqual( - terminal.PrintLine( - ' > def add_change(self, version, info):', - col.MAGENTA), - next(itr)) - self.assertEqual(terminal.PrintLine( - ' > """Add a new change line to the change list for a version.', - col.MAGENTA), next(itr)) - self.assertEqual(terminal.PrintLine( - ' >', col.MAGENTA), next(itr)) - self.assertEqual(terminal.PrintLine( - ' A comment', None), next(itr)) - self.assertEqual(terminal.PrintLine('', None), next(itr)) - - self.assertEqual(terminal.PrintLine( - '4 new responses available in patchwork (use -d to write them to a new branch)', - None), next(itr)) - - def test_insert_tags(self): - """Test inserting of review tags""" - msg = '''first line -second line.''' - tags = [ - 'Reviewed-by: Bin Meng <[email protected]>', - 'Tested-by: Bin Meng <[email protected]>' - ] - signoff = 'Signed-off-by: Simon Glass <[email protected]>' - tag_str = '\n'.join(tags) - - new_msg = patchstream.insert_tags(msg, tags) - self.assertEqual(msg + '\n\n' + tag_str, new_msg) - - new_msg = patchstream.insert_tags(msg + '\n', tags) - self.assertEqual(msg + '\n\n' + tag_str, new_msg) - - msg += '\n\n' + signoff - new_msg = patchstream.insert_tags(msg, tags) - self.assertEqual(msg + '\n' + tag_str, new_msg) 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 <[email protected]> -# -"""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": "[email protected]" - 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/ - [email protected]/", - "msgid": "<[email protected]>", - "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/ - [email protected]/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/ - [email protected]/", - "msgid": "<[email protected]>", - "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/ - [email protected]/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/ - [email protected]/", - project (dict): project information (id, url, name, link_name, - list_id, list_email, etc. - "msgid": "<[email protected]>", - "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": "[email protected]" - }, - "delegate": { - "id": 1, - "url": "https://patchwork.ozlabs.org/api/1.2/users/1/", - "username": "jk", - "first_name": "Jeremy", - "last_name": "Kerr", - "email": "[email protected]" - }, - "mbox": "https://patchwork.ozlabs.org/project/cbe-oss-dev/patch/ - [email protected]/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 <[email protected]>", - "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. - '<[email protected]>' - 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": "[email protected]" - 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/[email protected]/' - project (dict): project information (id, url, name, link_name, - list_id, list_email, etc. - msgid (str): Message ID, e.g. '[email protected]>' - 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": "[email protected]" - mbox (str): URL to mailbox, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/[email protected]/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": "<[email protected]>", - "X-Original-To": "[email protected]", - "Delivered-To": "[email protected]", - "Authentication-Results": [ - "legolas.ozlabs.org; -\tdkim=pass (1024-bit key; - unprotected) header.d=chromium.org [email protected] 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; - [email protected]; receiver=patchwork.ozlabs.org)", - "phobos.denx.de; - dmarc=pass (p=none dis=none) header.from=chromium.org", - "phobos.denx.de; - spf=pass [email protected]", - "phobos.denx.de; -\tdkim=pass (1024-bit key; - unprotected) header.d=chromium.org [email protected] - header.b=\"dG8yqtoK\"; -\tdkim-atps=neutral", - "phobos.denx.de; - dmarc=pass (p=none dis=none) header.from=chromium.org", - "phobos.denx.de; - spf=pass [email protected]" - ], - "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 <[email protected]>; 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 <[email protected]>; Tue, 4 Mar 2025 14:09:55 +0100 (CET)", - "by mail-io1-xd2e.google.com with SMTP id - ca18e2360f4ac-85ae33109f6so128326139f.2 - for <[email protected]>; 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 <[email protected]>", - "To": "U-Boot Mailing List <[email protected]>", - "Cc": "Simon Glass <[email protected]>, Alexander Kochetkov <[email protected]>, - Alper Nebi Yasak <[email protected]>, - Brandon Maier <[email protected]>, - Jerome Forissier <[email protected]>, - Jiaxun Yang <[email protected]>, - Neha Malcom Francis <[email protected]>, - Patrick Rudolph <[email protected]>, - Paul HENRYS <[email protected]>, Peng Fan <[email protected]>, - Philippe Reynes <[email protected]>, - Stefan Herbrechtsmeier <[email protected]>, - Tom Rini <[email protected]>", - "Subject": "[PATCH 0/7] binman: Check code-coverage requirements", - "Date": "Tue, 4 Mar 2025 06:09:37 -0700", - "Message-ID": "<[email protected]>", - "X-Mailer": "git-send-email 2.43.0", - "MIME-Version": "1.0", - "Content-Transfer-Encoding": "8bit", - "X-BeenThere": "[email protected]", - "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:[email protected]?subject=unsubscribe>", - "List-Archive": "<https://lists.denx.de/pipermail/u-boot/>", - "List-Post": "<mailto:[email protected]>", - "List-Help": "<mailto:[email protected]?subject=help>", - "List-Subscribe": "<https://lists.denx.de/listinfo/u-boot>, - <mailto:[email protected]?subject=subscribe>", - "Errors-To": "[email protected]", - "Sender": "\"U-Boot\" <[email protected]>", - "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/[email protected]/' - 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": "[email protected]" - content (str): Email content, e.g. 'Hi, - -On Tue, 4 Mar 2025 at 06:09, Simon Glass <[email protected]> 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/patman.rst b/tools/patman/patman.rst deleted file mode 100644 index 549e203c254..00000000000 --- a/tools/patman/patman.rst +++ /dev/null @@ -1,1023 +0,0 @@ -.. SPDX-License-Identifier: GPL-2.0+ -.. Copyright (c) 2011 The Chromium OS Authors -.. Simon Glass <[email protected]> -.. Maxim Cournoyer <[email protected]> -.. v1, v2, 19-Oct-11 -.. revised v3 24-Nov-11 -.. revised v4 Independence Day 2020, with Patchwork integration - -Patman patch manager -==================== - -This tool is a Python script which: - -- Creates patch directly from your branch -- Cleans them up by removing unwanted tags -- Inserts a cover letter with change lists -- Runs the patches through checkpatch.pl and its own checks -- Optionally emails them out to selected people -- Links the series automatically to Patchwork once sent - -It also has some Patchwork features: - -- Manage local series and their status on patchwork -- Show review tags from Patchwork and allows them to be gathered into commits -- List comments received on a series - -It is intended to automate patch creation and make it a less -error-prone process. It is useful for U-Boot and Linux work so far, -since they use the checkpatch.pl script. - -It is configured almost entirely by tags it finds in your commits. -This means that you can work on a number of different branches at -once, and keep the settings with each branch rather than having to -git format-patch, git send-email, etc. with the correct parameters -each time. So for example if you put:: - - Series-to: [email protected] - -in one of your commits, the series will be sent there. - -In Linux and U-Boot this will also call get_maintainer.pl on each of your -patches automatically (unless you use -m to disable this). - - -Installation ------------- - -You can install patman using:: - - pip install patch-manager - -The name is chosen since patman conflicts with an existing package. - -If you are using patman within the U-Boot tree, it may be easiest to add a -symlink from your local `~/.bin` directory to `/path/to/tools/patman/patman`. - -How to use this tool --------------------- - -This tool requires a certain way of working: - -- Maintain a number of branches, one for each patch series you are - working on -- Add tags into the commits within each branch to indicate where the - series should be sent, cover letter, version, etc. Most of these are - normally in the top commit so it is easy to change them with 'git - commit --amend' -- Each branch tracks the upstream branch, so that this script can - automatically determine the number of commits in it (optional) -- Check out a branch, and run this script to create and send out your - patches. Weeks later, change the patches and repeat, knowing that you - will get a consistent result each time. - - -How to configure it -------------------- - -For most cases of using patman for U-Boot development, patman can use the -file 'doc/git-mailrc' in your U-Boot directory to supply the email aliases -you need. To make this work, tell git where to find the file by typing -this once:: - - git config sendemail.aliasesfile doc/git-mailrc - -For both Linux and U-Boot the 'scripts/get_maintainer.pl' handles -figuring out where to send patches pretty well. For other projects, -you may want to specify a different script to be run, for example via -a project-specific `.patman` file:: - - # .patman configuration file at the root of some project - - [settings] - get_maintainer_script: etc/teams.scm get-maintainer - -The `get_maintainer_script` option corresponds to the -`--get-maintainer-script` argument of the `send` command. It is -looked relatively to the root of the current git repository, as well -as on PATH. It can also be provided arguments, as shown above. The -contract is that the script should accept a patch file name and return -a list of email addresses, one per line, like `get_maintainer.pl` -does. - -During the first run patman creates a config file for you by taking the default -user name and email address from the global .gitconfig file. - -To add your own, create a file `~/.patman` like this:: - - # patman alias file - - [alias] - me: Simon Glass <[email protected]> - - u-boot: U-Boot Mailing List <[email protected]> - wolfgang: Wolfgang Denk <[email protected]> - others: Mike Frysinger <[email protected]>, Fred Bloggs <[email protected]> - -As hinted above, Patman will also look for a `.patman` configuration -file at the root of the current project git repository, which makes it -possible to override the `project` settings variable or anything else -in a project-specific way. The values of this "local" configuration -file take precedence over those of the "global" one. - -Aliases are recursive. - -The checkpatch.pl in the U-Boot tools/ subdirectory will be located and -used. Failing that you can put it into your path or ~/bin/checkpatch.pl - -If you want to avoid sending patches to email addresses that are picked up -by patman but are known to bounce you can add a [bounces] section to your -.patman file. Unlike the [alias] section these are simple key: value pairs -that are not recursive:: - - [bounces] - gonefishing: Fred Bloggs <[email protected]> - - -If you want to change the defaults for patman's command-line arguments, -you can add a [settings] section to your .patman file. This can be used -for any command line option by referring to the "dest" for the option in -patman.py. For reference, the useful ones (at the moment) shown below -(all with the non-default setting):: - - [settings] - ignore_errors: True - process_tags: False - verbose: True - smtp_server: /path/to/sendmail - patchwork_url: https://patchwork.ozlabs.org - -If you want to adjust settings (or aliases) that affect just a single -project you can add a section that looks like [project_settings] or -[project_alias]. If you want to use tags for your linux work, you could do:: - - [linux_settings] - process_tags: True - - -How to run it -------------- - -First do a dry run: - -.. code-block:: bash - - ./tools/patman/patman send -n - -If it can't detect the upstream branch, try telling it how many patches -there are in your series - -.. code-block:: bash - - ./tools/patman/patman -c5 send -n - -This will create patch files in your current directory and tell you who -it is thinking of sending them to. Take a look at the patch files: - -.. code-block:: bash - - ./tools/patman/patman -c5 -s1 send -n - -Similar to the above, but skip the first commit and take the next 5. This -is useful if your top commit is for setting up testing. - - -How to install it ------------------ - -The most up to date version of patman can be found in the U-Boot sources. -However to use it on other projects it may be more convenient to install it as -a standalone application. A distutils installer is included, this can be used -to install patman: - -.. code-block:: bash - - cd tools/patman && python setup.py install - - -How to add tags ---------------- - -To make this script useful you must add tags like the following into any -commit. Most can only appear once in the whole series. - -Series-to: email / alias - Email address / alias to send patch series to (you can add this - multiple times) - -Series-cc: email / alias, ... - Email address / alias to Cc patch series to (you can add this - multiple times) - -Series-version: n - Sets the version number of this patch series - -Series-prefix: prefix - Sets the subject prefix. Normally empty but it can be RFC for - RFC patches, or RESEND if you are being ignored. The patch subject - is like [RFC PATCH] or [RESEND PATCH]. - In the meantime, git format.subjectprefix option will be added as - well. If your format.subjectprefix is set to InternalProject, then - the patch shows like: [InternalProject][RFC/RESEND PATCH] - -Series-postfix: postfix - Sets the subject "postfix". Normally empty, but can be the name of a - tree such as net or net-next if that needs to be specified. The patch - subject is like [PATCH net] or [PATCH net-next]. - -Series-name: name - Sets the name of the series. You don't need to have a name, and - patman does not yet use it, but it is convenient to put the branch - name here to help you keep track of multiple upstreaming efforts. - -Series-links: [id | version:id]... - Set the ID of the series in patchwork. You can set this after you send - out the series and look in patchwork for the resulting series. The - URL you want is the one for the series itself, not any particular patch. - E.g. for http://patchwork.ozlabs.org/project/uboot/list/?series=187331 - the series ID is 187331. This property can have a list of series IDs, - one for each version of the series, e.g. - - :: - - Series-links: 1:187331 2:188434 189372 - - Patman always uses the one without a version, since it assumes this is - the latest one. When this tag is provided, patman can compare your local - branch against patchwork to see what new reviews your series has - collected ('patman status'). - -Series-patchwork-url: url - This allows specifying the Patchwork URL for a branch. This overrides - both the setting files ("patchwork_url") and the command-line argument. - The URL should include the protocol and web site, with no trailing slash, - for example 'https://patchwork.ozlabs.org/project' - -Cover-letter: - Sets the cover letter contents for the series. The first line - will become the subject of the cover letter:: - - Cover-letter: - This is the patch set title - blah blah - more blah blah - END - -Cover-letter-cc: email / alias - Additional email addresses / aliases to send cover letter to (you - can add this multiple times) - -Series-notes: - Sets some notes for the patch series, which you don't want in - the commit messages, but do want to send, The notes are joined - together and put after the cover letter. Can appear multiple - times:: - - Series-notes: - blah blah - blah blah - more blah blah - END - -Commit-notes: - Similar, but for a single commit (patch). These notes will appear - immediately below the ``---`` cut in the patch file:: - - Commit-notes: - blah blah - blah blah - more blah blah - -Signed-off-by: Their Name <email> - A sign-off is added automatically to your patches (this is - probably a bug). If you put this tag in your patches, it will - override the default signoff that patman automatically adds. - Multiple duplicate signoffs will be removed. - -Tested-by / Reviewed-by / Acked-by - These indicate that someone has tested/reviewed/acked your patch. - When you get this reply on the mailing list, you can add this - tag to the relevant commit and the script will include it when - you send out the next version. If 'Tested-by:' is set to - yourself, it will be removed. No one will believe you. - - Example:: - - Tested-by: Their Name <[email protected]> - Reviewed-by: Their Name <email> - Acked-by: Their Name <email> - -Series-changes: n - This can appear in any commit. It lists the changes for a - particular version n of that commit. The change list is - created based on this information. Each commit gets its own - change list and also the whole thing is repeated in the cover - letter (where duplicate change lines are merged). - - By adding your change lists into your commits it is easier to - keep track of what happened. When you amend a commit, remember - to update the log there and then, knowing that the script will - do the rest. - - Example:: - - Series-changes: n - - Guinea pig moved into its cage - - Other changes ending with a blank line - <blank line> - -Commit-changes: n - This tag is like Series-changes, except changes in this changelog will - only appear in the changelog of the commit this tag is in. This is - useful when you want to add notes which may not make sense in the cover - letter. For example, you can have short changes such as "New" or - "Lint". - - Example:: - - Commit-changes: n - - This line will not appear in the cover-letter changelog - <blank line> - -Cover-changes: n - This tag is like Series-changes, except changes in this changelog will - only appear in the cover-letter changelog. This is useful to summarize - changes made with Commit-changes, or to add additional context to - changes. - - Example:: - - Cover-changes: n - - This line will only appear in the cover letter - <blank line> - -Commit-added-in: n - Add a change noting the version this commit was added in. This is - equivalent to:: - - Commit-changes: n - - New - - Cover-changes: n - - <commit subject> - - It is a convenient shorthand for suppressing the '(no changes in vN)' - message. - -Patch-cc / Commit-cc: Their Name <email> - This copies a single patch to another email address. Note that the - Cc: used by git send-email is ignored by patman, but will be - interpreted by git send-email if you use it. - -Series-process-log: sort, uniq - This tells patman to sort and/or uniq the change logs. Changes may be - multiple lines long, as long as each subsequent line of a change begins - with a whitespace character. For example, - - Example:: - - - This change - continues onto the next line - - But this change is separate - - Use 'sort' to sort the entries, and 'uniq' to include only - unique entries. If omitted, no change log processing is done. - Separate each tag with a comma. - -Change-Id: - This tag is used to generate the Message-Id of the emails that - will be sent. When you keep the Change-Id the same you are - asserting that this is a slightly different version (but logically - the same patch) as other patches that have been sent out with the - same Change-Id. The Change-Id tag line is removed from outgoing - patches, unless the `keep_change_id` settings is set to `True`. - -Various other tags are silently removed, like these Chrome OS and -Gerrit tags:: - - BUG=... - TEST=... - Review URL: - Reviewed-on: - Commit-xxxx: (except Commit-notes) - -Exercise for the reader: Try adding some tags to one of your current -patch series and see how the patches turn out. - - -Where Patches Are Sent ----------------------- - -Once the patches are created, patman sends them using git send-email. The -whole series is sent to the recipients in Series-to: and Series-cc. -You can Cc individual patches to other people with the Patch-cc: tag. Tags -in the subject are also picked up to Cc patches. For example, a commit like -this:: - - commit 10212537b85ff9b6e09c82045127522c0f0db981 - Author: Mike Frysinger <[email protected]> - Date: Mon Nov 7 23:18:44 2011 -0500 - - x86: arm: add a git mailrc file for maintainers - - This should make sending out e-mails to the right people easier. - - Patch-cc: sandbox, mikef, ag - Patch-cc: afleming - -will create a patch which is copied to x86, arm, sandbox, mikef, ag and -afleming. - -If you have a cover letter it will get sent to the union of the Patch-cc -lists of all of the other patches. If you want to sent it to additional -people you can add a tag:: - - Cover-letter-cc: <list of addresses> - -These people will get the cover letter even if they are not on the To/Cc -list for any of the patches. - - -Patchwork Integration ---------------------- - -Patman has a very basic integration with Patchwork. If you point patman to -your series on patchwork it can show you what new reviews have appeared since -you sent your series. - -To set this up, add a Series-link tag to one of the commits in your series -(see above). - -Then you can type: - -.. code-block:: bash - - patman status - -and patman will show you each patch and what review tags have been collected, -for example:: - - ... - 21 x86: mtrr: Update the command to use the new mtrr - Reviewed-by: Wolfgang Wallner <[email protected]> - + Reviewed-by: Bin Meng <[email protected]> - 22 x86: mtrr: Restructure so command execution is in - Reviewed-by: Wolfgang Wallner <[email protected]> - + Reviewed-by: Bin Meng <[email protected]> - ... - -This shows that patch 21 and 22 were sent out with one review but have since -attracted another review each. If the series needs changes, you can update -these commits with the new review tag before sending the next version of the -series. - -To automatically pull into these tags into a new branch, use the -d option: - -.. code-block:: bash - - patman status -d mtrr4 - -This will create a new 'mtrr4' branch which is the same as your current branch -but has the new review tags in it. The tags are added in alphabetic order and -are placed immediately after any existing ack/review/test/fixes tags, or at the -end. You can check that this worked with: - -.. code-block:: bash - - patman -b mtrr4 status - -which should show that there are no new responses compared to this new branch. - -There is also a -C option to list the comments received for each patch. - - -Example Work Flow ------------------ - -The basic workflow is to create your commits, add some tags to the top -commit, and type 'patman' to check and send them. - -Here is an example workflow for a series of 4 patches. Let's say you have -these rather contrived patches in the following order in branch us-cmd in -your tree where 'us' means your upstreaming activity (newest to oldest as -output by git log --oneline):: - - 7c7909c wip - 89234f5 Don't include standard parser if hush is used - 8d640a7 mmc: sparc: Stop using builtin_run_command() - 0c859a9 Rename run_command2() to run_command() - a74443f sandbox: Rename run_command() to builtin_run_command() - -The first patch is some test things that enable your code to be compiled, -but that you don't want to submit because there is an existing patch for it -on the list. So you can tell patman to create and check some patches -(skipping the first patch) with: - -.. code-block:: bash - - patman -s1 send -n - -If you want to do all of them including the work-in-progress one, then -(if you are tracking an upstream branch): - -.. code-block:: bash - - patman send -n - -Let's say that patman reports an error in the second patch. Then: - -.. code-block:: bash - - git rebase -i HEAD~6 - # change 'pick' to 'edit' in 89234f5 - # use editor to make code changes - git add -u - git rebase --continue - -Now you have an updated patch series. To check it: - -.. code-block:: bash - - patman -s1 send -n - -Let's say it is now clean and you want to send it. Now you need to set up -the destination. So amend the top commit with: - -.. code-block:: bash - - git commit --amend - -Use your editor to add some tags, so that the whole commit message is:: - - The current run_command() is really only one of the options, with - hush providing the other. It really shouldn't be called directly - in case the hush parser is bring used, so rename this function to - better explain its purpose:: - - Series-to: u-boot - Series-cc: bfin, marex - Series-prefix: RFC - Cover-letter: - Unified command execution in one place - - At present two parsers have similar code to execute commands. Also - cmd_usage() is called all over the place. This series adds a single - function which processes commands called cmd_process(). - END - - Change-Id: Ica71a14c1f0ecb5650f771a32fecb8d2eb9d8a17 - - -You want this to be an RFC and Cc the whole series to the bfin alias and -to Marek. Two of the patches have tags (those are the bits at the front of -the subject that say mmc: sparc: and sandbox:), so 8d640a7 will be Cc'd to -mmc and sparc, and the last one to sandbox. - -Now to send the patches, take off the -n flag: - -.. code-block:: bash - - patman -s1 send - -The patches will be created, shown in your editor, and then sent along with -the cover letter. Note that patman's tags are automatically removed so that -people on the list don't see your secret info. - -Of course patches often attract comments and you need to make some updates. -Let's say one person sent comments and you get an Acked-by: on one patch. -Also, the patch on the list that you were waiting for has been merged, -so you can drop your wip commit. - -Take a look on patchwork and find out the URL of the series. This will be -something like `http://patchwork.ozlabs.org/project/uboot/list/?series=187331` -Add this to a tag in your top commit:: - - Series-links: 187331 - -You can use then patman to collect the Acked-by tag to the correct commit, -creating a new 'version 2' branch for us-cmd: - -.. code-block:: bash - - patman status -d us-cmd2 - git checkout us-cmd2 - -You can look at the comments in Patchwork or with: - -.. code-block:: bash - - patman status -C - -Then you can resync with upstream: - -.. code-block:: bash - - git fetch origin # or whatever upstream is called - git rebase origin/master - -and use git rebase -i to edit the commits, dropping the wip one. - -Then update the `Series-cc:` in the top commit to add the person who reviewed -the v1 series:: - - Series-cc: bfin, marex, Heiko Schocher <[email protected]> - -and remove the Series-prefix: tag since it it isn't an RFC any more. The -series is now version two, so the series info in the top commit looks like -this:: - - Series-to: u-boot - Series-cc: bfin, marex, Heiko Schocher <[email protected]> - Series-version: 2 - Cover-letter: - ... - -Finally, you need to add a change log to the two commits you changed. You -add change logs to each individual commit where the changes happened, like -this:: - - Series-changes: 2 - - Updated the command decoder to reduce code size - - Wound the torque propounder up a little more - -(note the blank line at the end of the list) - -When you run patman it will collect all the change logs from the different -commits and combine them into the cover letter, if you have one. So finally -you have a new series of commits:: - - faeb973 Don't include standard parser if hush is used - 1b2f2fe mmc: sparc: Stop using builtin_run_command() - cfbe330 Rename run_command2() to run_command() - 0682677 sandbox: Rename run_command() to builtin_run_command() - -so to send them: - -.. code-block:: bash - - patman - -and it will create and send the version 2 series. - - -Series Management ------------------ - -Sometimes you might have several series in flight at the same time. Each of -these receives comments and you want to create a new version of each series with -those comments addressed. - -Patman provides a few subcommands which are helpful for managing series. - -Series and branches -~~~~~~~~~~~~~~~~~~~ - -'patman series' works with the concept of a series. It maintains a local -database (.patman.db in your top-level git tree) and uses that to keep track of -series and patches. - -Each series goes through muliple versions. Patman requires that the first -version of your series is in a branch without a numeric suffix. Branch names -like 'serial' and 'video' are OK, but 'part3' is not. This is because Patman -uses the number at the end of the branch name to indicate the version. - -If your series name is 'video', then you can have a 'video' branch for version -1 of the series, 'video2' for version 2 and 'video3' for version 3. All three -branches are for the same series. Patman keeps track of these different -versions. It handles the branch naming automatically, but you need to be aware -of what it is doing. - -You will have an easier time if the branch names you use with 'patman series' -are short, no more than 15 characters. This is the amount of columnar space in -listings. You can add a longer description as the series description. If you -are used to having very descriptive branch names, remember that patman lets you -add metadata into commit which is automatically removed before sending. - -This documentation uses the term 'series' to mean all the versions of a series -and 'series/version' to mean a particular version of a series. - -Updating commits -~~~~~~~~~~~~~~~~ - -Since Patman provides quite a bit of automation, it updates your commits in -some cases, effectively doing a rebase of a branch in order to change the tags -in the commits. It never makes code changes. - -In extremis you can use 'git reflog' to revert something that Patman did. - - -Series subcommands -~~~~~~~~~~~~~~~~~~ - -Note that 'patman series ...' can be abbreviated as 'patman s' or 'patman ser'. - -Here is a short overview of the available subcommands: - - add - Add a new series. Use this on an existing branch to tell Patman about it. - - archive (ar) - Archive a series when you have finished upstreaming it. Archived series - are not shown by most commands. This creates a dated tag for each - version of the series, pointing to the series branch, then deletes the - branches. It puts the tag names in the database so that it can - 'unarchive' to restore things how they were. - - unarchive (unar) - Unarchive a series when you decide you need to do something more with - it. The branches are restored and tags deleted. - - autolink (au) - Search patchwork for the series link for your series, so Patman can - track the status - - autolink-all - Same but for all series - - inc - Increase the series number, effectively creating a new branch with the - next highest version number. The new branch is created based on the - existing branch. So if you use 'patman series inc' on branch 'video2' - it will create branch 'video3' and add v3 into its database - - dec - Decrease the series number, thus deleting the current branch and - removing that version from the data. If you use this comment on branch - 'video3' Patman will delete version 3 and branch 'video3'. - - get-link - Shows the Patchwork link for a series/version - - ls - Lists the series in the database - - mark - Mark a series with 'Change-Id' tags so that Patman can track patches - even when the subject changes. Unmarked patches just use the subject to - decided which is which. - - unmark - Remove 'Change-Id' tags from a series. - - open (o) - Open a series in Patchwork using your web browser - - patches - Show the patches in a particular series/version - - progress (p) - Show upstream progress for your series, or for all series - - rm - Remove a series entirely, including all versions - - rm-version (rmv) - Remove a particular version of a series. This is similar to 'dec' - except that any version can be removed, not just the latest one. - - scan - Scan the local branch and update the database with the set of patches - in that branch. This throws away the old patches. - - send - Send a series out as patches. This is similar to 'patman send' except - that it can send any series, not just the current branch. It also - waits a little for patchwork to see the cover letter, so it can find - out the patchwork link for the series. - - set-link - Sets the Patchwork link for a series-version manually. - - status (st) - Run 'patman status' on a series. This is similar to 'patman status' - except that it can get status on any series, not just the current - branch - - summary - Shows a quick summary of series with their status and description. - - sync - Sync the status of a series with Pathwork, so that - 'patman series progress' can show the right information. - - sync-all - Sync the status of all series. - - -Patman series workflow -~~~~~~~~~~~~~~~~~~~~~~ - -Here is a run-through of how to incorporate 'patman series' into your workflow. - -Firstly, set up your project:: - - patman patchwork set-project U-Boot - -This just tells Patman to look on the Patchwork server for a project of that -name. Internally Patman stores the ID and URL 'link-name' for the project, so it -can access it. - -If you need to use a different patchwork server, use the `--patchwork-url` -option or put the URL in your Patman-settings file. - -Now create a branch. For our example we are going to send out a series related -to video so the branch will be called 'video'. The upstream remove is called -'us':: - - git checkout -b video us/master - -We now have a branch and so we can do some commits:: - - <edit files> - git add ... - <edit files> - git add -u - git commit ... - git commit ... - -We now have a few commits in our 'video' branch. Let's tell patman about it:: - - patman series add - -Like most commands, if no series is given (`patman series -s video add`) then -the current branch is assumed. Since the branch is called 'video' patman knows -that it is version one of the video series. - -You'll likely get a warning that there is no cover letter. Let's add some tags -to the top commit:: - - Series-to: u-boot - Series-cc: ... - Cover-letter: - video: Improve syncing performance with cyclic - -Trying again:: - - patman series add - -You'll likely get a warning that the commits are unmarked. You can either let -patman add Change-Id values itself with the `-m` flag, or tell it not to worry -about it with `-M`. You must choose one or the other. Let's leave the commits -unmarked:: - - patman series add -M - -Congratulations, you've now got a patman database! - -Now let's send out the series. We will add tags to the top commit. - -To send it:: - - patman series send - -You should send 'git send-email' start up and you can confirm the sending of -each email. - -After that, patman waits a bit to see if it can find your new series appearing -on Patchwork. With a bit of luck this will only take 20 seconds or so. Then your -series is linked. - -To gather tags (Reviewed-by ...) for your series from patchwork:: - - patman series gather - -Now you can check your progress:: - - patman series progress - -Later on you get some comments, or perhaps you just decide to make a change on -your own. You have several options. - -The first option is that you can just create a new branch:: - - git checkout -b video2 video - -then you can add this 'v2' series to Patman with:: - - patman series add - -The second option is to get patman to create the new 'video2' branch in one -step:: - - patman inc - -The third option is to collect some tags using the 'patman status' command and -put them in a new branch:: - - patman status -d video2 - -One day the fourth option will be to ask patman to collect tags as part of the -'patman inc' command. - -Again, you do your edits, perhaps adding/removing patches, rebasing on -master -and so on. Then, send your v2:: - - patman series send - -Let's say the patches are accepted. You can use:: - - patch series gather - patch series progress - -to check, or:: - - patman series status -cC - -to see comments. You can now archive the series:: - - patman series archive - -At this point you have the basics. Some of the subcommands useful options, so -be sure to check out the help. - -Here is a sample 'progress' view: - -.. image:: pics/patman.jpg - :width: 800 - :alt: Patman showing the progress view - -General points --------------- - -#. When you change back to the us-cmd branch days or weeks later all your - information is still there, safely stored in the commits. You don't need - to remember what version you are up to, who you sent the last lot of patches - to, or anything about the change logs. -#. If you put tags in the subject, patman will Cc the maintainers - automatically in many cases. -#. If you want to keep the commits from each series you sent so that you can - compare change and see what you did, you can either create a new branch for - each version, or just tag the branch before you start changing it: - - .. code-block:: bash - - git tag sent/us-cmd-rfc - # ...later... - git tag sent/us-cmd-v2 - -#. If you want to modify the patches a little before sending, you can do - this in your editor, but be careful! -#. If you want to run git send-email yourself, use the -n flag which will - print out the command line patman would have used. -#. It is a good idea to add the change log info as you change the commit, - not later when you can't remember which patch you changed. You can always - go back and change or remove logs from commits. -#. Some mailing lists have size limits and when we add binary contents to - our patches it's easy to exceed the size limits. Use "--no-binary" to - generate patches without any binary contents. You are supposed to include - a link to a git repository in your "Commit-notes", "Series-notes" or - "Cover-letter" for maintainers to fetch the original commit. -#. Patches will have no changelog entries for revisions where they did not - change. For clarity, if there are no changes for this patch in the most - recent revision of the series, a note will be added. For example, a patch - with the following tags in the commit:: - - Series-version: 5 - Series-changes: 2 - - Some change - - Series-changes: 4 - - Another change - - would have a changelog of::: - - (no changes since v4) - - Changes in v4: - - Another change - - Changes in v2: - - Some change - - -Other thoughts --------------- - -This script has been split into sensible files but still needs work. -Most of these are indicated by a TODO in the code. - -It would be nice if this could handle the In-reply-to side of things. - -The tests are incomplete, as is customary. Use the 'test' subcommand to run -them: - -.. code-block:: bash - - $ tools/patman/patman test - -Note that since the test suite depends on data files only available in -the git checkout, the `test` command is hidden unless `patman` is -invoked from the U-Boot git repository. - -Alternatively, you can run the test suite via Pytest: - -.. code-block:: bash - - $ cd tools/patman && pytest - -Error handling doesn't always produce friendly error messages - e.g. -putting an incorrect tag in a commit may provide a confusing message. - -There might be a few other features not mentioned in this README. They -might be bugs. In particular, tags are case sensitive which is probably -a bad thing. 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/pyproject.toml b/tools/patman/pyproject.toml deleted file mode 100644 index 0fc4b7d4ecd..00000000000 --- a/tools/patman/pyproject.toml +++ /dev/null @@ -1,29 +0,0 @@ -[build-system] -requires = ["setuptools>=78.1.1"] -build-backend = "setuptools.build_meta" - -[project] -name = "patch-manager" -version = "0.0.6" -authors = [ - { name="Simon Glass", email="[email protected]" }, -] -dependencies = ["u_boot_pylib >= 0.0.6", "aiohttp >= 3.9.1" ] -description = "Patman patch manager" -readme = "README.rst" -requires-python = ">=3.7" -classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", - "Operating System :: OS Independent", -] - -[project.urls] -"Homepage" = "https://docs.u-boot-project.org/en/latest/develop/patman.html" -"Bug Tracker" = "https://source.denx.de/groups/u-boot/-/issues" - -[project.scripts] -patman = "patman.__main__:run_patman" - -[tool.setuptools.package-data] -patman = ["*.rst"] diff --git a/tools/patman/pytest.ini b/tools/patman/pytest.ini deleted file mode 100644 index df3eb518d0f..00000000000 --- a/tools/patman/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -addopts = --doctest-modules diff --git a/tools/patman/requirements.txt b/tools/patman/requirements.txt deleted file mode 100644 index d4fcb1061c2..00000000000 --- a/tools/patman/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -aiohttp==3.10.11 -ConfigParser==7.1.0 -importlib_resources==6.5.2 -pygit2==1.14.1 -requests==2.32.4 -setuptools==78.1.1 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/setup.py b/tools/patman/setup.py deleted file mode 100644 index bcaad69a1c2..00000000000 --- a/tools/patman/setup.py +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0+ - -from setuptools import setup -setup(name='patman', - version='1.0', - scripts=['patman'], - packages=['patman'], - package_dir={'patman': ''}, - package_data={'patman': ['README.rst']}, - classifiers=['Environment :: Console', - 'Topic :: Software Development']) 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) diff --git a/tools/patman/test/0000-cover-letter.patch b/tools/patman/test/0000-cover-letter.patch deleted file mode 100644 index c99e635623f..00000000000 --- a/tools/patman/test/0000-cover-letter.patch +++ /dev/null @@ -1,23 +0,0 @@ -From 5ab48490f03051875ab13d288a4bf32b507d76fd Mon Sep 17 00:00:00 2001 -From: Simon Glass <[email protected]> -Date: Sat, 27 May 2017 20:52:11 -0600 -Subject: [RFC 0/2] *** SUBJECT HERE *** -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -*** BLURB HERE *** - -Simon Glass (2): - pci: Correct cast for sandbox - fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base() - - cmd/pci.c | 3 ++- - fs/fat/fat.c | 1 + - lib/efi_loader/efi_memory.c | 1 + - lib/fdtdec.c | 3 ++- - 4 files changed, 6 insertions(+), 2 deletions(-) - --- -2.7.4 - diff --git a/tools/patman/test/0001-pci-Correct-cast-for-sandbox.patch b/tools/patman/test/0001-pci-Correct-cast-for-sandbox.patch deleted file mode 100644 index 038943c2c9b..00000000000 --- a/tools/patman/test/0001-pci-Correct-cast-for-sandbox.patch +++ /dev/null @@ -1,51 +0,0 @@ -From b9da5f937bd5ea4931ea17459bf79b2905d9594d Mon Sep 17 00:00:00 2001 -From: Simon Glass <[email protected]> -Date: Sat, 15 Apr 2017 15:39:08 -0600 -Subject: [RFC 1/2] pci: Correct cast for sandbox -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -This gives a warning with some native compilers: - -cmd/pci.c:152:11: warning: format ‘%llx’ expects argument of type - ‘long long unsigned int’, but argument 3 has type - ‘u64 {aka long unsigned int}’ [-Wformat=] - -Fix it with a cast. - -Signed-off-by: Simon Glass <[email protected]> -Commit-changes: 2 -- Changes only for this commit - -Series-notes: -some notes -about some things -from the first commit -END - -Commit-notes: -Some notes about -the first commit -END ---- - cmd/pci.c | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) - -diff --git a/cmd/pci.c b/cmd/pci.c -index 41b4fff..fe27b4f 100644 ---- a/cmd/pci.c -+++ b/cmd/pci.c -@@ -150,7 +150,8 @@ int pci_bar_show(struct udevice *dev) - if ((!is_64 && size_low) || (is_64 && size)) { - size = ~size + 1; - printf(" %d %#016llx %#016llx %d %s %s\n", -- bar_id, base, size, is_64 ? 64 : 32, -+ bar_id, (unsigned long long)base, -+ (unsigned long long)size, is_64 ? 64 : 32, - is_io ? "I/O" : "MEM", - prefetchable ? "Prefetchable" : ""); - } --- -2.7.4 - diff --git a/tools/patman/test/0002-fdt-Correct-cast-for-sandbox-in-fdtdec_setup_mem_siz.patch b/tools/patman/test/0002-fdt-Correct-cast-for-sandbox-in-fdtdec_setup_mem_siz.patch deleted file mode 100644 index 48ea1793b47..00000000000 --- a/tools/patman/test/0002-fdt-Correct-cast-for-sandbox-in-fdtdec_setup_mem_siz.patch +++ /dev/null @@ -1,85 +0,0 @@ -From 5ab48490f03051875ab13d288a4bf32b507d76fd Mon Sep 17 00:00:00 2001 -From: Simon Glass <[email protected]> -Date: Sat, 15 Apr 2017 15:39:08 -0600 -Subject: [RFC 2/2] fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base() -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -This gives a warning with some native compilers: - -lib/fdtdec.c:1203:8: warning: format ‘%llx’ expects argument of type - ‘long long unsigned int’, but argument 3 has type - ‘long unsigned int’ [-Wformat=] - -Fix it with a cast. - -Signed-off-by: Simon Glass <[email protected]> -Series-to: u-boot -Series-prefix: RFC -Series-cc: Stefan Brüns <[email protected]> -Cover-letter-cc: Lord Mëlchett <[email protected]> -Series-version: 3 -Patch-cc: fred -Commit-cc: joe -Series-process-log: sort, uniq -Commit-added-in: 4 -Series-changes: 4 -- Some changes -- Multi - line - change - -Commit-changes: 2 -- Changes only for this commit - -Cover-changes: 4 -- Some notes for the cover letter - -Cover-letter: -test: A test patch series -This is a test of how the cover -letter -works -END ---- - fs/fat/fat.c | 1 + - lib/efi_loader/efi_memory.c | 1 + - lib/fdtdec.c | 3 ++- - 3 files changed, 4 insertions(+), 1 deletion(-) - -diff --git a/fs/fat/fat.c b/fs/fat/fat.c -index a71bad1..ba169dc 100644 ---- a/fs/fat/fat.c -+++ b/fs/fat/fat.c -@@ -1,3 +1,4 @@ -+ - /* - * fat.c - * -diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c -index db2ae19..05f75d1 100644 ---- a/lib/efi_loader/efi_memory.c -+++ b/lib/efi_loader/efi_memory.c -@@ -1,3 +1,4 @@ -+ - /* - * EFI application memory management - * -diff --git a/lib/fdtdec.c b/lib/fdtdec.c -index c072e54..942244f 100644 ---- a/lib/fdtdec.c -+++ b/lib/fdtdec.c -@@ -1200,7 +1200,8 @@ int fdtdec_setup_mem_size_base(void) - } - - gd->ram_size = (phys_size_t)(res.end - res.start + 1); -- debug("%s: Initial DRAM size %llx\n", __func__, (u64)gd->ram_size); -+ debug("%s: Initial DRAM size %llx\n", __func__, -+ (unsigned long long)gd->ram_size); - - return 0; - } --- -2.7.4 - diff --git a/tools/patman/test/test01.txt b/tools/patman/test/test01.txt deleted file mode 100644 index b2d73c5972c..00000000000 --- a/tools/patman/test/test01.txt +++ /dev/null @@ -1,72 +0,0 @@ -commit b9da5f937bd5ea4931ea17459bf79b2905d9594d -Author: Simon Glass <[email protected]> -Date: Sat Apr 15 15:39:08 2017 -0600 - - pci: Correct cast for sandbox - - This gives a warning with some native compilers: - - cmd/pci.c:152:11: warning: format ‘%llx’ expects argument of type - ‘long long unsigned int’, but argument 3 has type - ‘u64 {aka long unsigned int}’ [-Wformat=] - - Fix it with a cast. - - Signed-off-by: Simon Glass <[email protected]> - Commit-changes: 2 - - second revision change - - Series-notes: - some notes - about some things - from the first commit - END - - Commit-notes: - Some notes about - the first commit - END - -commit 5ab48490f03051875ab13d288a4bf32b507d76fd -Author: Simon Glass <[email protected]> -Date: Sat Apr 15 15:39:08 2017 -0600 - - fdt: Correct cast for sandbox in fdtdec_setup_mem_size_base() - - This gives a warning with some native compilers: - - lib/fdtdec.c:1203:8: warning: format ‘%llx’ expects argument of type - ‘long long unsigned int’, but argument 3 has type - ‘long unsigned int’ [-Wformat=] - - Fix it with a cast. - - Signed-off-by: Simon Glass <[email protected]> - Series-to: u-boot - Series-prefix: RFC - Series-postfix: some-branch - Series-cc: Stefan Brüns <[email protected]> - Cover-letter-cc: Lord Mëlchett <[email protected]> - Series-version: 3 - Patch-cc: fred - Commit-cc: joe - Series-process-log: sort, uniq - Commit-added-in: 4 - Series-changes: 4 - - Some changes - - Multi - line - change - - Commit-changes: 2 - - Changes only for this commit - - Cover-changes: 4 - - Some notes for the cover letter - - Cover-letter: - test: A test patch series - This is a test of how the cover - letter - works - END diff --git a/tools/patman/test_checkpatch.py b/tools/patman/test_checkpatch.py deleted file mode 100644 index b4722330f86..00000000000 --- a/tools/patman/test_checkpatch.py +++ /dev/null @@ -1,526 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: GPL-2.0+ -# -# Tests for U-Boot-specific checkpatch.pl features -# -# Copyright (c) 2011 The Chromium OS Authors. -# - -import os -import tempfile -import unittest - -from patman import checkpatch -from patman import patchstream -from patman import series -from patman import commit -from u_boot_pylib import gitutil - - -class Line: - """Single changed line in one file in a patch - - Args: - fname (str): Filename containing the added line - text (str): Text of the added line - """ - def __init__(self, fname, text): - self.fname = fname - self.text = text - - -class PatchMaker: - """Makes a patch for checking with checkpatch.pl - - The idea here is to create a patch which adds one line in one file, - intended to provoke a checkpatch error or warning. The base patch is empty - (i.e. invalid), so you should call add_line() to add at least one line. - """ - def __init__(self): - """Set up the PatchMaker object - - Properties: - lines (list of Line): List of lines to add to the patch. Note that - each line has both a file and some text associated with it, - since for simplicity we just add a single line for each file - """ - self.lines = [] - - def add_line(self, fname, text): - """Add to the list of filename/line pairs""" - self.lines.append(Line(fname, text)) - - def get_patch_text(self): - """Build the patch text - - Takes a base patch and adds a diffstat and patch for each filename/line - pair in the list. - - Returns: - str: Patch text ready for submission to checkpatch - """ - base = '''From 125b77450f4c66b8fd9654319520bbe795c9ef31 Mon Sep 17 00:00:00 2001 -From: Simon Glass <[email protected]> -Date: Sun, 14 Jun 2020 09:45:14 -0600 -Subject: [PATCH] Test commit - -This is a test commit. - -Signed-off-by: Simon Glass <[email protected]> ---- - -''' - lines = base.splitlines() - - # Create the diffstat - change = 0 - insert = 0 - for line in self.lines: - lines.append(' %s | 1 +' % line.fname) - change += 1 - insert += 1 - lines.append(' %d files changed, %d insertions(+)' % (change, insert)) - lines.append('') - - # Create the patch info for each file - for line in self.lines: - lines.append('diff --git a/%s b/%s' % (line.fname, line.fname)) - lines.append('index 7837d459f18..5ba7840f68e 100644') - lines.append('--- a/%s' % line.fname) - lines.append('+++ b/%s' % line.fname) - lines += ('''@@ -121,6 +121,7 @@ enum uclass_id { - UCLASS_W1, /* Dallas 1-Wire bus */ - UCLASS_W1_EEPROM, /* one-wire EEPROMs */ - UCLASS_WDT, /* Watchdog Timer driver */ -+%s - - UCLASS_COUNT, - UCLASS_INVALID = -1, -''' % line.text).splitlines() - lines.append('---') - lines.append('2.17.1') - - return '\n'.join(lines) - - def get_patch(self): - """Get the patch text and write it into a temporary file - - Returns: - str: Filename containing the patch - """ - inhandle, inname = tempfile.mkstemp() - infd = os.fdopen(inhandle, 'w') - infd.write(self.get_patch_text()) - infd.close() - return inname - - def run_checkpatch(self): - """Run checkpatch on the patch file - - 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 - """ - return checkpatch.check_patch(self.get_patch(), show_types=True) - - -class TestPatch(unittest.TestCase): - """Test the u_boot_line() function in checkpatch.pl""" - - def test_filter(self): - """Test basic filter operation""" - data=''' - -From 656c9a8c31fa65859d924cd21da920d6ba537fad Mon Sep 17 00:00:00 2001 -From: Simon Glass <[email protected]> -Date: Thu, 28 Apr 2011 09:58:51 -0700 -Subject: [PATCH (resend) 3/7] Tegra2: Add more clock support - -This adds functions to enable/disable clocks and reset to on-chip peripherals. - -cmd/pci.c:152:11: warning: format ‘%llx’ expects argument of type - ‘long long unsigned int’, but argument 3 has type - ‘u64 {aka long unsigned int}’ [-Wformat=] - -BUG=chromium-os:13875 -TEST=build U-Boot for Seaboard, boot - -Change-Id: I80fe1d0c0b7dd10aa58ce5bb1d9290b6664d5413 - -Review URL: http://codereview.chromium.org/6900006 - -Signed-off-by: Simon Glass <[email protected]> ---- - arch/arm/cpu/armv7/tegra2/Makefile | 2 +- - arch/arm/cpu/armv7/tegra2/ap20.c | 57 ++---- - arch/arm/cpu/armv7/tegra2/clock.c | 163 +++++++++++++++++ -''' - expected='''Message-Id: <19991231235959.0.I80fe1d0c0b7dd10aa58ce5bb1d9290b6664d5413@changeid> - - -From 656c9a8c31fa65859d924cd21da920d6ba537fad Mon Sep 17 00:00:00 2001 -From: Simon Glass <[email protected]> -Date: Thu, 28 Apr 2011 09:58:51 -0700 -Subject: [PATCH (resend) 3/7] Tegra2: Add more clock support - -This adds functions to enable/disable clocks and reset to on-chip peripherals. - -cmd/pci.c:152:11: warning: format ‘%llx’ expects argument of type - ‘long long unsigned int’, but argument 3 has type - ‘u64 {aka long unsigned int}’ [-Wformat=] - -Signed-off-by: Simon Glass <[email protected]> ---- - - arch/arm/cpu/armv7/tegra2/Makefile | 2 +- - arch/arm/cpu/armv7/tegra2/ap20.c | 57 ++---- - arch/arm/cpu/armv7/tegra2/clock.c | 163 +++++++++++++++++ -''' - out = '' - inhandle, inname = tempfile.mkstemp() - infd = os.fdopen(inhandle, 'w', encoding='utf-8') - infd.write(data) - infd.close() - - exphandle, expname = tempfile.mkstemp() - expfd = os.fdopen(exphandle, 'w', encoding='utf-8') - expfd.write(expected) - expfd.close() - - # Normally by the time we call fix_patch we've already collected - # metadata. Here, we haven't, but at least fake up something. - # Set the "count" to -1 which tells fix_patch to use a bogus/fixed - # time for generating the Message-Id. - com = commit.Commit('') - com.change_id = 'I80fe1d0c0b7dd10aa58ce5bb1d9290b6664d5413' - com.count = -1 - - patchstream.fix_patch(None, inname, series.Series(), com) - - rc = os.system('diff -u %s %s' % (inname, expname)) - self.assertEqual(rc, 0) - os.remove(inname) - - # Test whether the keep_change_id settings works. - inhandle, inname = tempfile.mkstemp() - infd = os.fdopen(inhandle, 'w', encoding='utf-8') - infd.write(data) - infd.close() - - patchstream.fix_patch(None, inname, series.Series(), com, - keep_change_id=True) - - with open(inname, 'r') as f: - content = f.read() - self.assertIn( - 'Change-Id: I80fe1d0c0b7dd10aa58ce5bb1d9290b6664d5413', - content) - - os.remove(inname) - os.remove(expname) - - def get_data(self, data_type): - data='''From 4924887af52713cabea78420eff03badea8f0035 Mon Sep 17 00:00:00 2001 -From: Simon Glass <[email protected]> -Date: Thu, 7 Apr 2011 10:14:41 -0700 -Subject: [PATCH 1/4] Add microsecond boot time measurement - -This defines the basics of a new boot time measurement feature. This allows -logging of very accurate time measurements as the boot proceeds, by using -an available microsecond counter. - -%s ---- - README | 11 ++++++++ - MAINTAINERS | 3 ++ - common/bootstage.c | 50 ++++++++++++++++++++++++++++++++++++ - include/bootstage.h | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++ - include/common.h | 8 ++++++ - 5 files changed, 141 insertions(+), 0 deletions(-) - create mode 100644 common/bootstage.c - create mode 100644 include/bootstage.h - -diff --git a/README b/README -index 6f3748d..f9e4e65 100644 ---- a/README -+++ b/README -@@ -2026,6 +2026,17 @@ The following options need to be configured: - example, some LED's) on your board. At the moment, - the following checkpoints are implemented: - -+- Time boot progress -+ CONFIG_BOOTSTAGE -+ -+ Define this option to enable microsecond boot stage timing -+ on supported platforms. For this to work your platform -+ needs to define a function timer_get_us() which returns the -+ number of microseconds since reset. This would normally -+ be done in your SOC or board timer.c file. -+ -+ You can add calls to bootstage_mark() to set time markers. -+ - - Standalone program support: - CONFIG_STANDALONE_LOAD_ADDR - -diff --git a/MAINTAINERS b/MAINTAINERS -index b167b028ec..beb7dc634f 100644 ---- a/MAINTAINERS -+++ b/MAINTAINERS -@@ -474,3 +474,8 @@ S: Maintained - T: git git://git.denx.de/u-boot.git - F: * - F: */ -+ -+BOOTSTAGE -+M: Simon Glass <[email protected]> -+L: [email protected] -+F: common/bootstage.c -diff --git a/common/bootstage.c b/common/bootstage.c -new file mode 100644 -index 0000000..2234c87 ---- /dev/null -+++ b/common/bootstage.c -@@ -0,0 +1,37 @@ -+%s -+/* -+ * Copyright (c) 2011, Google Inc. All rights reserved. -+ * -+ */ -+ -+/* -+ * This module records the progress of boot and arbitrary commands, and -+ * permits accurate timestamping of each. The records can optionally be -+ * passed to kernel in the ATAGs -+ */ -+ -+#include <config.h> -+ -+struct bootstage_record { -+ u32 time_us; -+ const char *name; -+}; -+ -+static struct bootstage_record record[BOOTSTAGE_COUNT]; -+ -+u32 bootstage_mark(enum bootstage_id id, const char *name) -+{ -+ struct bootstage_record *rec = &record[id]; -+ -+ /* Only record the first event for each */ -+%sif (!rec->name) { -+ rec->time_us = (u32)timer_get_us(); -+ rec->name = name; -+ } -+ if (!rec->name && -+ %ssomething_else) { -+ rec->time_us = (u32)timer_get_us(); -+ rec->name = name; -+ } -+%sreturn rec->time_us; -+} --- -1.7.3.1 -''' - signoff = 'Signed-off-by: Simon Glass <[email protected]>\n' - license = '// SPDX-License-Identifier: GPL-2.0+' - tab = ' ' - indent = ' ' - if data_type == 'good': - pass - elif data_type == 'no-signoff': - signoff = '' - elif data_type == 'no-license': - license = '' - elif data_type == 'spaces': - tab = ' ' - elif data_type == 'indent': - indent = tab - else: - print('not implemented') - return data % (signoff, license, tab, indent, tab) - - def setup_data(self, data_type): - inhandle, inname = tempfile.mkstemp() - infd = os.fdopen(inhandle, 'w') - data = self.get_data(data_type) - infd.write(data) - infd.close() - return inname - - def test_good(self): - """Test checkpatch operation""" - inf = self.setup_data('good') - result = checkpatch.check_patch(inf) - self.assertEqual(result.ok, True) - self.assertEqual(result.problems, []) - self.assertEqual(result.errors, 0) - self.assertEqual(result.warnings, 0) - self.assertEqual(result.checks, 0) - self.assertEqual(result.lines, 62) - os.remove(inf) - - def test_no_signoff(self): - inf = self.setup_data('no-signoff') - result = checkpatch.check_patch(inf) - self.assertEqual(result.ok, False) - self.assertEqual(len(result.problems), 1) - self.assertEqual(result.errors, 1) - self.assertEqual(result.warnings, 0) - self.assertEqual(result.checks, 0) - self.assertEqual(result.lines, 62) - os.remove(inf) - - def test_no_license(self): - inf = self.setup_data('no-license') - result = checkpatch.check_patch(inf) - self.assertEqual(result.ok, False) - self.assertEqual(len(result.problems), 1) - self.assertEqual(result.errors, 0) - self.assertEqual(result.warnings, 1) - self.assertEqual(result.checks, 0) - self.assertEqual(result.lines, 62) - os.remove(inf) - - def test_spaces(self): - inf = self.setup_data('spaces') - result = checkpatch.check_patch(inf) - self.assertEqual(result.ok, False) - self.assertEqual(len(result.problems), 3) - self.assertEqual(result.errors, 0) - self.assertEqual(result.warnings, 3) - self.assertEqual(result.checks, 0) - self.assertEqual(result.lines, 62) - os.remove(inf) - - def test_indent(self): - inf = self.setup_data('indent') - result = checkpatch.check_patch(inf) - self.assertEqual(result.ok, False) - self.assertEqual(len(result.problems), 1) - self.assertEqual(result.errors, 0) - self.assertEqual(result.warnings, 0) - self.assertEqual(result.checks, 1) - self.assertEqual(result.lines, 62) - os.remove(inf) - - def check_single_message(self, pm, msg, pmtype = 'warning'): - """Helper function to run checkpatch and check the result - - Args: - pm: PatchMaker object to use - msg: Expected message (e.g. 'LIVETREE') - pmtype: Type of problem ('error', 'warning') - """ - result = pm.run_checkpatch() - if pmtype == 'warning': - self.assertEqual(result.warnings, 1) - elif pmtype == 'error': - self.assertEqual(result.errors, 1) - if len(result.problems) != 1: - print(result.problems) - self.assertEqual(len(result.problems), 1) - self.assertIn(msg, result.problems[0]['cptype']) - - def test_uclass(self): - """Test for possible new uclass""" - pm = PatchMaker() - pm.add_line('include/dm/uclass-id.h', 'UCLASS_WIBBLE,') - self.check_single_message(pm, 'NEW_UCLASS') - - def test_livetree(self): - """Test for using the livetree API""" - pm = PatchMaker() - pm.add_line('common/main.c', 'fdtdec_do_something()') - self.check_single_message(pm, 'LIVETREE') - - def test_new_command(self): - """Test for adding a new command""" - pm = PatchMaker() - pm.add_line('common/main.c', 'do_wibble(struct cmd_tbl *cmd_tbl)') - self.check_single_message(pm, 'CMD_TEST') - - def test_prefer_if(self): - """Test for using #ifdef""" - pm = PatchMaker() - pm.add_line('common/main.c', '#ifdef CONFIG_YELLOW') - pm.add_line('common/init.h', '#ifdef CONFIG_YELLOW') - pm.add_line('fred.dtsi', '#ifdef CONFIG_YELLOW') - self.check_single_message(pm, "PREFER_IF") - - def test_command_use_defconfig(self): - """Test for enabling/disabling commands using preprocesor""" - pm = PatchMaker() - pm.add_line('common/main.c', '#undef CONFIG_CMD_WHICH') - self.check_single_message(pm, 'DEFINE_CONFIG_SYM', 'error') - - def test_barred_include_in_hdr(self): - """Test for using a barred include in a header file""" - pm = PatchMaker() - pm.add_line('include/myfile.h', '#include <dm.h>') - self.check_single_message(pm, 'BARRED_INCLUDE_IN_HDR', 'error') - - def test_barred_include_common_h(self): - """Test for adding common.h to a file""" - pm = PatchMaker() - pm.add_line('include/myfile.h', '#include <common.h>') - self.check_single_message(pm, 'BARRED_INCLUDE_COMMON_H', 'error') - - def test_config_is_enabled_config(self): - """Test for accidental CONFIG_IS_ENABLED(CONFIG_*) calls""" - pm = PatchMaker() - pm.add_line('common/main.c', 'if (CONFIG_IS_ENABLED(CONFIG_CLK))') - self.check_single_message(pm, 'CONFIG_IS_ENABLED_CONFIG', 'error') - - def check_struct(self, auto, suffix, warning): - """Check one of the warnings for struct naming - - Args: - auto: Auto variable name, e.g. 'per_child_auto' - suffix: Suffix to expect on member, e.g. '_priv' - warning: Warning name, e.g. 'PRIV_AUTO' - """ - pm = PatchMaker() - pm.add_line('common/main.c', '.%s = sizeof(struct(fred)),' % auto) - pm.add_line('common/main.c', '.%s = sizeof(struct(mary%s)),' % - (auto, suffix)) - self.check_single_message( - pm, warning, "struct 'fred' should have a %s suffix" % suffix) - - def test_dm_driver_auto(self): - """Check for the correct suffix on 'struct driver' auto members""" - self.check_struct('priv_auto', '_priv', 'PRIV_AUTO') - self.check_struct('plat_auto', '_plat', 'PLAT_AUTO') - self.check_struct('per_child_auto', '_priv', 'CHILD_PRIV_AUTO') - self.check_struct('per_child_plat_auto', '_plat', 'CHILD_PLAT_AUTO') - - def test_dm_uclass_auto(self): - """Check for the correct suffix on 'struct uclass' auto members""" - # Some of these are omitted since they match those from struct driver - self.check_struct('per_device_auto', '_priv', 'DEVICE_PRIV_AUTO') - self.check_struct('per_device_plat_auto', '_plat', 'DEVICE_PLAT_AUTO') - - def check_strl(self, func): - """Check one of the checks for strn(cpy|cat)""" - pm = PatchMaker() - pm.add_line('common/main.c', "strn%s(foo, bar, sizeof(foo));" % func) - self.check_single_message(pm, "STRL", - "strl%s is preferred over strn%s because it always produces a nul-terminated string\n" - % (func, func)) - - def test_strl(self): - """Check for uses of strn(cat|cpy)""" - self.check_strl("cat"); - self.check_strl("cpy"); - -if __name__ == "__main__": - unittest.main() diff --git a/tools/patman/test_common.py b/tools/patman/test_common.py deleted file mode 100644 index 7da995dda22..00000000000 --- a/tools/patman/test_common.py +++ /dev/null @@ -1,254 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0+ -# -# Copyright 2025 Simon Glass <[email protected]> -# -"""Functional tests for checking that patman behaves correctly""" - -import os -import shutil -import tempfile - -import pygit2 - -from u_boot_pylib import gitutil -from u_boot_pylib import terminal -from u_boot_pylib import tools -from u_boot_pylib import tout - - -class TestCommon: - """Contains common test functions""" - leb = (b'Lord Edmund Blackadd\xc3\xabr <[email protected]>'. - decode('utf-8')) - - # Fake patchwork project ID for U-Boot - PROJ_ID = 6 - PROJ_LINK_NAME = 'uboot' - SERIES_ID_FIRST_V3 = 31 - SERIES_ID_SECOND_V1 = 456 - SERIES_ID_SECOND_V2 = 457 - TITLE_SECOND = 'Series for my board' - - verbosity = False - preserve_outdirs = False - - @classmethod - def setup_test_args(cls, preserve_indir=False, preserve_outdirs=False, - toolpath=None, verbosity=None, no_capture=False): - """Accept arguments controlling test execution - - Args: - preserve_indir (bool): not used by patman - preserve_outdirs (bool): Preserve the output directories used by - tests. Each test has its own, so this is normally only useful - when running a single test. - toolpath (str): not used by patman - verbosity (int): verbosity to use (0 means tout.INIT, 1 means means - tout.DEBUG) - no_capture (bool): True to output all captured text after capturing - completes - """ - del preserve_indir - cls.preserve_outdirs = preserve_outdirs - cls.toolpath = toolpath - cls.verbosity = verbosity - cls.no_capture = no_capture - - def __init__(self): - super().__init__() - self.repo = None - self.tmpdir = None - self.gitdir = None - - def setUp(self): - """Set up the test temporary dir and git dir""" - self.tmpdir = tempfile.mkdtemp(prefix='patman.') - self.gitdir = os.path.join(self.tmpdir, '.git') - tout.init(tout.DEBUG if self.verbosity else tout.INFO, - allow_colour=False) - - def tearDown(self): - """Delete the temporary dir""" - if self.preserve_outdirs: - print(f'Output dir: {self.tmpdir}') - else: - shutil.rmtree(self.tmpdir) - terminal.set_print_test_mode(False) - - def make_commit_with_file(self, subject, body, fname, text): - """Create a file and add it to the git repo with a new commit - - Args: - subject (str): Subject for the commit - body (str): Body text of the commit - fname (str): Filename of file to create - text (str): Text to put into the file - """ - path = os.path.join(self.tmpdir, fname) - tools.write_file(path, text, binary=False) - index = self.repo.index - index.add(fname) - # pylint doesn't seem to find this - # pylint: disable=E1101 - author = pygit2.Signature('Test user', '[email protected]') - committer = author - tree = index.write_tree() - message = subject + '\n' + body - self.repo.create_commit('HEAD', author, committer, message, tree, - [self.repo.head.target]) - - def make_git_tree(self): - """Make a simple git tree suitable for testing - - It has four branches: - 'base' has two commits: PCI, main - 'first' has base as upstream and two more commits: I2C, SPI - 'second' has base as upstream and three more: video, serial, bootm - 'third4' has second as upstream and four more: usb, main, test, lib - - Returns: - pygit2.Repository: repository - """ - os.environ['GIT_CONFIG_GLOBAL'] = '/dev/null' - os.environ['GIT_CONFIG_SYSTEM'] = '/dev/null' - - repo = pygit2.init_repository(self.gitdir) - self.repo = repo - new_tree = repo.TreeBuilder().write() - - common = ['git', f'--git-dir={self.gitdir}', 'config'] - tools.run(*(common + ['user.name', 'Dummy']), cwd=self.gitdir) - tools.run(*(common + ['user.email', '[email protected]']), - cwd=self.gitdir) - - # pylint doesn't seem to find this - # pylint: disable=E1101 - author = pygit2.Signature('Test user', '[email protected]') - committer = author - _ = repo.create_commit('HEAD', author, committer, 'Created master', - new_tree, []) - - self.make_commit_with_file('Initial commit', ''' -Add a README - -''', 'README', '''This is the README file -describing this project -in very little detail''') - - self.make_commit_with_file('pci: PCI implementation', ''' -Here is a basic PCI implementation - -''', 'pci.c', '''This is a file -it has some contents -and some more things''') - self.make_commit_with_file('main: Main program', ''' -Hello here is the second commit. -''', 'main.c', '''This is the main file -there is very little here -but we can always add more later -if we want to - -Series-to: u-boot -Series-cc: Barry Crump <[email protected]> -''') - base_target = repo.revparse_single('HEAD') - self.make_commit_with_file('i2c: I2C things', ''' -This has some stuff to do with I2C -''', 'i2c.c', '''And this is the file contents -with some I2C-related things in it''') - self.make_commit_with_file('spi: SPI fixes', f''' -SPI needs some fixes -and here they are - -Signed-off-by: {self.leb} - -Series-to: u-boot -Commit-notes: -title of the series -This is the cover letter for the series -with various details -END -''', 'spi.c', '''Some fixes for SPI in this -file to make SPI work -better than before''') - first_target = repo.revparse_single('HEAD') - - target = repo.revparse_single('HEAD~2') - # pylint doesn't seem to find this - # pylint: disable=E1101 - repo.reset(target.oid, pygit2.enums.ResetMode.HARD) - self.make_commit_with_file('video: Some video improvements', ''' -Fix up the video so that -it looks more purple. Purple is -a very nice colour. -''', 'video.c', '''More purple here -Purple and purple -Even more purple -Could not be any more purple''') - self.make_commit_with_file('serial: Add a serial driver', f''' -Here is the serial driver -for my chip. - -Cover-letter: -{self.TITLE_SECOND} -This series implements support -for my glorious board. -END -Series-to: u-boot -Series-links: {self.SERIES_ID_SECOND_V1} -''', 'serial.c', '''The code for the -serial driver is here''') - self.make_commit_with_file('bootm: Make it boot', ''' -This makes my board boot -with a fix to the bootm -command -''', 'bootm.c', '''Fix up the bootm -command to make the code as -complicated as possible''') - second_target = repo.revparse_single('HEAD') - - self.make_commit_with_file('usb: Try out the new DMA feature', ''' -This is just a fix that -ensures that DMA is enabled -''', 'usb-uclass.c', '''Here is the USB -implementation and as you can see it -it very nice''') - self.make_commit_with_file('main: Change to the main program', ''' -Here we adjust the main -program just a little bit -''', 'main.c', '''This is the text of the main program''') - self.make_commit_with_file('test: Check that everything works', ''' -This checks that all the -various things we've been -adding actually work. -''', 'test.c', '''Here is the test code and it seems OK''') - self.make_commit_with_file('lib: Sort out the extra library', ''' -The extra library is currently -broken. Fix it so that we can -use it in various place. -''', 'lib.c', '''Some library code is here -and a little more''') - third_target = repo.revparse_single('HEAD') - - repo.branches.local.create('first', first_target) - repo.config.set_multivar('branch.first.remote', '', '.') - repo.config.set_multivar('branch.first.merge', '', 'refs/heads/base') - - repo.branches.local.create('second', second_target) - repo.config.set_multivar('branch.second.remote', '', '.') - repo.config.set_multivar('branch.second.merge', '', 'refs/heads/base') - - repo.branches.local.create('base', base_target) - - repo.branches.local.create('third4', third_target) - repo.config.set_multivar('branch.third4.remote', '', '.') - repo.config.set_multivar('branch.third4.merge', '', - 'refs/heads/second') - - target = repo.lookup_reference('refs/heads/first') - repo.checkout(target, strategy=pygit2.GIT_CHECKOUT_FORCE) - target = repo.revparse_single('HEAD') - repo.reset(target.oid, pygit2.enums.ResetMode.HARD) - - self.assertFalse(gitutil.check_dirty(self.gitdir, self.tmpdir)) - return repo diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py deleted file mode 100644 index 4c211c8ee89..00000000000 --- a/tools/patman/test_cseries.py +++ /dev/null @@ -1,3684 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0+ - -# Copyright 2025 Simon Glass <[email protected]> -# -"""Functional tests for checking that patman behaves correctly""" - -import asyncio -from datetime import datetime -import os -import re -import unittest -from unittest import mock - -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 tools -from patman import cmdline -from patman import control -from patman import cser_helper -from patman import cseries -from patman.database import Pcommit -from patman import database -from patman import patchstream -from patman.patchwork import Patchwork -from patman.test_common import TestCommon - -HASH_RE = r'[0-9a-f]+' -#pylint: disable=protected-access - -class Namespace: - """Simple namespace for use instead of argparse in tests""" - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - -class TestCseries(unittest.TestCase, TestCommon): - """Test cases for the Cseries class - - In some cases there are tests for both direct Cseries calls and for - accessing the feature via the cmdline. It is possible to do this with mocks - but it is a bit painful to catch all cases that way. The approach here is - to create a check_...() function which yields back to the test routines to - make the call or run the command. The check_...() function typically yields - a Cseries while it is working and False when it is done, allowing the test - to check that everything is finished. - - Some subcommands don't have command tests, if it would be duplicative. Some - tests avoid using the check_...() function and just write the test out - twice, if it would be too confusing to use a coroutine. - - Note the -N flag which sort-of disables capturing of output, although in - fact it is still captured, just output at the end. When debugging the code - you may need to temporarily comment out the 'with terminal.capture()' - parts. - """ - def setUp(self): - TestCommon.setUp(self) - self.autolink_extra = None - self.loop = asyncio.get_event_loop() - self.cser = None - - def tearDown(self): - TestCommon.tearDown(self) - - class _Stage: - def __init__(self, name): - self.name = name - - def __enter__(self): - if not terminal.USE_CAPTURE: - print(f"--- starting '{self.name}'") - - def __exit__(self, exc_type, exc_val, exc_tb): - if not terminal.USE_CAPTURE: - print(f"--- finished '{self.name}'\n") - - def stage(self, name): - """Context manager to count requests across a range of patchwork calls - - Args: - name (str): Stage name - - Return: - _Stage: contect object - - Usage: - with self.stage('name'): - ...do things - - Note that the output only appears if the -N flag is used - """ - return self._Stage(name) - - def assert_finished(self, itr): - """Assert that an iterator is finished - - Args: - itr (iter): Iterator to check - """ - self.assertFalse(list(itr)) - - def test_database_setup(self): - """Check setting up of the series database""" - cser = cseries.Cseries(self.tmpdir) - with terminal.capture() as (_, err): - cser.open_database() - self.assertEqual(f'Creating new database {self.tmpdir}/.patman.db', - err.getvalue().strip()) - res = cser.db.execute("SELECT name FROM series") - self.assertTrue(res) - cser.close_database() - - def get_database(self): - """Open the database and silence the warning output - - Return: - Cseries: Resulting Cseries object - """ - cser = cseries.Cseries(self.tmpdir, terminal.COLOR_NEVER) - with terminal.capture() as _: - cser.open_database() - self.cser = cser - return cser - - def get_cser(self): - """Set up a git tree and database - - Return: - Cseries: object - """ - self.make_git_tree() - return self.get_database() - - def db_close(self): - """Close the database if open""" - if self.cser and self.cser.db.cur: - self.cser.close_database() - return True - return False - - def db_open(self): - """Open the database if closed""" - if self.cser and not self.cser.db.cur: - self.cser.open_database() - - def run_args(self, *argv, expect_ret=0, pwork=None, cser=None): - """Run patman with the given arguments - - Args: - argv (list of str): List of arguments, excluding 'patman' - expect_ret (int): Expected return code, used to check errors - pwork (Patchwork): Patchwork object to use when executing the - command, or None to create one - cser (Cseries): Cseries object to use when executing the command, - or None to create one - """ - was_open = self.db_close() - args = cmdline.parse_args(['-D'] + list(argv), config_fname=False) - exit_code = control.do_patman(args, self.tmpdir, pwork, cser) - self.assertEqual(expect_ret, exit_code) - if was_open: - self.db_open() - - def test_series_add(self): - """Test adding a new cseries""" - cser = self.get_cser() - self.assertFalse(cser.db.series_get_dict()) - - with terminal.capture() as (out, _): - cser.add('first', 'my description', allow_unmarked=True) - lines = out.getvalue().strip().splitlines() - self.assertEqual( - "Adding series 'first' v1: mark False allow_unmarked True", - lines[0]) - self.assertEqual("Added series 'first' v1 (2 commits)", lines[1]) - self.assertEqual(2, len(lines)) - - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - self.assertEqual('first', slist['first'].name) - self.assertEqual('my description', slist['first'].desc) - - svlist = cser.get_ser_ver_list() - self.assertEqual(1, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(1, svlist[0].version) - - pclist = cser.get_pcommit_dict() - self.assertEqual(2, len(pclist)) - self.assertIn(1, pclist) - self.assertEqual( - Pcommit(1, 0, 'i2c: I2C things', 1, None, None, None, None), - pclist[1]) - self.assertEqual( - Pcommit(2, 1, 'spi: SPI fixes', 1, None, None, None, None), - pclist[2]) - - def test_series_not_checked_out(self): - """Test adding a new cseries when a different one is checked out""" - cser = self.get_cser() - self.assertFalse(cser.db.series_get_dict()) - - with terminal.capture() as (out, _): - cser.add('second', allow_unmarked=True) - lines = out.getvalue().strip().splitlines() - self.assertEqual( - "Adding series 'second' v1: mark False allow_unmarked True", - lines[0]) - self.assertEqual("Added series 'second' v1 (3 commits)", lines[1]) - self.assertEqual(2, len(lines)) - - def test_series_add_manual(self): - """Test adding a new cseries with a version number""" - cser = self.get_cser() - self.assertFalse(cser.db.series_get_dict()) - - repo = pygit2.init_repository(self.gitdir) - first_target = repo.revparse_single('first') - repo.branches.local.create('first2', first_target) - repo.config.set_multivar('branch.first2.remote', '', '.') - repo.config.set_multivar('branch.first2.merge', '', 'refs/heads/base') - - with terminal.capture() as (out, _): - cser.add('first2', 'description', allow_unmarked=True) - lines = out.getvalue().splitlines() - self.assertEqual( - "Adding series 'first' v2: mark False allow_unmarked True", - lines[0]) - self.assertEqual("Added series 'first' v2 (2 commits)", lines[1]) - self.assertEqual(2, len(lines)) - - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - self.assertEqual('first', slist['first'].name) - - # We should have just one entry, with version 2 - svlist = cser.get_ser_ver_list() - self.assertEqual(1, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(2, svlist[0].version) - - def add_first2(self, checkout): - """Add a new first2 branch, a copy of first""" - repo = pygit2.init_repository(self.gitdir) - first_target = repo.revparse_single('first') - repo.branches.local.create('first2', first_target) - repo.config.set_multivar('branch.first2.remote', '', '.') - repo.config.set_multivar('branch.first2.merge', '', 'refs/heads/base') - - if checkout: - target = repo.lookup_reference('refs/heads/first2') - repo.checkout(target, strategy=pygit2.enums.CheckoutStrategy.FORCE) - - def test_series_add_different(self): - """Test adding a different version of a series from that checked out""" - cser = self.get_cser() - - self.add_first2(True) - - # Add first2 initially - with terminal.capture() as (out, _): - cser.add(None, 'description', allow_unmarked=True) - lines = out.getvalue().splitlines() - self.assertEqual( - "Adding series 'first' v2: mark False allow_unmarked True", - lines[0]) - self.assertEqual("Added series 'first' v2 (2 commits)", lines[1]) - self.assertEqual(2, len(lines)) - - # Now add first: it should be added as a new version - with terminal.capture() as (out, _): - cser.add('first', 'description', allow_unmarked=True) - lines = out.getvalue().splitlines() - self.assertEqual( - "Adding series 'first' v1: mark False allow_unmarked True", - lines[0]) - self.assertEqual( - "Added v1 to existing series 'first' (2 commits)", lines[1]) - self.assertEqual(2, len(lines)) - - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - self.assertEqual('first', slist['first'].name) - - # We should have two entries, one of each version - svlist = cser.get_ser_ver_list() - self.assertEqual(2, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(2, svlist[0].version) - - self.assertEqual(2, svlist[1].idnum) - self.assertEqual(1, svlist[1].series_id) - self.assertEqual(1, svlist[1].version) - - def test_series_add_dup(self): - """Test adding a series twice""" - cser = self.get_cser() - with terminal.capture() as (out, _): - cser.add(None, 'description', allow_unmarked=True) - - with terminal.capture() as (out, _): - cser.add(None, 'description', allow_unmarked=True) - self.assertIn("Series 'first' v1 already exists", - out.getvalue().strip()) - - self.add_first2(False) - - with terminal.capture() as (out, _): - cser.add('first2', 'description', allow_unmarked=True) - lines = out.getvalue().splitlines() - self.assertEqual( - "Added v2 to existing series 'first' (2 commits)", lines[1]) - - def test_series_add_dup_reverse(self): - """Test adding a series twice, v2 then v1""" - cser = self.get_cser() - self.add_first2(True) - with terminal.capture() as (out, _): - cser.add(None, 'description', allow_unmarked=True) - self.assertIn("Added series 'first' v2", out.getvalue().strip()) - - with terminal.capture() as (out, _): - cser.add('first', 'description', allow_unmarked=True) - self.assertIn("Added v1 to existing series 'first'", - out.getvalue().strip()) - - def test_series_add_dup_reverse_cmdline(self): - """Test adding a series twice, v2 then v1""" - cser = self.get_cser() - self.add_first2(True) - with terminal.capture() as (out, _): - self.run_args('series', 'add', '-M', '-D', 'description', - pwork=True) - self.assertIn("Added series 'first' v2 (2 commits)", - out.getvalue().strip()) - - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first', 'add', '-M', - '-D', 'description', pwork=True) - cser.add('first', 'description', allow_unmarked=True) - self.assertIn("Added v1 to existing series 'first'", - out.getvalue().strip()) - - def test_series_add_skip_version(self): - """Test adding a series which is v4 but has no earlier version""" - cser = self.get_cser() - with terminal.capture() as (out, _): - cser.add('third4', 'The glorious third series', mark=False, - allow_unmarked=True) - lines = out.getvalue().splitlines() - self.assertEqual( - "Adding series 'third' v4: mark False allow_unmarked True", - lines[0]) - self.assertEqual("Added series 'third' v4 (4 commits)", lines[1]) - self.assertEqual(2, len(lines)) - - sdict = cser.db.series_get_dict() - self.assertIn('third', sdict) - chk = sdict['third'] - self.assertEqual('third', chk['name']) - self.assertEqual('The glorious third series', chk['desc']) - - svid = cser.get_series_svid(chk['idnum'], 4) - self.assertEqual(4, len(cser.get_pcommit_dict(svid))) - - # Remove the series and add it again with just two commits - with terminal.capture(): - cser.remove('third4') - - with terminal.capture() as (out, _): - cser.add('third4', 'The glorious third series', mark=False, - allow_unmarked=True, end='third4~2') - lines = out.getvalue().splitlines() - self.assertEqual( - "Adding series 'third' v4: mark False allow_unmarked True", - lines[0]) - self.assertRegex( - lines[1], - 'Ending before .* main: Change to the main program') - self.assertEqual("Added series 'third' v4 (2 commits)", lines[2]) - - sdict = cser.db.series_get_dict() - self.assertIn('third', sdict) - chk = sdict['third'] - self.assertEqual('third', chk['name']) - self.assertEqual('The glorious third series', chk['desc']) - - svid = cser.get_series_svid(chk['idnum'], 4) - self.assertEqual(2, len(cser.get_pcommit_dict(svid))) - - def test_series_add_wrong_version(self): - """Test adding a series with an incorrect branch name or version - - This updates branch 'first' to have version 2, then tries to add it. - """ - cser = self.get_cser() - self.assertFalse(cser.db.series_get_dict()) - - with terminal.capture(): - _, ser, max_vers, _ = cser.prep_series('first') - cser.update_series('first', ser, max_vers, None, False, - add_vers=2) - - with self.assertRaises(ValueError) as exc: - with terminal.capture(): - cser.add('first', 'my description', allow_unmarked=True) - self.assertEqual( - "Series name 'first' suggests version 1 but Series-version tag " - 'indicates 2 (see --force-version)', str(exc.exception)) - - # Now try again with --force-version which should force version 1 - with terminal.capture() as (out, _): - cser.add('first', 'my description', allow_unmarked=True, - force_version=True) - itr = iter(out.getvalue().splitlines()) - self.assertEqual( - "Adding series 'first' v1: mark False allow_unmarked True", - next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual( - "Processing 2 commits from branch 'first'", next(itr)) - self.assertRegex(next(itr), - f'- {HASH_RE} as {HASH_RE} i2c: I2C things') - self.assertRegex(next(itr), - f'- rm v1: {HASH_RE} as {HASH_RE} spi: SPI fixes') - self.assertRegex(next(itr), - f'Updating branch first from {HASH_RE} to {HASH_RE}') - self.assertEqual("Added series 'first' v1 (2 commits)", next(itr)) - try: - self.assertEqual('extra line', next(itr)) - except StopIteration: - pass - - # Since this is v1 the Series-version tag should have been removed - series = patchstream.get_metadata('first', 0, 2, git_dir=self.gitdir) - self.assertNotIn('version', series) - - def _fake_patchwork_cser(self, subpath): - """Fake Patchwork server for the function below - - This handles accessing various things used by the tests below. It has - hard-coded data, about from self.autolink_extra which can be adjusted - by the test. - - Args: - subpath (str): URL subpath to use - """ - # Get a list of projects - if subpath == 'projects/': - return [ - {'id': self.PROJ_ID, 'name': 'U-Boot', - 'link_name': self.PROJ_LINK_NAME}, - {'id': 9, 'name': 'other', 'link_name': 'other'} - ] - - # Search for series by their cover-letter name - re_search = re.match(r'series/\?project=(\d+)&q=.*$', subpath) - if re_search: - result = [ - {'id': 56, 'name': 'contains first name', 'version': 1}, - {'id': 43, 'name': 'has first in it', 'version': 1}, - {'id': 1234, 'name': 'first series', 'version': 1}, - {'id': self.SERIES_ID_SECOND_V1, 'name': self.TITLE_SECOND, - 'version': 1}, - {'id': self.SERIES_ID_SECOND_V2, 'name': self.TITLE_SECOND, - 'version': 2}, - {'id': 12345, 'name': 'i2c: I2C things', 'version': 1}, - ] - if self.autolink_extra: - result += [self.autolink_extra] - return result - - # Read information about a series, given its link (patchwork series ID) - m_series = re.match(r'series/(\d+)/$', subpath) - series_id = int(m_series.group(1)) if m_series else '' - if series_id: - if series_id == self.SERIES_ID_SECOND_V1: - # series 'second' - return { - 'patches': [ - {'id': '10', - 'name': '[PATCH,1/3] video: Some video improvements', - 'content': ''}, - {'id': '11', - 'name': '[PATCH,2/3] serial: Add a serial driver', - 'content': ''}, - {'id': '12', 'name': '[PATCH,3/3] bootm: Make it boot', - 'content': ''}, - ], - 'cover_letter': { - 'id': 39, - 'name': 'The name of the cover letter', - } - } - if series_id == self.SERIES_ID_SECOND_V2: - # series 'second2' - return { - 'patches': [ - {'id': '110', - 'name': - '[PATCH,v2,1/3] video: Some video improvements', - 'content': ''}, - {'id': '111', - 'name': '[PATCH,v2,2/3] serial: Add a serial driver', - 'content': ''}, - {'id': '112', - 'name': '[PATCH,v2,3/3] bootm: Make it boot', - 'content': ''}, - ], - 'cover_letter': { - 'id': 139, - 'name': 'The name of the cover letter', - } - } - if series_id == self.SERIES_ID_FIRST_V3: - # series 'first3' - return { - 'patches': [ - {'id': 20, 'name': '[PATCH,v3,1/2] i2c: I2C things', - 'content': ''}, - {'id': 21, 'name': '[PATCH,v3,2/2] spi: SPI fixes', - 'content': ''}, - ], - 'cover_letter': { - 'id': 29, - 'name': 'Cover letter for first', - } - } - if series_id == 123: - return { - 'patches': [ - {'id': 20, 'name': '[PATCH,1/2] i2c: I2C things', - 'content': ''}, - {'id': 21, 'name': '[PATCH,2/2] spi: SPI fixes', - 'content': ''}, - ], - } - if series_id == 1234: - return { - 'patches': [ - {'id': 20, 'name': '[PATCH,v2,1/2] i2c: I2C things', - 'content': ''}, - {'id': 21, 'name': '[PATCH,v2,2/2] spi: SPI fixes', - 'content': ''}, - ], - } - raise ValueError(f'Fake Patchwork unknown series_id: {series_id}') - - # Read patch status - m_pat = re.search(r'patches/(\d*)/$', subpath) - patch_id = int(m_pat.group(1)) if m_pat else '' - if patch_id: - if patch_id in [10, 110]: - return {'state': 'accepted', - 'content': - 'Reviewed-by: Fred Bloggs <[email protected]>'} - if patch_id in [11, 111]: - return {'state': 'changes-requested', 'content': ''} - if patch_id in [12, 112]: - return {'state': 'rejected', - 'content': "I don't like this at all, sorry"} - if patch_id == 20: - return {'state': 'awaiting-upstream', 'content': ''} - if patch_id == 21: - return {'state': 'not-applicable', 'content': ''} - raise ValueError(f'Fake Patchwork unknown patch_id: {patch_id}') - - # Read comments a from patch - m_comm = re.search(r'patches/(\d*)/comments/', subpath) - patch_id = int(m_comm.group(1)) if m_comm else '' - if patch_id: - if patch_id in [10, 110]: - return [ - {'id': 1, 'content': ''}, - {'id': 2, - 'content': - '''On some date Mary Smith <[email protected]> wrote: -> This was my original patch -> which is being quoted - -I like the approach here and I would love to see more of it. - -Reviewed-by: Fred Bloggs <[email protected]> -''', - 'submitter': { - 'name': 'Fred Bloggs', - 'email': '[email protected]', - } - }, - ] - if patch_id in [11, 111]: - return [] - if patch_id in [12, 112]: - return [ - {'id': 4, 'content': ''}, - {'id': 5, 'content': ''}, - {'id': 6, 'content': ''}, - ] - if patch_id == 20: - return [ - {'id': 7, 'content': - '''On some date Alex Miller <[email protected]> wrote: - -> Sometimes we need to create a patch. -> This is one of those times - -Tested-by: Mary Smith <[email protected]> # yak -'''}, - {'id': 8, 'content': ''}, - ] - if patch_id == 21: - return [] - raise ValueError( - f'Fake Patchwork does not understand patch_id {patch_id}: ' - f'{subpath}') - - # Read comments from a cover letter - m_cover_id = re.search(r'covers/(\d*)/comments/', subpath) - cover_id = int(m_cover_id.group(1)) if m_cover_id else '' - if cover_id: - if cover_id in [39, 139]: - return [ - {'content': 'some comment', - 'submitter': { - 'name': 'A user', - 'email': '[email protected]', - }, - 'date': 'Sun 13 Apr 14:06:02 MDT 2025', - }, - {'content': 'another comment', - 'submitter': { - 'name': 'Ghenkis Khan', - 'email': '[email protected]', - }, - 'date': 'Sun 13 Apr 13:06:02 MDT 2025', - }, - ] - if cover_id == 29: - return [] - - raise ValueError(f'Fake Patchwork unknown cover_id: {cover_id}') - - raise ValueError(f'Fake Patchwork does not understand: {subpath}') - - def setup_second(self, do_sync=True): - """Set up the 'second' series synced with the fake patchwork - - Args: - do_sync (bool): True to sync the series - - Return: tuple: - Cseries: New Cseries object - pwork: Patchwork object - """ - with self.stage('setup second'): - cser = self.get_cser() - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - pwork.project_set(self.PROJ_ID, self.PROJ_LINK_NAME) - - with terminal.capture() as (out, _): - cser.add('first', '', allow_unmarked=True) - cser.add('second', allow_unmarked=True) - - series = patchstream.get_metadata_for_list('second', self.gitdir, - 3) - self.assertEqual('456', series.links) - - with terminal.capture() as (out, _): - cser.increment('second') - - series = patchstream.get_metadata_for_list('second', self.gitdir, - 3) - self.assertEqual('456', series.links) - - series = patchstream.get_metadata_for_list('second2', self.gitdir, - 3) - self.assertEqual('1:456', series.links) - - if do_sync: - with terminal.capture() as (out, _): - cser.link_auto(pwork, 'second', 2, True) - with terminal.capture() as (out, _): - cser.gather(pwork, 'second', 2, False, True, False) - lines = out.getvalue().splitlines() - self.assertEqual( - "Updating series 'second' version 2 from link '457'", - lines[0]) - self.assertEqual( - '3 patches and cover letter updated (8 requests)', - lines[1]) - self.assertEqual(2, len(lines)) - - return cser, pwork - - def test_series_add_no_cover(self): - """Test patchwork when adding a series which has no cover letter""" - cser = self.get_cser() - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - pwork.project_set(self.PROJ_ID, self.PROJ_LINK_NAME) - - with terminal.capture() as (out, _): - cser.add('first', 'my name for this', mark=False, - allow_unmarked=True) - self.assertIn("Added series 'first' v1 (2 commits)", out.getvalue()) - - with terminal.capture() as (out, _): - cser.link_auto(pwork, 'first', 1, True) - self.assertIn("Setting link for series 'first' v1 to 12345", - out.getvalue()) - - def test_series_list(self): - """Test listing cseries""" - self.setup_second() - - self.db_close() - args = Namespace(subcmd='ls') - with terminal.capture() as (out, _): - control.do_series(args, test_db=self.tmpdir, pwork=True) - lines = out.getvalue().splitlines() - self.assertEqual(5, len(lines)) - self.assertEqual( - 'Name Description ' - 'Accepted Versions', lines[0]) - self.assertTrue(lines[1].startswith('--')) - self.assertEqual( - 'first ' - ' -/2 1', lines[2]) - self.assertEqual( - 'second Series for my board ' - ' 1/3 1 2', lines[3]) - self.assertTrue(lines[4].startswith('--')) - - def test_do_series_add(self): - """Add a new cseries""" - self.make_git_tree() - args = Namespace(subcmd='add', desc='my-description', series='first', - mark=False, allow_unmarked=True, upstream=None, - dry_run=False) - with terminal.capture() as (out, _): - control.do_series(args, test_db=self.tmpdir, pwork=True) - - cser = self.get_database() - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - ser = slist.get('first') - self.assertTrue(ser) - self.assertEqual('first', ser.name) - self.assertEqual('my-description', ser.desc) - - self.db_close() - args.subcmd = 'ls' - with terminal.capture() as (out, _): - control.do_series(args, test_db=self.tmpdir, pwork=True) - lines = out.getvalue().splitlines() - self.assertEqual(4, len(lines)) - self.assertTrue(lines[1].startswith('--')) - self.assertEqual( - 'first my-description ' - '-/2 1', lines[2]) - - def test_do_series_add_cmdline(self): - """Add a new cseries using the cmdline""" - self.make_git_tree() - with terminal.capture(): - self.run_args('series', '-s', 'first', 'add', '-M', - '-D', 'my-description', pwork=True) - - cser = self.get_database() - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - ser = slist.get('first') - self.assertTrue(ser) - self.assertEqual('first', ser.name) - self.assertEqual('my-description', ser.desc) - - def test_do_series_add_auto(self): - """Add a new cseries without any arguments""" - self.make_git_tree() - - # Use the 'second' branch, which has a cover letter - gitutil.checkout('second', self.gitdir, work_tree=self.tmpdir, - force=True) - args = Namespace(subcmd='add', series=None, mark=False, - allow_unmarked=True, upstream=None, dry_run=False, - desc=None) - with terminal.capture(): - control.do_series(args, test_db=self.tmpdir, pwork=True) - - cser = self.get_database() - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - ser = slist.get('second') - self.assertTrue(ser) - self.assertEqual('second', ser.name) - self.assertEqual('Series for my board', ser.desc) - cser.close_database() - - def _check_inc(self, out): - """Check output from an 'increment' operation - - Args: - out (StringIO): Text to check - """ - itr = iter(out.getvalue().splitlines()) - - self.assertEqual("Increment 'first' v1: 2 patches", next(itr)) - self.assertRegex(next(itr), 'Checking out upstream commit .*') - self.assertEqual("Processing 2 commits from branch 'first2'", - next(itr)) - self.assertRegex(next(itr), - f'- {HASH_RE} as {HASH_RE} i2c: I2C things') - self.assertRegex(next(itr), - f'- add v2: {HASH_RE} as {HASH_RE} spi: SPI fixes') - self.assertRegex( - next(itr), f'Updating branch first2 from {HASH_RE} to {HASH_RE}') - self.assertEqual('Added new branch first2', next(itr)) - return itr - - def test_series_link(self): - """Test adding a patchwork link to a cseries""" - cser = self.get_cser() - - repo = pygit2.init_repository(self.gitdir) - first = repo.lookup_branch('first').peel( - pygit2.enums.ObjectType.COMMIT).oid - base = repo.lookup_branch('base').peel( - pygit2.enums.ObjectType.COMMIT).oid - - gitutil.checkout('first', self.gitdir, work_tree=self.tmpdir, - force=True) - - with terminal.capture() as (out, _): - cser.add('first', '', allow_unmarked=True) - - with self.assertRaises(ValueError) as exc: - cser.link_set('first', 2, '1234', True) - self.assertEqual("Series 'first' does not have a version 2", - str(exc.exception)) - - self.assertEqual('first', gitutil.get_branch(self.gitdir)) - with terminal.capture() as (out, _): - cser.increment('first') - self.assertTrue(repo.lookup_branch('first2')) - - with terminal.capture() as (out, _): - cser.link_set('first', 2, '2345', True) - - lines = out.getvalue().splitlines() - self.assertEqual(6, len(lines)) - self.assertRegex( - lines[0], 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual("Processing 2 commits from branch 'first2'", - lines[1]) - self.assertRegex( - lines[2], - f'- {HASH_RE} as {HASH_RE} i2c: I2C things') - self.assertRegex( - lines[3], - f"- add v2 links '2:2345': {HASH_RE} as {HASH_RE} spi: SPI fixes") - self.assertRegex( - lines[4], f'Updating branch first2 from {HASH_RE} to {HASH_RE}') - self.assertEqual("Setting link for series 'first' v2 to 2345", - lines[5]) - - self.assertEqual('2345', cser.link_get('first', 2)) - - series = patchstream.get_metadata_for_list('first2', self.gitdir, 2) - self.assertEqual('2:2345', series.links) - - self.assertEqual('first2', gitutil.get_branch(self.gitdir)) - - # Check the original series was left alone - self.assertEqual( - first, repo.lookup_branch('first').peel( - pygit2.enums.ObjectType.COMMIT).oid) - count = 2 - series1 = patchstream.get_metadata_for_list('first', self.gitdir, - count) - self.assertFalse('links' in series1) - self.assertFalse('version' in series1) - - # Check that base is left alone - self.assertEqual( - base, repo.lookup_branch('base').peel( - pygit2.enums.ObjectType.COMMIT).oid) - series1 = patchstream.get_metadata_for_list('base', self.gitdir, count) - self.assertFalse('links' in series1) - self.assertFalse('version' in series1) - - # Check out second and try to update first - gitutil.checkout('second', self.gitdir, work_tree=self.tmpdir, - force=True) - with terminal.capture(): - cser.link_set('first', 1, '16', True) - - # Overwrite the link - with terminal.capture(): - cser.link_set('first', 1, '17', True) - - series2 = patchstream.get_metadata_for_list('first', self.gitdir, - count) - self.assertEqual('1:17', series2.links) - - def test_series_link_cmdline(self): - """Test adding a patchwork link to a cseries using the cmdline""" - cser = self.get_cser() - - gitutil.checkout('first', self.gitdir, work_tree=self.tmpdir, - force=True) - - with terminal.capture() as (out, _): - cser.add('first', '', allow_unmarked=True) - - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first', '-V', '4', 'set-link', '-u', - '1234', expect_ret=1, pwork=True) - self.assertIn("Series 'first' does not have a version 4", - out.getvalue()) - - with self.assertRaises(ValueError) as exc: - cser.link_get('first', 4) - self.assertEqual("Series 'first' does not have a version 4", - str(exc.exception)) - - with terminal.capture() as (out, _): - cser.increment('first') - - with self.assertRaises(ValueError) as exc: - cser.link_get('first', 4) - self.assertEqual("Series 'first' does not have a version 4", - str(exc.exception)) - - with terminal.capture() as (out, _): - cser.increment('first') - cser.increment('first') - - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first', '-V', '4', 'set-link', '-u', - '1234', pwork=True) - lines = out.getvalue().splitlines() - self.assertRegex( - lines[-3], - f"- add v4 links '4:1234': {HASH_RE} as {HASH_RE} spi: SPI fixes") - self.assertEqual("Setting link for series 'first' v4 to 1234", - lines[-1]) - - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first', '-V', '4', 'get-link', - pwork=True) - self.assertIn('1234', out.getvalue()) - - series = patchstream.get_metadata_for_list('first4', self.gitdir, 1) - self.assertEqual('4:1234', series.links) - - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first', '-V', '5', 'get-link', - expect_ret=1, pwork=True) - - self.assertIn("Series 'first' does not have a version 5", - out.getvalue()) - - # Checkout 'first' and try to get the link from 'first4' - gitutil.checkout('first', self.gitdir, work_tree=self.tmpdir, - force=True) - - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first4', 'get-link', pwork=True) - self.assertIn('1234', out.getvalue()) - - # This should get the link for 'first' - with terminal.capture() as (out, _): - self.run_args('series', 'get-link', pwork=True) - self.assertIn('None', out.getvalue()) - - # Checkout 'first4' again; this should get the link for 'first4' - gitutil.checkout('first4', self.gitdir, work_tree=self.tmpdir, - force=True) - - with terminal.capture() as (out, _): - self.run_args('series', 'get-link', pwork=True) - self.assertIn('1234', out.getvalue()) - - def test_series_link_auto_version(self): - """Test finding the patchwork link for a cseries automatically""" - cser = self.get_cser() - - with terminal.capture() as (out, _): - cser.add('second', allow_unmarked=True) - - # Make sure that the link is there - count = 3 - series = patchstream.get_metadata('second', 0, count, - git_dir=self.gitdir) - self.assertEqual(f'{self.SERIES_ID_SECOND_V1}', series.links) - - # Set link with detected version - with terminal.capture() as (out, _): - cser.link_set('second', None, f'{self.SERIES_ID_SECOND_V1}', True) - self.assertEqual( - "Setting link for series 'second' v1 to 456", - out.getvalue().splitlines()[-1]) - - # Make sure that the link was set - series = patchstream.get_metadata('second', 0, count, - git_dir=self.gitdir) - self.assertEqual(f'1:{self.SERIES_ID_SECOND_V1}', series.links) - - with terminal.capture(): - cser.increment('second') - - # Make sure that the new series gets the same link - series = patchstream.get_metadata('second2', 0, 3, - git_dir=self.gitdir) - - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - pwork.project_set(self.PROJ_ID, self.PROJ_LINK_NAME) - self.assertFalse(cser.project_get()) - cser.project_set(pwork, 'U-Boot', quiet=True) - - self.assertEqual( - (self.SERIES_ID_SECOND_V1, None, 'second', 1, - 'Series for my board'), - cser.link_search(pwork, 'second', 1)) - - with terminal.capture(): - cser.increment('second') - - self.assertEqual((457, None, 'second', 2, 'Series for my board'), - cser.link_search(pwork, 'second', 2)) - - def test_series_link_auto_name(self): - """Test finding the patchwork link for a cseries with auto name""" - cser = self.get_cser() - - with terminal.capture() as (out, _): - cser.add('first', '', allow_unmarked=True) - - # Set link with detected name - with self.assertRaises(ValueError) as exc: - cser.link_set(None, 2, '2345', True) - self.assertEqual( - "Series 'first' does not have a version 2", str(exc.exception)) - - with terminal.capture(): - cser.increment('first') - - with terminal.capture() as (out, _): - cser.link_set(None, 2, '2345', True) - self.assertEqual( - "Setting link for series 'first' v2 to 2345", - out.getvalue().splitlines()[-1]) - - svlist = cser.get_ser_ver_list() - self.assertEqual(2, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(1, svlist[0].version) - self.assertIsNone(svlist[0].link) - - self.assertEqual(2, svlist[1].idnum) - self.assertEqual(1, svlist[1].series_id) - self.assertEqual(2, svlist[1].version) - self.assertEqual('2345', svlist[1].link) - - def test_series_link_auto_name_version(self): - """Find patchwork link for a cseries with auto name + version""" - cser = self.get_cser() - - with terminal.capture() as (out, _): - cser.add('first', '', allow_unmarked=True) - - # Set link with detected name and version - with terminal.capture() as (out, _): - cser.link_set(None, None, '1234', True) - self.assertEqual( - "Setting link for series 'first' v1 to 1234", - out.getvalue().splitlines()[-1]) - - with terminal.capture(): - cser.increment('first') - - with terminal.capture() as (out, _): - cser.link_set(None, None, '2345', True) - self.assertEqual( - "Setting link for series 'first' v2 to 2345", - out.getvalue().splitlines()[-1]) - - svlist = cser.get_ser_ver_list() - self.assertEqual(2, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(1, svlist[0].version) - self.assertEqual('1234', svlist[0].link) - - self.assertEqual(2, svlist[1].idnum) - self.assertEqual(1, svlist[1].series_id) - self.assertEqual(2, svlist[1].version) - self.assertEqual('2345', svlist[1].link) - - def test_series_link_missing(self): - """Test finding patchwork link for a cseries but it is missing""" - cser = self.get_cser() - - with terminal.capture(): - cser.add('second', allow_unmarked=True) - - with terminal.capture(): - cser.increment('second') - cser.increment('second') - - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - pwork.project_set(self.PROJ_ID, self.PROJ_LINK_NAME) - self.assertFalse(cser.project_get()) - cser.project_set(pwork, 'U-Boot', quiet=True) - - self.assertEqual( - (self.SERIES_ID_SECOND_V1, None, 'second', 1, - 'Series for my board'), - cser.link_search(pwork, 'second', 1)) - self.assertEqual((457, None, 'second', 2, 'Series for my board'), - cser.link_search(pwork, 'second', 2)) - res = cser.link_search(pwork, 'second', 3) - self.assertEqual( - (None, - [{'id': self.SERIES_ID_SECOND_V1, 'name': 'Series for my board', - 'version': 1}, - {'id': 457, 'name': 'Series for my board', 'version': 2}], - 'second', 3, 'Series for my board'), - res) - - def check_series_autolink(self): - """Common code for autolink tests""" - cser = self.get_cser() - - with self.stage('setup'): - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - pwork.project_set(self.PROJ_ID, self.PROJ_LINK_NAME) - self.assertFalse(cser.project_get()) - cser.project_set(pwork, 'U-Boot', quiet=True) - - with terminal.capture(): - cser.add('first', '', allow_unmarked=True) - cser.add('second', allow_unmarked=True) - - with self.stage('autolink unset'): - with terminal.capture() as (out, _): - yield cser, pwork - self.assertEqual( - "Setting link for series 'second' v1 to " - f'{self.SERIES_ID_SECOND_V1}', - out.getvalue().splitlines()[-1]) - - svlist = cser.get_ser_ver_list() - self.assertEqual(2, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(1, svlist[0].version) - self.assertEqual(2, svlist[1].idnum) - self.assertEqual(2, svlist[1].series_id) - self.assertEqual(1, svlist[1].version) - self.assertEqual(str(self.SERIES_ID_SECOND_V1), svlist[1].link) - yield None - - def test_series_autolink(self): - """Test linking a cseries to its patchwork series by description""" - cor = self.check_series_autolink() - cser, pwork = next(cor) - - with self.assertRaises(ValueError) as exc: - cser.link_auto(pwork, 'first', None, True) - self.assertIn("Series 'first' has an empty description", - str(exc.exception)) - - # autolink unset - cser.link_auto(pwork, 'second', None, True) - - self.assertFalse(next(cor)) - cor.close() - - def test_series_autolink_cmdline(self): - """Test linking to patchwork series by description on cmdline""" - cor = self.check_series_autolink() - _, pwork = next(cor) - - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first', 'autolink', expect_ret=1, - pwork=pwork) - self.assertEqual( - "patman: ValueError: Series 'first' has an empty description", - out.getvalue().strip()) - - # autolink unset - self.run_args('series', '-s', 'second', 'autolink', '-u', pwork=pwork) - - self.assertFalse(next(cor)) - cor.close() - - def _autolink_setup(self): - """Set things up for autolink tests - - Return: tuple: - Cseries object - Patchwork object - """ - cser = self.get_cser() - - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - pwork.project_set(self.PROJ_ID, self.PROJ_LINK_NAME) - self.assertFalse(cser.project_get()) - cser.project_set(pwork, 'U-Boot', quiet=True) - - with terminal.capture(): - cser.add('first', 'first series', allow_unmarked=True) - cser.add('second', allow_unmarked=True) - cser.increment('first') - return cser, pwork - - def test_series_link_auto_all(self): - """Test linking all cseries to their patchwork series by description""" - cser, pwork = self._autolink_setup() - with terminal.capture() as (out, _): - summary = cser.link_auto_all(pwork, update_commit=True, - link_all_versions=True, - replace_existing=False, dry_run=True, - show_summary=False) - self.assertEqual(3, len(summary)) - items = iter(summary.values()) - linked = next(items) - self.assertEqual( - ('first', 1, None, 'first series', 'linked:1234'), linked) - self.assertEqual( - ('first', 2, None, 'first series', 'not found'), next(items)) - self.assertEqual( - ('second', 1, f'{self.SERIES_ID_SECOND_V1}', 'Series for my board', - f'already:{self.SERIES_ID_SECOND_V1}'), - next(items)) - self.assertEqual('Dry run completed', out.getvalue().splitlines()[-1]) - - # A second dry run should do exactly the same thing - with terminal.capture() as (out2, _): - summary2 = cser.link_auto_all(pwork, update_commit=True, - link_all_versions=True, - replace_existing=False, dry_run=True, - show_summary=False) - self.assertEqual(out.getvalue(), out2.getvalue()) - self.assertEqual(summary, summary2) - - # Now do it for real - with terminal.capture(): - summary = cser.link_auto_all(pwork, update_commit=True, - link_all_versions=True, - replace_existing=False, dry_run=False, - show_summary=False) - - # Check the link was updated - pdict = cser.get_ser_ver_dict() - svid = list(summary)[0] - self.assertEqual('1234', pdict[svid].link) - - series = patchstream.get_metadata_for_list('first', self.gitdir, 2) - self.assertEqual('1:1234', series.links) - - def test_series_autolink_latest(self): - """Test linking the lastest versions""" - cser, pwork = self._autolink_setup() - with terminal.capture(): - summary = cser.link_auto_all(pwork, update_commit=True, - link_all_versions=False, - replace_existing=False, dry_run=False, - show_summary=False) - self.assertEqual(2, len(summary)) - items = iter(summary.values()) - self.assertEqual( - ('first', 2, None, 'first series', 'not found'), next(items)) - self.assertEqual( - ('second', 1, f'{self.SERIES_ID_SECOND_V1}', 'Series for my board', - f'already:{self.SERIES_ID_SECOND_V1}'), - next(items)) - - def test_series_autolink_no_update(self): - """Test linking the lastest versions without updating commits""" - cser, pwork = self._autolink_setup() - with terminal.capture(): - cser.link_auto_all(pwork, update_commit=False, - link_all_versions=True, replace_existing=False, - dry_run=False, - show_summary=False) - - series = patchstream.get_metadata_for_list('first', self.gitdir, 2) - self.assertNotIn('links', series) - - def test_series_autolink_replace(self): - """Test linking the lastest versions without updating commits""" - cser, pwork = self._autolink_setup() - with terminal.capture(): - summary = cser.link_auto_all(pwork, update_commit=True, - link_all_versions=True, - replace_existing=True, dry_run=False, - show_summary=False) - self.assertEqual(3, len(summary)) - items = iter(summary.values()) - linked = next(items) - self.assertEqual( - ('first', 1, None, 'first series', 'linked:1234'), linked) - self.assertEqual( - ('first', 2, None, 'first series', 'not found'), next(items)) - self.assertEqual( - ('second', 1, f'{self.SERIES_ID_SECOND_V1}', 'Series for my board', - f'linked:{self.SERIES_ID_SECOND_V1}'), - next(items)) - - def test_series_autolink_extra(self): - """Test command-line operation - - This just uses mocks for now since we can rely on the direct tests for - the actual operation. - """ - _, pwork = self._autolink_setup() - with (mock.patch.object(cseries.Cseries, 'link_auto_all', - return_value=None) as method): - self.run_args('series', 'autolink-all', pwork=True) - method.assert_called_once_with(True, update_commit=False, - link_all_versions=False, - replace_existing=False, dry_run=False, - show_summary=True) - - with (mock.patch.object(cseries.Cseries, 'link_auto_all', - return_value=None) as method): - self.run_args('series', 'autolink-all', '-a', pwork=True) - method.assert_called_once_with(True, update_commit=False, - link_all_versions=True, - replace_existing=False, dry_run=False, - show_summary=True) - - with (mock.patch.object(cseries.Cseries, 'link_auto_all', - return_value=None) as method): - self.run_args('series', 'autolink-all', '-a', '-r', pwork=True) - method.assert_called_once_with(True, update_commit=False, - link_all_versions=True, - replace_existing=True, dry_run=False, - show_summary=True) - - with (mock.patch.object(cseries.Cseries, 'link_auto_all', - return_value=None) as method): - self.run_args('series', '-n', 'autolink-all', '-r', pwork=True) - method.assert_called_once_with(True, update_commit=False, - link_all_versions=False, - replace_existing=True, dry_run=True, - show_summary=True) - - with (mock.patch.object(cseries.Cseries, 'link_auto_all', - return_value=None) as method): - self.run_args('series', 'autolink-all', '-u', pwork=True) - method.assert_called_once_with(True, update_commit=True, - link_all_versions=False, - replace_existing=False, dry_run=False, - show_summary=True) - - # Now do a real one to check the patchwork handling and output - with terminal.capture() as (out, _): - self.run_args('series', 'autolink-all', '-a', pwork=pwork) - itr = iter(out.getvalue().splitlines()) - self.assertEqual( - '1 series linked, 1 already linked, 1 not found (3 requests)', - next(itr)) - self.assertEqual('', next(itr)) - self.assertEqual( - 'Name Version Description ' - ' Result', next(itr)) - self.assertTrue(next(itr).startswith('--')) - self.assertEqual( - 'first 1 first series ' - ' linked:1234', next(itr)) - self.assertEqual( - 'first 2 first series ' - ' not found', next(itr)) - self.assertEqual( - 'second 1 Series for my board ' - f' already:{self.SERIES_ID_SECOND_V1}', - next(itr)) - self.assertTrue(next(itr).startswith('--')) - self.assert_finished(itr) - - def check_series_archive(self): - """Coroutine to run the archive test""" - cser = self.get_cser() - with self.stage('setup'): - with terminal.capture(): - cser.add('first', '', allow_unmarked=True) - - # Check the series is visible in the list - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - self.assertEqual('first', slist['first'].name) - - # Add a second branch - with terminal.capture(): - cser.increment('first') - - cser.fake_now = datetime(24, 9, 14) - repo = pygit2.init_repository(self.gitdir) - with self.stage('archive'): - expected_commit1 = repo.revparse_single('first') - expected_commit2 = repo.revparse_single('first2') - expected_tag1 = 'first-14sep24' - expected_tag2 = 'first2-14sep24' - - # Archive it and make sure it is invisible - yield cser - slist = cser.db.series_get_dict() - self.assertFalse(slist) - - # ...unless we include archived items - slist = cser.db.series_get_dict(include_archived=True) - self.assertEqual(1, len(slist)) - first = slist['first'] - self.assertEqual('first', first.name) - - # Make sure the branches have been tagged - svlist = cser.db.ser_ver_get_for_series(first.idnum) - self.assertEqual(expected_tag1, svlist[0].archive_tag) - self.assertEqual(expected_tag2, svlist[1].archive_tag) - - # Check that the tags were created and point to old branch commits - target1 = repo.revparse_single(expected_tag1) - self.assertEqual(expected_commit1, target1.get_object()) - target2 = repo.revparse_single(expected_tag2) - self.assertEqual(expected_commit2, target2.get_object()) - - # The branches should be deleted - self.assertFalse('first' in repo.branches) - self.assertFalse('first2' in repo.branches) - - with self.stage('unarchive'): - # or we unarchive it - yield cser - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - - # Make sure the branches have been restored - branch1 = repo.branches['first'] - branch2 = repo.branches['first2'] - self.assertEqual(expected_commit1.oid, branch1.target) - self.assertEqual(expected_commit2.oid, branch2.target) - - # Make sure the tags were deleted - try: - target1 = repo.revparse_single(expected_tag1) - self.fail('target1 is still present') - except KeyError: - pass - try: - target1 = repo.revparse_single(expected_tag2) - self.fail('target2 is still present') - except KeyError: - pass - - # Make sure the tag information has been removed - svlist = cser.db.ser_ver_get_for_series(first.idnum) - self.assertFalse(svlist[0].archive_tag) - self.assertFalse(svlist[1].archive_tag) - - yield False - - def test_series_archive(self): - """Test marking a series as archived""" - cor = self.check_series_archive() - cser = next(cor) - - # Archive it and make sure it is invisible - cser.archive('first') - cser = next(cor) - cser.unarchive('first') - self.assertFalse(next(cor)) - cor.close() - - def test_series_archive_cmdline(self): - """Test marking a series as archived with cmdline""" - cor = self.check_series_archive() - cser = next(cor) - - # Archive it and make sure it is invisible - self.run_args('series', '-s', 'first', 'archive', pwork=True, - cser=cser) - next(cor) - self.run_args('series', '-s', 'first', 'unarchive', pwork=True, - cser=cser) - self.assertFalse(next(cor)) - cor.close() - - def check_series_inc(self): - """Coroutine to run the increment test""" - cser = self.get_cser() - - with self.stage('setup'): - gitutil.checkout('first', self.gitdir, work_tree=self.tmpdir, - force=True) - with terminal.capture() as (out, _): - cser.add('first', '', allow_unmarked=True) - - with self.stage('increment'): - with terminal.capture() as (out, _): - yield cser - self._check_inc(out) - - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - - svlist = cser.get_ser_ver_list() - self.assertEqual(2, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(1, svlist[0].version) - - self.assertEqual(2, svlist[1].idnum) - self.assertEqual(1, svlist[1].series_id) - self.assertEqual(2, svlist[1].version) - - series = patchstream.get_metadata_for_list('first2', self.gitdir, - 1) - self.assertEqual('2', series.version) - - series = patchstream.get_metadata_for_list('first', self.gitdir, 1) - self.assertNotIn('version', series) - - self.assertEqual('first2', gitutil.get_branch(self.gitdir)) - yield None - - def test_series_inc(self): - """Test incrementing the version""" - cor = self.check_series_inc() - cser = next(cor) - - cser.increment('first') - self.assertFalse(next(cor)) - - cor.close() - - def test_series_inc_cmdline(self): - """Test incrementing the version with cmdline""" - cor = self.check_series_inc() - next(cor) - - self.run_args('series', '-s', 'first', 'inc', pwork=True) - self.assertFalse(next(cor)) - cor.close() - - def test_series_inc_no_upstream(self): - """Increment a series which has no upstream branch""" - cser = self.get_cser() - - gitutil.checkout('first', self.gitdir, work_tree=self.tmpdir, - force=True) - with terminal.capture(): - cser.add('first', '', allow_unmarked=True) - - repo = pygit2.init_repository(self.gitdir) - upstream = repo.lookup_branch('base') - upstream.delete() - with terminal.capture(): - cser.increment('first') - - slist = cser.db.series_get_dict() - self.assertEqual(1, len(slist)) - - def test_series_inc_dryrun(self): - """Test incrementing the version with cmdline""" - cser = self.get_cser() - - gitutil.checkout('first', self.gitdir, work_tree=self.tmpdir, - force=True) - with terminal.capture() as (out, _): - cser.add('first', '', allow_unmarked=True) - - with terminal.capture() as (out, _): - cser.increment('first', dry_run=True) - itr = self._check_inc(out) - self.assertEqual('Dry run completed', next(itr)) - - # Make sure that nothing was added - svlist = cser.get_ser_ver_list() - self.assertEqual(1, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(1, svlist[0].version) - - # We should still be on the same branch - self.assertEqual('first', gitutil.get_branch(self.gitdir)) - - def test_series_dec(self): - """Test decrementing the version""" - cser = self.get_cser() - - gitutil.checkout('first', self.gitdir, work_tree=self.tmpdir, - force=True) - with terminal.capture() as (out, _): - cser.add('first', '', allow_unmarked=True) - - pclist = cser.get_pcommit_dict() - self.assertEqual(2, len(pclist)) - - # Try decrementing when there is only one version - with self.assertRaises(ValueError) as exc: - cser.decrement('first') - self.assertEqual("Series 'first' only has one version", - str(exc.exception)) - - # Add a version; now there should be two - with terminal.capture() as (out, _): - cser.increment('first') - svdict = cser.get_ser_ver_dict() - self.assertEqual(2, len(svdict)) - - pclist = cser.get_pcommit_dict() - self.assertEqual(4, len(pclist)) - - # Remove version two, using dry run (i.e. no effect) - with terminal.capture() as (out, _): - cser.decrement('first', dry_run=True) - svdict = cser.get_ser_ver_dict() - self.assertEqual(2, len(svdict)) - - repo = pygit2.init_repository(self.gitdir) - branch = repo.lookup_branch('first2') - self.assertTrue(branch) - branch_oid = branch.peel(pygit2.enums.ObjectType.COMMIT).oid - - pclist = cser.get_pcommit_dict() - self.assertEqual(4, len(pclist)) - - # Now remove version two for real - with terminal.capture() as (out, _): - cser.decrement('first') - lines = out.getvalue().splitlines() - self.assertEqual(2, len(lines)) - self.assertEqual("Removing series 'first' v2", lines[0]) - self.assertEqual( - f"Deleted branch 'first2' {str(branch_oid)[:10]}", lines[1]) - - svdict = cser.get_ser_ver_dict() - self.assertEqual(1, len(svdict)) - - pclist = cser.get_pcommit_dict() - self.assertEqual(2, len(pclist)) - - branch = repo.lookup_branch('first2') - self.assertFalse(branch) - - # Removing the only version should not be allowed - with self.assertRaises(ValueError) as exc: - cser.decrement('first', dry_run=True) - self.assertEqual("Series 'first' only has one version", - str(exc.exception)) - - def test_upstream_add(self): - """Test adding an upsream""" - cser = self.get_cser() - - cser.upstream_add('us', 'https://one') - ulist = cser.get_upstream_dict() - self.assertEqual(1, len(ulist)) - self.assertEqual(('https://one', None), ulist['us']) - - cser.upstream_add('ci', 'git@two') - ulist = cser.get_upstream_dict() - self.assertEqual(2, len(ulist)) - self.assertEqual(('https://one', None), ulist['us']) - self.assertEqual(('git@two', None), ulist['ci']) - - # Try to add a duplicate - with self.assertRaises(ValueError) as exc: - cser.upstream_add('ci', 'git@three') - self.assertEqual("Upstream 'ci' already exists", str(exc.exception)) - - with terminal.capture() as (out, _): - cser.upstream_list() - lines = out.getvalue().splitlines() - self.assertEqual(2, len(lines)) - self.assertEqual('us https://one', lines[0]) - self.assertEqual('ci git@two', lines[1]) - - def test_upstream_add_cmdline(self): - """Test adding an upsream with cmdline""" - with terminal.capture(): - self.run_args('upstream', 'add', 'us', 'https://one') - - with terminal.capture() as (out, _): - self.run_args('upstream', 'list') - lines = out.getvalue().splitlines() - self.assertEqual(1, len(lines)) - self.assertEqual('us https://one', lines[0]) - - def test_upstream_default(self): - """Operation of the default upstream""" - cser = self.get_cser() - - with self.assertRaises(ValueError) as exc: - cser.upstream_set_default('us') - self.assertEqual("No such upstream 'us'", str(exc.exception)) - - cser.upstream_add('us', 'https://one') - cser.upstream_add('ci', 'git@two') - - self.assertIsNone(cser.upstream_get_default()) - - cser.upstream_set_default('us') - self.assertEqual('us', cser.upstream_get_default()) - - cser.upstream_set_default('us') - - cser.upstream_set_default('ci') - self.assertEqual('ci', cser.upstream_get_default()) - - with terminal.capture() as (out, _): - cser.upstream_list() - lines = out.getvalue().splitlines() - self.assertEqual(2, len(lines)) - self.assertEqual('us https://one', lines[0]) - self.assertEqual('ci default git@two', lines[1]) - - cser.upstream_set_default(None) - self.assertIsNone(cser.upstream_get_default()) - - def test_upstream_default_cmdline(self): - """Operation of the default upstream on cmdline""" - with terminal.capture() as (out, _): - self.run_args('upstream', 'default', 'us', expect_ret=1) - self.assertEqual("patman: ValueError: No such upstream 'us'", - out.getvalue().strip().splitlines()[-1]) - - self.run_args('upstream', 'add', 'us', 'https://one') - self.run_args('upstream', 'add', 'ci', 'git@two') - - with terminal.capture() as (out, _): - self.run_args('upstream', 'default') - self.assertEqual('unset', out.getvalue().strip()) - - self.run_args('upstream', 'default', 'us') - with terminal.capture() as (out, _): - self.run_args('upstream', 'default') - self.assertEqual('us', out.getvalue().strip()) - - self.run_args('upstream', 'default', 'ci') - with terminal.capture() as (out, _): - self.run_args('upstream', 'default') - self.assertEqual('ci', out.getvalue().strip()) - - with terminal.capture() as (out, _): - self.run_args('upstream', 'default', '--unset') - self.assertFalse(out.getvalue().strip()) - - with terminal.capture() as (out, _): - self.run_args('upstream', 'default') - self.assertEqual('unset', out.getvalue().strip()) - - def test_upstream_delete(self): - """Test operation of the default upstream""" - cser = self.get_cser() - - with self.assertRaises(ValueError) as exc: - cser.upstream_delete('us') - self.assertEqual("No such upstream 'us'", str(exc.exception)) - - cser.upstream_add('us', 'https://one') - cser.upstream_add('ci', 'git@two') - - cser.upstream_set_default('us') - cser.upstream_delete('us') - self.assertIsNone(cser.upstream_get_default()) - - cser.upstream_delete('ci') - ulist = cser.get_upstream_dict() - self.assertFalse(ulist) - - def test_upstream_delete_cmdline(self): - """Test deleting an upstream""" - with terminal.capture() as (out, _): - self.run_args('upstream', 'delete', 'us', expect_ret=1) - self.assertEqual("patman: ValueError: No such upstream 'us'", - out.getvalue().strip().splitlines()[-1]) - - self.run_args('us', 'add', 'us', 'https://one') - self.run_args('us', 'add', 'ci', 'git@two') - - self.run_args('upstream', 'default', 'us') - self.run_args('upstream', 'delete', 'us') - with terminal.capture() as (out, _): - self.run_args('upstream', 'default', 'us', expect_ret=1) - self.assertEqual("patman: ValueError: No such upstream 'us'", - out.getvalue().strip()) - - self.run_args('upstream', 'delete', 'ci') - with terminal.capture() as (out, _): - self.run_args('upstream', 'list') - self.assertFalse(out.getvalue().strip()) - - def test_series_add_mark(self): - """Test marking a cseries with Change-Id fields""" - cser = self.get_cser() - - with terminal.capture(): - cser.add('first', '', mark=True) - - pcdict = cser.get_pcommit_dict() - - series = patchstream.get_metadata('first', 0, 2, git_dir=self.gitdir) - self.assertEqual(2, len(series.commits)) - self.assertIn(1, pcdict) - self.assertEqual(1, pcdict[1].idnum) - self.assertEqual('i2c: I2C things', pcdict[1].subject) - self.assertEqual(1, pcdict[1].svid) - self.assertEqual(series.commits[0].change_id, pcdict[1].change_id) - - self.assertIn(2, pcdict) - self.assertEqual(2, pcdict[2].idnum) - self.assertEqual('spi: SPI fixes', pcdict[2].subject) - self.assertEqual(1, pcdict[2].svid) - self.assertEqual(series.commits[1].change_id, pcdict[2].change_id) - - def test_series_add_mark_fail(self): - """Test marking a cseries when the tree is dirty""" - cser = self.get_cser() - - tools.write_file(os.path.join(self.tmpdir, 'fname'), b'123') - with terminal.capture(): - cser.add('first', '', mark=True) - - tools.write_file(os.path.join(self.tmpdir, 'i2c.c'), b'123') - with self.assertRaises(ValueError) as exc: - with terminal.capture(): - cser.add('first', '', mark=True) - self.assertEqual( - "Modified files exist: use 'git status' to check: [' M i2c.c']", - str(exc.exception)) - - def test_series_add_mark_dry_run(self): - """Test marking a cseries with Change-Id fields""" - cser = self.get_cser() - - with terminal.capture() as (out, _): - cser.add('first', '', mark=True, dry_run=True) - itr = iter(out.getvalue().splitlines()) - self.assertEqual( - "Adding series 'first' v1: mark True allow_unmarked False", - next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual("Processing 2 commits from branch 'first'", - next(itr)) - self.assertRegex( - next(itr), f'- marked: {HASH_RE} as {HASH_RE} i2c: I2C things') - self.assertRegex( - next(itr), f'- marked: {HASH_RE} as {HASH_RE} spi: SPI fixes') - self.assertRegex( - next(itr), f'Updating branch first from {HASH_RE} to {HASH_RE}') - self.assertEqual("Added series 'first' v1 (2 commits)", - next(itr)) - self.assertEqual('Dry run completed', next(itr)) - - # Doing another dry run should produce the same result - with terminal.capture() as (out2, _): - cser.add('first', '', mark=True, dry_run=True) - self.assertEqual(out.getvalue(), out2.getvalue()) - - tools.write_file(os.path.join(self.tmpdir, 'i2c.c'), b'123') - with terminal.capture() as (out, _): - with self.assertRaises(ValueError) as exc: - cser.add('first', '', mark=True, dry_run=True) - self.assertEqual( - "Modified files exist: use 'git status' to check: [' M i2c.c']", - str(exc.exception)) - - pcdict = cser.get_pcommit_dict() - self.assertFalse(pcdict) - - def test_series_add_mark_cmdline(self): - """Test marking a cseries with Change-Id fields using the cmdline""" - cser = self.get_cser() - - with terminal.capture(): - self.run_args('series', '-s', 'first', 'add', '-m', - '-D', 'my-description', pwork=True) - - pcdict = cser.get_pcommit_dict() - self.assertTrue(pcdict[1].change_id) - self.assertTrue(pcdict[2].change_id) - - def test_series_add_unmarked_cmdline(self): - """Test adding an unmarked cseries using the command line""" - cser = self.get_cser() - - with terminal.capture(): - self.run_args('series', '-s', 'first', 'add', '-M', - '-D', 'my-description', pwork=True) - - pcdict = cser.get_pcommit_dict() - self.assertFalse(pcdict[1].change_id) - self.assertFalse(pcdict[2].change_id) - - def test_series_add_unmarked_bad_cmdline(self): - """Test failure to add an unmarked cseries using a bad command line""" - self.get_cser() - - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first', 'add', - '-D', 'my-description', expect_ret=1, pwork=True) - last_line = out.getvalue().splitlines()[-2] - self.assertEqual( - 'patman: ValueError: 2 commit(s) are unmarked; ' - 'please use -m or -M', last_line) - - def check_series_unmark(self): - """Checker for unmarking tests""" - cser = self.get_cser() - with self.stage('unmarked commits'): - yield cser - - with self.stage('mark commits'): - with terminal.capture() as (out, _): - yield cser - - with self.stage('unmark: dry run'): - with terminal.capture() as (out, _): - yield cser - - itr = iter(out.getvalue().splitlines()) - self.assertEqual( - "Unmarking series 'first': allow_unmarked False", - next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual("Processing 2 commits from branch 'first'", - next(itr)) - self.assertRegex( - next(itr), - f'- unmarked: {HASH_RE} as {HASH_RE} i2c: I2C things') - self.assertRegex( - next(itr), - f'- unmarked: {HASH_RE} as {HASH_RE} spi: SPI fixes') - self.assertRegex( - next(itr), f'Updating branch first from {HASH_RE} to {HASH_RE}') - self.assertEqual('Dry run completed', next(itr)) - - with self.stage('unmark'): - with terminal.capture() as (out, _): - yield cser - self.assertIn('- unmarked', out.getvalue()) - - with self.stage('unmark: allow unmarked'): - with terminal.capture() as (out, _): - yield cser - self.assertIn('- no mark', out.getvalue()) - - yield None - - def test_series_unmark(self): - """Test unmarking a cseries, i.e. removing Change-Id fields""" - cor = self.check_series_unmark() - cser = next(cor) - - # check the allow_unmarked flag - with terminal.capture(): - with self.assertRaises(ValueError) as exc: - cser.unmark('first', dry_run=True) - self.assertEqual('Unmarked commits 2/2', str(exc.exception)) - - # mark commits - cser = next(cor) - cser.add('first', '', mark=True) - - # unmark: dry run - cser = next(cor) - cser.unmark('first', dry_run=True) - - # unmark - cser = next(cor) - cser.unmark('first') - - # unmark: allow unmarked - cser = next(cor) - cser.unmark('first', allow_unmarked=True) - - self.assertFalse(next(cor)) - - def test_series_unmark_cmdline(self): - """Test the unmark command""" - cor = self.check_series_unmark() - next(cor) - - # check the allow_unmarked flag - with terminal.capture() as (out, _): - self.run_args('series', 'unmark', expect_ret=1, pwork=True) - self.assertIn('Unmarked commits 2/2', out.getvalue()) - - # mark commits - next(cor) - self.run_args('series', '-s', 'first', 'add', '-D', '', '--mark', - pwork=True) - - # unmark: dry run - next(cor) - self.run_args('series', '-s', 'first', '-n', 'unmark', pwork=True) - - # unmark - next(cor) - self.run_args('series', '-s', 'first', 'unmark', pwork=True) - - # unmark: allow unmarked - next(cor) - self.run_args('series', '-s', 'first', 'unmark', '--allow-unmarked', - pwork=True) - - self.assertFalse(next(cor)) - - def test_series_unmark_middle(self): - """Test unmarking with Change-Id fields not last in the commit""" - cser = self.get_cser() - with terminal.capture(): - cser.add('first', '', allow_unmarked=True) - - # Add some change IDs in the middle of the commit message - with terminal.capture(): - name, ser, _, _ = cser.prep_series('first') - old_msgs = [] - for vals in cser.process_series(name, ser): - old_msgs.append(vals.msg) - lines = vals.msg.splitlines() - change_id = cser.make_change_id(vals.commit) - extra = [f'{cser_helper.CHANGE_ID_TAG}: {change_id}'] - vals.msg = '\n'.join(lines[:2] + extra + lines[2:]) + '\n' - - with terminal.capture(): - cser.unmark('first') - - # We should get back the original commit message - series = patchstream.get_metadata('first', 0, 2, git_dir=self.gitdir) - self.assertEqual(old_msgs[0], series.commits[0].msg) - self.assertEqual(old_msgs[1], series.commits[1].msg) - - def check_series_mark(self): - """Checker for marking tests""" - cser = self.get_cser() - yield cser - - # Start with a dry run, which should do nothing - with self.stage('dry run'): - with terminal.capture(): - yield cser - - series = patchstream.get_metadata_for_list('first', self.gitdir, 2) - self.assertEqual(2, len(series.commits)) - self.assertFalse(series.commits[0].change_id) - self.assertFalse(series.commits[1].change_id) - - # Now do a real run - with self.stage('real run'): - with terminal.capture(): - yield cser - - series = patchstream.get_metadata_for_list('first', self.gitdir, 2) - self.assertEqual(2, len(series.commits)) - self.assertTrue(series.commits[0].change_id) - self.assertTrue(series.commits[1].change_id) - - # Try to mark again, which should fail - with self.stage('mark twice'): - with terminal.capture(): - with self.assertRaises(ValueError) as exc: - cser.mark('first', dry_run=False) - self.assertEqual('Marked commits 2/2', str(exc.exception)) - - # Use the --marked flag to make it succeed - with self.stage('mark twice with --marked'): - with terminal.capture(): - yield cser - self.assertEqual('Marked commits 2/2', str(exc.exception)) - - series2 = patchstream.get_metadata_for_list('first', self.gitdir, - 2) - self.assertEqual(2, len(series2.commits)) - self.assertEqual(series.commits[0].change_id, - series2.commits[0].change_id) - self.assertEqual(series.commits[1].change_id, - series2.commits[1].change_id) - - yield None - - def test_series_mark(self): - """Test marking a cseries, i.e. adding Change-Id fields""" - cor = self.check_series_mark() - cser = next(cor) - - # Start with a dry run, which should do nothing - cser = next(cor) - cser.mark('first', dry_run=True) - - # Now do a real run - cser = next(cor) - cser.mark('first', dry_run=False) - - # Try to mark again, which should fail - with terminal.capture(): - with self.assertRaises(ValueError) as exc: - cser.mark('first', dry_run=False) - self.assertEqual('Marked commits 2/2', str(exc.exception)) - - # Use the --allow-marked flag to make it succeed - cser = next(cor) - cser.mark('first', allow_marked=True, dry_run=False) - - self.assertFalse(next(cor)) - - def test_series_mark_cmdline(self): - """Test marking a cseries, i.e. adding Change-Id fields""" - cor = self.check_series_mark() - next(cor) - - # Start with a dry run, which should do nothing - next(cor) - self.run_args('series', '-n', '-s', 'first', 'mark', pwork=True) - - # Now do a real run - next(cor) - self.run_args('series', '-s', 'first', 'mark', pwork=True) - - # Try to mark again, which should fail - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first', 'mark', expect_ret=1, - pwork=True) - self.assertIn('Marked commits 2/2', out.getvalue()) - - # Use the --allow-marked flag to make it succeed - next(cor) - self.run_args('series', '-s', 'first', 'mark', '--allow-marked', - pwork=True) - self.assertFalse(next(cor)) - - def test_series_remove(self): - """Test removing a series""" - cser = self.get_cser() - - with self.stage('remove non-existent series'): - with self.assertRaises(ValueError) as exc: - cser.remove('first') - self.assertEqual("No such series 'first'", str(exc.exception)) - - with self.stage('add'): - with terminal.capture() as (out, _): - cser.add('first', '', mark=True) - self.assertTrue(cser.db.series_get_dict()) - pclist = cser.get_pcommit_dict() - self.assertEqual(2, len(pclist)) - - with self.stage('remove'): - with terminal.capture() as (out, _): - cser.remove('first') - self.assertEqual("Removed series 'first'", out.getvalue().strip()) - self.assertFalse(cser.db.series_get_dict()) - - pclist = cser.get_pcommit_dict() - self.assertFalse(len(pclist)) - - def test_series_remove_cmdline(self): - """Test removing a series using the command line""" - cser = self.get_cser() - - with self.stage('remove non-existent series'): - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'first', 'rm', expect_ret=1, - pwork=True) - self.assertEqual("patman: ValueError: No such series 'first'", - out.getvalue().strip()) - - with self.stage('add'): - with terminal.capture() as (out, _): - cser.add('first', '', mark=True) - self.assertTrue(cser.db.series_get_dict()) - - with self.stage('remove'): - with terminal.capture() as (out, _): - cser.remove('first') - self.assertEqual("Removed series 'first'", out.getvalue().strip()) - self.assertFalse(cser.db.series_get_dict()) - - def check_series_remove_multiple(self): - """Check for removing a series with more than one version""" - cser = self.get_cser() - - with self.stage('setup'): - self.add_first2(True) - - with terminal.capture() as (out, _): - cser.add(None, '', mark=True) - cser.add('first', '', mark=True) - self.assertTrue(cser.db.series_get_dict()) - pclist = cser.get_pcommit_dict() - self.assertEqual(4, len(pclist)) - - # Do a dry-run removal - with self.stage('dry run'): - with terminal.capture() as (out, _): - yield cser - self.assertEqual("Removed version 1 from series 'first'\n" - 'Dry run completed', out.getvalue().strip()) - self.assertEqual({'first'}, cser.db.series_get_dict().keys()) - - svlist = cser.get_ser_ver_list() - self.assertEqual(2, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(2, svlist[0].version) - - self.assertEqual(2, svlist[1].idnum) - self.assertEqual(1, svlist[1].series_id) - self.assertEqual(1, svlist[1].version) - - # Now remove for real - with self.stage('real'): - with terminal.capture() as (out, _): - yield cser - self.assertEqual("Removed version 1 from series 'first'", - out.getvalue().strip()) - self.assertEqual({'first'}, cser.db.series_get_dict().keys()) - plist = cser.get_ser_ver_list() - self.assertEqual(1, len(plist)) - pclist = cser.get_pcommit_dict() - self.assertEqual(2, len(pclist)) - - with self.stage('remove only version'): - yield cser - self.assertEqual({'first'}, cser.db.series_get_dict().keys()) - - svlist = cser.get_ser_ver_list() - self.assertEqual(1, len(svlist)) - self.assertEqual(1, svlist[0].idnum) - self.assertEqual(1, svlist[0].series_id) - self.assertEqual(2, svlist[0].version) - - with self.stage('remove series (dry run'): - with terminal.capture() as (out, _): - yield cser - self.assertEqual("Removed series 'first'\nDry run completed", - out.getvalue().strip()) - self.assertTrue(cser.db.series_get_dict()) - self.assertTrue(cser.get_ser_ver_list()) - - with self.stage('remove series'): - with terminal.capture() as (out, _): - yield cser - self.assertEqual("Removed series 'first'", out.getvalue().strip()) - self.assertFalse(cser.db.series_get_dict()) - self.assertFalse(cser.get_ser_ver_list()) - - yield False - - def test_series_remove_multiple(self): - """Test removing a series with more than one version""" - cor = self.check_series_remove_multiple() - cser = next(cor) - - # Do a dry-run removal - cser.version_remove('first', 1, dry_run=True) - cser = next(cor) - - # Now remove for real - cser.version_remove('first', 1) - cser = next(cor) - - # Remove only version - with self.assertRaises(ValueError) as exc: - cser.version_remove('first', 2, dry_run=True) - self.assertEqual( - "Series 'first' only has one version: remove the series", - str(exc.exception)) - cser = next(cor) - - # Remove series (dry run) - cser.remove('first', dry_run=True) - cser = next(cor) - - # Remove series (real) - cser.remove('first') - - self.assertFalse(next(cor)) - cor.close() - - def test_series_remove_multiple_cmdline(self): - """Test removing a series with more than one version on cmdline""" - cor = self.check_series_remove_multiple() - next(cor) - - # Do a dry-run removal - self.run_args('series', '-n', '-s', 'first', '-V', '1', 'rm-version', - pwork=True) - next(cor) - - # Now remove for real - self.run_args('series', '-s', 'first', '-V', '1', 'rm-version', - pwork=True) - next(cor) - - # Remove only version - with terminal.capture() as (out, _): - self.run_args('series', '-n', '-s', 'first', '-V', '2', - 'rm-version', expect_ret=1, pwork=True) - self.assertIn( - "Series 'first' only has one version: remove the series", - out.getvalue().strip()) - next(cor) - - # Remove series (dry run) - self.run_args('series', '-n', '-s', 'first', 'rm', pwork=True) - next(cor) - - # Remove series (real) - self.run_args('series', '-s', 'first', 'rm', pwork=True) - - self.assertFalse(next(cor)) - cor.close() - - def test_patchwork_set_project(self): - """Test setting the project ID""" - cser = self.get_cser() - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - with terminal.capture() as (out, _): - cser.project_set(pwork, 'U-Boot') - self.assertEqual( - f"Project 'U-Boot' patchwork-ID {self.PROJ_ID} link-name uboot", - out.getvalue().strip()) - - def test_patchwork_project_get(self): - """Test setting the project ID""" - cser = self.get_cser() - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - self.assertFalse(cser.project_get()) - with terminal.capture() as (out, _): - cser.project_set(pwork, 'U-Boot') - self.assertEqual( - f"Project 'U-Boot' patchwork-ID {self.PROJ_ID} link-name uboot", - out.getvalue().strip()) - - name, pwid, link_name = cser.project_get() - self.assertEqual('U-Boot', name) - self.assertEqual(self.PROJ_ID, pwid) - self.assertEqual('uboot', link_name) - - def test_patchwork_project_get_cmdline(self): - """Test setting the project ID""" - cser = self.get_cser() - - self.assertFalse(cser.project_get()) - - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - with terminal.capture() as (out, _): - self.run_args('-P', 'https://url', 'patchwork', 'set-project', - 'U-Boot', pwork=pwork) - self.assertEqual( - f"Project 'U-Boot' patchwork-ID {self.PROJ_ID} link-name uboot", - out.getvalue().strip()) - - name, pwid, link_name = cser.project_get() - self.assertEqual('U-Boot', name) - self.assertEqual(6, pwid) - self.assertEqual('uboot', link_name) - - with terminal.capture() as (out, _): - self.run_args('-P', 'https://url', 'patchwork', 'get-project') - self.assertEqual( - f"Project 'U-Boot' patchwork-ID {self.PROJ_ID} link-name uboot", - out.getvalue().strip()) - - def check_series_list_patches(self): - """Test listing the patches for a series""" - cser = self.get_cser() - - with self.stage('setup'): - with terminal.capture() as (out, _): - cser.add(None, '', allow_unmarked=True) - cser.add('second', allow_unmarked=True) - target = self.repo.lookup_reference('refs/heads/second') - self.repo.checkout( - target, strategy=pygit2.enums.CheckoutStrategy.FORCE) - cser.increment('second') - - with self.stage('list first'): - with terminal.capture() as (out, _): - yield cser - itr = iter(out.getvalue().splitlines()) - self.assertEqual("Branch 'first' (total 2): 2:unknown", next(itr)) - self.assertIn('PatchId', next(itr)) - self.assertRegex(next(itr), r' 0 .* i2c: I2C things') - self.assertRegex(next(itr), r' 1 .* spi: SPI fixes') - - with self.stage('list second2'): - with terminal.capture() as (out, _): - yield cser - itr = iter(out.getvalue().splitlines()) - self.assertEqual( - "Branch 'second2' (total 3): 3:unknown", next(itr)) - self.assertIn('PatchId', next(itr)) - self.assertRegex( - next(itr), ' 0 .* video: Some video improvements') - self.assertRegex(next(itr), ' 1 .* serial: Add a serial driver') - self.assertRegex(next(itr), ' 2 .* bootm: Make it boot') - - yield None - - def test_series_list_patches(self): - """Test listing the patches for a series""" - cor = self.check_series_list_patches() - cser = next(cor) - - # list first - cser.list_patches('first', 1) - cser = next(cor) - - # list second2 - cser.list_patches('second2', 2) - self.assertFalse(next(cor)) - cor.close() - - def test_series_list_patches_cmdline(self): - """Test listing the patches for a series using the cmdline""" - cor = self.check_series_list_patches() - next(cor) - - # list first - self.run_args('series', '-s', 'first', 'patches', pwork=True) - next(cor) - - # list second2 - self.run_args('series', '-s', 'second', '-V', '2', 'patches', - pwork=True) - self.assertFalse(next(cor)) - cor.close() - - def test_series_list_patches_detail(self): - """Test listing the patches for a series""" - cser = self.get_cser() - with terminal.capture(): - cser.add(None, '', allow_unmarked=True) - cser.add('second', allow_unmarked=True) - target = self.repo.lookup_reference('refs/heads/second') - self.repo.checkout( - target, strategy=pygit2.enums.CheckoutStrategy.FORCE) - cser.increment('second') - - with terminal.capture() as (out, _): - cser.list_patches('first', 1, show_commit=True) - expect = r'''Branch 'first' (total 2): 2:unknown -Seq State Com PatchId Commit Subject - 0 unknown - .* i2c: I2C things - -commit .* -Author: Test user <[email protected]> -Date: .* - - i2c: I2C things - - This has some stuff to do with I2C - - i2c.c | 2 ++ - 1 file changed, 2 insertions(+) - - - 1 unknown - .* spi: SPI fixes - -commit .* -Author: Test user <[email protected]> -Date: .* - - spi: SPI fixes - - SPI needs some fixes - and here they are - - Signed-off-by: Lord Edmund Blackaddër <[email protected]> - - Series-to: u-boot - Commit-notes: - title of the series - This is the cover letter for the series - with various details - END - - spi.c | 3 +++ - 1 file changed, 3 insertions(+) -''' - itr = iter(out.getvalue().splitlines()) - for seq, eline in enumerate(expect.splitlines()): - line = next(itr).rstrip() - if '*' in eline: - self.assertRegex(line, eline, f'line {seq + 1}') - else: - self.assertEqual(eline, line, f'line {seq + 1}') - - # Show just the patch; this should exclude the commit message - with terminal.capture() as (out, _): - cser.list_patches('first', 1, show_patch=True) - chk = out.getvalue() - self.assertIn('SPI fixes', chk) # subject - self.assertNotIn('SPI needs some fixes', chk) # commit body - self.assertIn('make SPI work', chk) # patch body - - # Show both - with terminal.capture() as (out, _): - cser.list_patches('first', 1, show_commit=True, show_patch=True) - chk = out.getvalue() - self.assertIn('SPI fixes', chk) # subject - self.assertIn('SPI needs some fixes', chk) # commit body - self.assertIn('make SPI work', chk) # patch body - - def check_series_gather(self): - """Checker for gathering tags for a series""" - cser = self.get_cser() - with self.stage('setup'): - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - self.assertFalse(cser.project_get()) - cser.project_set(pwork, 'U-Boot', quiet=True) - - with terminal.capture() as (out, _): - cser.add('second', 'description', allow_unmarked=True) - - ser = cser.get_series_by_name('second') - pwid = cser.get_series_svid(ser.idnum, 1) - - # First do a dry run - with self.stage('gather: dry run'): - with terminal.capture() as (out, _): - yield cser, pwork - lines = out.getvalue().splitlines() - self.assertEqual( - f"Updating series 'second' version 1 from link " - f"'{self.SERIES_ID_SECOND_V1}'", - lines[0]) - self.assertEqual('3 patches updated (7 requests)', lines[1]) - self.assertEqual('Dry run completed', lines[2]) - self.assertEqual(3, len(lines)) - - pwc = cser.get_pcommit_dict(pwid) - self.assertIsNone(pwc[0].state) - self.assertIsNone(pwc[1].state) - self.assertIsNone(pwc[2].state) - - # Now try it again, gathering tags - with self.stage('gather: dry run'): - with terminal.capture() as (out, _): - yield cser, pwork - lines = out.getvalue().splitlines() - itr = iter(lines) - self.assertEqual( - f"Updating series 'second' version 1 from link " - f"'{self.SERIES_ID_SECOND_V1}'", - next(itr)) - self.assertEqual(' 1 video: Some video improvements', next(itr)) - self.assertEqual(' + Reviewed-by: Fred Bloggs <[email protected]>', - next(itr)) - self.assertEqual(' 2 serial: Add a serial driver', next(itr)) - self.assertEqual(' 3 bootm: Make it boot', next(itr)) - - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual("Processing 3 commits from branch 'second'", - next(itr)) - self.assertRegex( - next(itr), - f'- added 1 tag: {HASH_RE} as {HASH_RE} ' - 'video: Some video improvements') - self.assertRegex( - next(itr), - f"- upd links '1:456': {HASH_RE} as {HASH_RE} " - 'serial: Add a serial driver') - self.assertRegex( - next(itr), - f'- {HASH_RE} as {HASH_RE} ' - 'bootm: Make it boot') - self.assertRegex( - next(itr), - f'Updating branch second from {HASH_RE} to {HASH_RE}') - self.assertEqual('3 patches updated (7 requests)', next(itr)) - self.assertEqual('Dry run completed', next(itr)) - self.assert_finished(itr) - - # Make sure that no tags were added to the branch - series = patchstream.get_metadata_for_list('second', self.gitdir, - 3) - for cmt in series.commits: - self.assertFalse(cmt.rtags, - 'Commit {cmt.subject} rtags {cmt.rtags}') - - # Now do it for real - with self.stage('gather: real'): - with terminal.capture() as (out, _): - yield cser, pwork - lines2 = out.getvalue().splitlines() - self.assertEqual(lines2, lines[:-1]) - - # Make sure that the tags were added to the branch - series = patchstream.get_metadata_for_list('second', self.gitdir, - 3) - self.assertEqual( - {'Reviewed-by': {'Fred Bloggs <[email protected]>'}}, - series.commits[0].rtags) - self.assertFalse(series.commits[1].rtags) - self.assertFalse(series.commits[2].rtags) - - # Make sure the status was updated - pwc = cser.get_pcommit_dict(pwid) - self.assertEqual('accepted', pwc[0].state) - self.assertEqual('changes-requested', pwc[1].state) - self.assertEqual('rejected', pwc[2].state) - - yield None - - def test_series_gather(self): - """Test gathering tags for a series""" - cor = self.check_series_gather() - cser, pwork = next(cor) - - # sync (dry_run) - cser.gather(pwork, 'second', None, False, False, False, dry_run=True) - cser, pwork = next(cor) - - # gather (dry_run) - cser.gather(pwork, 'second', None, False, False, True, dry_run=True) - cser, pwork = next(cor) - - # gather (real) - cser.gather(pwork, 'second', None, False, False, True) - - self.assertFalse(next(cor)) - - def test_series_gather_cmdline(self): - """Test gathering tags for a series with cmdline""" - cor = self.check_series_gather() - _, pwork = next(cor) - - # sync (dry_run) - self.run_args( - 'series', '-n', '-s', 'second', 'gather', '-G', pwork=pwork) - - # gather (dry_run) - _, pwork = next(cor) - self.run_args('series', '-n', '-s', 'second', 'gather', pwork=pwork) - - # gather (real) - _, pwork = next(cor) - self.run_args('series', '-s', 'second', 'gather', pwork=pwork) - - self.assertFalse(next(cor)) - - def check_series_gather_all(self): - """Gather all series at once""" - with self.stage('setup'): - cser, pwork = self.setup_second(False) - - with terminal.capture(): - cser.add('first', 'description', allow_unmarked=True) - cser.increment('first') - cser.increment('first') - cser.link_set('first', 1, '123', True) - cser.link_set('first', 2, '1234', True) - cser.link_set('first', 3, f'{self.SERIES_ID_FIRST_V3}', True) - cser.link_auto(pwork, 'second', 2, True) - - with self.stage('no options'): - with terminal.capture() as (out, _): - yield cser, pwork - self.assertEqual( - "Syncing 'first' v3\n" - "Syncing 'second' v2\n" - '\n' - '5 patches and 2 cover letters updated, 0 missing links ' - '(14 requests)\n' - 'Dry run completed', - out.getvalue().strip()) - - with self.stage('gather'): - with terminal.capture() as (out, _): - yield cser, pwork - lines = out.getvalue().splitlines() - itr = iter(lines) - self.assertEqual("Syncing 'first' v3", next(itr)) - self.assertEqual(' 1 i2c: I2C things', next(itr)) - self.assertEqual( - ' + Tested-by: Mary Smith <[email protected]> # yak', - next(itr)) - self.assertEqual(' 2 spi: SPI fixes', next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual( - "Processing 2 commits from branch 'first3'", next(itr)) - self.assertRegex( - next(itr), - f'- added 1 tag: {HASH_RE} as {HASH_RE} i2c: I2C things') - self.assertRegex( - next(itr), - f"- upd links '3:31': {HASH_RE} as {HASH_RE} spi: SPI fixes") - self.assertRegex( - next(itr), - f'Updating branch first3 from {HASH_RE} to {HASH_RE}') - self.assertEqual('', next(itr)) - - self.assertEqual("Syncing 'second' v2", next(itr)) - self.assertEqual(' 1 video: Some video improvements', next(itr)) - self.assertEqual( - ' + Reviewed-by: Fred Bloggs <[email protected]>', next(itr)) - self.assertEqual(' 2 serial: Add a serial driver', next(itr)) - self.assertEqual(' 3 bootm: Make it boot', next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual( - "Processing 3 commits from branch 'second2'", next(itr)) - self.assertRegex( - next(itr), - f'- added 1 tag: {HASH_RE} as {HASH_RE} ' - 'video: Some video improvements') - self.assertRegex( - next(itr), - f"- upd links '2:457 1:456': {HASH_RE} as {HASH_RE} " - 'serial: Add a serial driver') - self.assertRegex( - next(itr), - f'- {HASH_RE} as {HASH_RE} ' - 'bootm: Make it boot') - self.assertRegex( - next(itr), - f'Updating branch second2 from {HASH_RE} to {HASH_RE}') - self.assertEqual('', next(itr)) - self.assertEqual( - '5 patches and 2 cover letters updated, 0 missing links ' - '(14 requests)', - next(itr)) - self.assertEqual('Dry run completed', next(itr)) - self.assert_finished(itr) - - with self.stage('gather, patch comments,!dry_run'): - with terminal.capture() as (out, _): - yield cser, pwork - lines = out.getvalue().splitlines() - itr = iter(lines) - self.assertEqual("Syncing 'first' v1", next(itr)) - self.assertEqual(' 1 i2c: I2C things', next(itr)) - self.assertEqual( - ' + Tested-by: Mary Smith <[email protected]> # yak', - next(itr)) - self.assertEqual(' 2 spi: SPI fixes', next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual( - "Processing 2 commits from branch 'first'", next(itr)) - self.assertRegex( - next(itr), - f'- added 1 tag: {HASH_RE} as {HASH_RE} i2c: I2C things') - self.assertRegex( - next(itr), - f"- upd links '1:123': {HASH_RE} as {HASH_RE} spi: SPI fixes") - self.assertRegex( - next(itr), - f'Updating branch first from {HASH_RE} to {HASH_RE}') - self.assertEqual('', next(itr)) - - self.assertEqual("Syncing 'first' v2", next(itr)) - self.assertEqual(' 1 i2c: I2C things', next(itr)) - self.assertEqual( - ' + Tested-by: Mary Smith <[email protected]> # yak', - next(itr)) - self.assertEqual(' 2 spi: SPI fixes', next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual( - "Processing 2 commits from branch 'first2'", next(itr)) - self.assertRegex( - next(itr), - f'- added 1 tag: {HASH_RE} as {HASH_RE} ' - 'i2c: I2C things') - self.assertRegex( - next(itr), - f"- upd links '2:1234': {HASH_RE} as {HASH_RE} spi: SPI fixes") - self.assertRegex( - next(itr), - f'Updating branch first2 from {HASH_RE} to {HASH_RE}') - self.assertEqual('', next(itr)) - self.assertEqual("Syncing 'first' v3", next(itr)) - self.assertEqual(' 1 i2c: I2C things', next(itr)) - self.assertEqual( - ' + Tested-by: Mary Smith <[email protected]> # yak', - next(itr)) - self.assertEqual(' 2 spi: SPI fixes', next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual( - "Processing 2 commits from branch 'first3'", next(itr)) - self.assertRegex( - next(itr), - f'- added 1 tag: {HASH_RE} as {HASH_RE} i2c: I2C things') - self.assertRegex( - next(itr), - f"- upd links '3:31': {HASH_RE} as {HASH_RE} spi: SPI fixes") - self.assertRegex( - next(itr), - f'Updating branch first3 from {HASH_RE} to {HASH_RE}') - self.assertEqual('', next(itr)) - - self.assertEqual("Syncing 'second' v1", next(itr)) - self.assertEqual(' 1 video: Some video improvements', next(itr)) - self.assertEqual( - ' + Reviewed-by: Fred Bloggs <[email protected]>', next(itr)) - self.assertEqual( - 'Review: Fred Bloggs <[email protected]>', next(itr)) - self.assertEqual(' > This was my original patch', next(itr)) - self.assertEqual(' > which is being quoted', next(itr)) - self.assertEqual( - ' I like the approach here and I would love to see more ' - 'of it.', next(itr)) - self.assertEqual('', next(itr)) - self.assertEqual(' 2 serial: Add a serial driver', next(itr)) - self.assertEqual(' 3 bootm: Make it boot', next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual( - "Processing 3 commits from branch 'second'", next(itr)) - self.assertRegex( - next(itr), - f'- added 1 tag: {HASH_RE} as {HASH_RE} ' - 'video: Some video improvements') - self.assertRegex( - next(itr), - f"- upd links '1:456': {HASH_RE} as {HASH_RE} " - 'serial: Add a serial driver') - self.assertRegex( - next(itr), - f'- {HASH_RE} as {HASH_RE} ' - 'bootm: Make it boot') - self.assertRegex( - next(itr), - f'Updating branch second from {HASH_RE} to {HASH_RE}') - self.assertEqual('', next(itr)) - - self.assertEqual("Syncing 'second' v2", next(itr)) - self.assertEqual(' 1 video: Some video improvements', next(itr)) - self.assertEqual( - ' + Reviewed-by: Fred Bloggs <[email protected]>', next(itr)) - self.assertEqual( - 'Review: Fred Bloggs <[email protected]>', next(itr)) - self.assertEqual(' > This was my original patch', next(itr)) - self.assertEqual(' > which is being quoted', next(itr)) - self.assertEqual( - ' I like the approach here and I would love to see more ' - 'of it.', next(itr)) - self.assertEqual('', next(itr)) - self.assertEqual(' 2 serial: Add a serial driver', next(itr)) - self.assertEqual(' 3 bootm: Make it boot', next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual( - "Processing 3 commits from branch 'second2'", next(itr)) - self.assertRegex( - next(itr), - f'- added 1 tag: {HASH_RE} as {HASH_RE} ' - 'video: Some video improvements') - self.assertRegex( - next(itr), - f"- upd links '2:457 1:456': {HASH_RE} as {HASH_RE} " - 'serial: Add a serial driver') - self.assertRegex( - next(itr), - f'- {HASH_RE} as {HASH_RE} ' - 'bootm: Make it boot') - self.assertRegex( - next(itr), - f'Updating branch second2 from {HASH_RE} to {HASH_RE}') - self.assertEqual('', next(itr)) - self.assertEqual( - '12 patches and 3 cover letters updated, 0 missing links ' - '(32 requests)', next(itr)) - self.assert_finished(itr) - - yield None - - def test_series_gather_all(self): - """Gather all series at once""" - cor = self.check_series_gather_all() - cser, pwork = next(cor) - - # no options - cser.gather_all(pwork, False, True, False, False, dry_run=True) - cser, pwork = next(cor) - - # gather - cser.gather_all(pwork, False, False, False, True, dry_run=True) - cser, pwork = next(cor) - - # gather, patch comments, !dry_run - cser.gather_all(pwork, True, False, True, True) - - self.assertFalse(next(cor)) - - def test_series_gather_all_cmdline(self): - """Sync all series at once using cmdline""" - cor = self.check_series_gather_all() - _, pwork = next(cor) - - # no options - self.run_args('series', '-n', '-s', 'second', 'gather-all', '-G', - pwork=pwork) - _, pwork = next(cor) - - # gather - self.run_args('series', '-n', '-s', 'second', 'gather-all', - pwork=pwork) - _, pwork = next(cor) - - # gather, patch comments, !dry_run - self.run_args('series', '-s', 'second', 'gather-all', '-a', '-c', - pwork=pwork) - - self.assertFalse(next(cor)) - - def _check_second(self, itr, show_all): - """Check output from a 'progress' command - - Args: - itr (Iterator): Contains the output lines to check - show_all (bool): True if all versions are being shown, not just - latest - """ - self.assertEqual('second: Series for my board (versions: 1 2)', - next(itr)) - if show_all: - self.assertEqual("Branch 'second' (total 3): 3:unknown", - next(itr)) - self.assertIn('PatchId', next(itr)) - self.assertRegex( - next(itr), - ' 0 unknown - .* video: Some video improvements') - self.assertRegex( - next(itr), - ' 1 unknown - .* serial: Add a serial driver') - self.assertRegex( - next(itr), - ' 2 unknown - .* bootm: Make it boot') - self.assertEqual('', next(itr)) - self.assertEqual( - "Branch 'second2' (total 3): 1:accepted 1:changes 1:rejected", - next(itr)) - self.assertIn('PatchId', next(itr)) - self.assertEqual( - 'Cov 2 139 ' - 'The name of the cover letter', next(itr)) - self.assertRegex( - next(itr), - ' 0 accepted 2 110 .* video: Some video improvements') - self.assertRegex( - next(itr), - ' 1 changes 111 .* serial: Add a serial driver') - self.assertRegex( - next(itr), - ' 2 rejected 3 112 .* bootm: Make it boot') - - def test_series_progress(self): - """Test showing progress for a cseries""" - self.setup_second() - self.db_close() - - with self.stage('latest versions'): - args = Namespace(subcmd='progress', series='second', - show_all_versions=False, list_patches=True) - with terminal.capture() as (out, _): - control.do_series(args, test_db=self.tmpdir, pwork=True) - lines = iter(out.getvalue().splitlines()) - self._check_second(lines, False) - - with self.stage('all versions'): - args.show_all_versions = True - with terminal.capture() as (out, _): - control.do_series(args, test_db=self.tmpdir, pwork=True) - lines = iter(out.getvalue().splitlines()) - self._check_second(lines, True) - - def _check_first(self, itr): - """Check output from the progress command - - Args: - itr (Iterator): Contains the output lines to check - """ - self.assertEqual('first: (versions: 1)', next(itr)) - self.assertEqual("Branch 'first' (total 2): 2:unknown", next(itr)) - self.assertIn('PatchId', next(itr)) - self.assertRegex( - next(itr), - ' 0 unknown - .* i2c: I2C things') - self.assertRegex( - next(itr), - ' 1 unknown - .* spi: SPI fixes') - self.assertEqual('', next(itr)) - - def test_series_progress_all(self): - """Test showing progress for all cseries""" - self.setup_second() - self.db_close() - - with self.stage('progress with patches'): - args = Namespace(subcmd='progress', series=None, - show_all_versions=False, list_patches=True) - with terminal.capture() as (out, _): - control.do_series(args, test_db=self.tmpdir, pwork=True) - lines = iter(out.getvalue().splitlines()) - self._check_first(lines) - self._check_second(lines, False) - - with self.stage('all versions'): - args.show_all_versions = True - with terminal.capture() as (out, _): - control.do_series(args, test_db=self.tmpdir, pwork=True) - lines = iter(out.getvalue().splitlines()) - self._check_first(lines) - self._check_second(lines, True) - - def test_series_progress_no_patches(self): - """Test showing progress for all cseries without patches""" - self.setup_second() - - with terminal.capture() as (out, _): - self.run_args('series', 'progress', pwork=True) - itr = iter(out.getvalue().splitlines()) - self.assertEqual( - 'Name Description ' - 'Count Status', next(itr)) - self.assertTrue(next(itr).startswith('--')) - self.assertEqual( - 'first ' - ' 2 2:unknown', next(itr)) - self.assertEqual( - 'second2 The name of the cover letter ' - ' 3 1:accepted 1:changes 1:rejected', next(itr)) - self.assertTrue(next(itr).startswith('--')) - self.assertEqual( - ['2', 'series', '5', '2:unknown', '1:accepted', '1:changes', - '1:rejected'], - next(itr).split()) - self.assert_finished(itr) - - def test_series_progress_all_no_patches(self): - """Test showing progress for all cseries versions without patches""" - self.setup_second() - - with terminal.capture() as (out, _): - self.run_args('series', 'progress', '--show-all-versions', - pwork=True) - itr = iter(out.getvalue().splitlines()) - self.assertEqual( - 'Name Description ' - 'Count Status', next(itr)) - self.assertTrue(next(itr).startswith('--')) - self.assertEqual( - 'first ' - ' 2 2:unknown', next(itr)) - self.assertEqual( - 'second Series for my board ' - ' 3 3:unknown', next(itr)) - self.assertEqual( - 'second2 The name of the cover letter ' - ' 3 1:accepted 1:changes 1:rejected', next(itr)) - self.assertTrue(next(itr).startswith('--')) - self.assertEqual( - ['3', 'series', '8', '5:unknown', '1:accepted', '1:changes', - '1:rejected'], - next(itr).split()) - self.assert_finished(itr) - - def test_series_summary(self): - """Test showing a summary of series status""" - self.setup_second() - - self.db_close() - args = Namespace(subcmd='summary', series=None) - with terminal.capture() as (out, _): - control.do_series(args, test_db=self.tmpdir, pwork=True) - lines = out.getvalue().splitlines() - self.assertEqual( - 'Name Status Description', - lines[0]) - self.assertEqual( - '----------------- ------ ------------------------------', - lines[1]) - self.assertEqual('first -/2 ', lines[2]) - self.assertEqual('second 1/3 Series for my board', lines[3]) - - def test_series_open(self): - """Test opening a series in a web browser""" - cser = self.get_cser() - pwork = Patchwork.for_testing(self._fake_patchwork_cser) - self.assertFalse(cser.project_get()) - pwork.project_set(self.PROJ_ID, self.PROJ_LINK_NAME) - - with terminal.capture(): - cser.add('second', allow_unmarked=True) - cser.increment('second') - cser.link_auto(pwork, 'second', 2, True) - cser.gather(pwork, 'second', 2, False, False, False) - - with mock.patch.object(cros_subprocess.Popen, '__init__', - return_value=None) as method: - with terminal.capture() as (out, _): - cser.open(pwork, 'second2', 2) - - url = ('https://patchwork.ozlabs.org/project/uboot/list/?series=457' - '&state=*&archive=both') - method.assert_called_once_with(['xdg-open', url]) - self.assertEqual(f'Opening {url}', out.getvalue().strip()) - - def test_name_version(self): - """Test handling of series names and versions""" - cser = self.get_cser() - repo = self.repo - - self.assertEqual(('fred', None), - cser_helper.split_name_version('fred')) - self.assertEqual(('mary', 2), cser_helper.split_name_version('mary2')) - - ser, version = cser._parse_series_and_version(None, None) - self.assertEqual('first', ser.name) - self.assertEqual(1, version) - - ser, version = cser._parse_series_and_version('first', None) - self.assertEqual('first', ser.name) - self.assertEqual(1, version) - - ser, version = cser._parse_series_and_version('first', 2) - self.assertEqual('first', ser.name) - self.assertEqual(2, version) - - with self.assertRaises(ValueError) as exc: - cser._parse_series_and_version('123', 2) - self.assertEqual( - "Series name '123' cannot be a number, use '<name><version>'", - str(exc.exception)) - - with self.assertRaises(ValueError) as exc: - cser._parse_series_and_version('first', 100) - self.assertEqual("Version 100 exceeds 99", str(exc.exception)) - - with terminal.capture() as (_, err): - cser._parse_series_and_version('mary3', 4) - self.assertIn('Version mismatch: -V has 4 but branch name indicates 3', - err.getvalue()) - - ser, version = cser._parse_series_and_version('mary', 4) - self.assertEqual('mary', ser.name) - self.assertEqual(4, version) - - # Move off the branch and check for a sensible error - commit = repo.revparse_single('first~') - repo.checkout_tree(commit) - repo.set_head(commit.oid) - - with self.assertRaises(ValueError) as exc: - cser._parse_series_and_version(None, None) - self.assertEqual('No branch detected: please use -s <series>', - str(exc.exception)) - - def test_name_version_extra(self): - """More tests for some corner cases""" - cser, _ = self.setup_second() - target = self.repo.lookup_reference('refs/heads/second2') - self.repo.checkout( - target, strategy=pygit2.enums.CheckoutStrategy.FORCE) - - ser, version = cser._parse_series_and_version(None, None) - self.assertEqual('second', ser.name) - self.assertEqual(2, version) - - ser, version = cser._parse_series_and_version('second2', None) - self.assertEqual('second', ser.name) - self.assertEqual(2, version) - - def test_migrate(self): - """Test migration to later schema versions""" - db = database.Database(f'{self.tmpdir}/.patman.db') - with terminal.capture() as (out, err): - db.open_it() - self.assertEqual( - f'Creating new database {self.tmpdir}/.patman.db', - err.getvalue().strip()) - - self.assertEqual(0, db.get_schema_version()) - - for version in range(1, database.LATEST + 1): - with terminal.capture() as (out, _): - db.migrate_to(version) - self.assertTrue(os.path.exists( - f'{self.tmpdir}/.patman.dbold.v{version - 1}')) - self.assertEqual(f'Update database to v{version}', - out.getvalue().strip()) - self.assertEqual(version, db.get_schema_version()) - self.assertEqual(4, database.LATEST) - - def test_series_scan(self): - """Test scanning a series for updates""" - cser, _ = self.setup_second() - target = self.repo.lookup_reference('refs/heads/second2') - self.repo.checkout( - target, strategy=pygit2.enums.CheckoutStrategy.FORCE) - - # Add a new commit - self.repo = pygit2.init_repository(self.gitdir) - self.make_commit_with_file( - 'wip: Try out a new thing', 'Just checking', 'wibble.c', - '''changes to wibble''') - target = self.repo.revparse_single('HEAD') - self.repo.reset(target.oid, pygit2.enums.ResetMode.HARD) - - # name = gitutil.get_branch(self.gitdir) - # upstream_name = gitutil.get_upstream(self.gitdir, name) - name, ser, version, _ = cser.prep_series(None) - - # We now have 4 commits numbered 0 (second~3) to 3 (the one we just - # added). Drop commit 1 (the 'serial' one) from the branch - cser._filter_commits(name, ser, 1) - svid = cser.get_ser_ver(ser.idnum, version).idnum - old_pcdict = cser.get_pcommit_dict(svid).values() - - expect = '''Syncing series 'second2' v2: mark False allow_unmarked True - 0 video: Some video improvements -- 1 serial: Add a serial driver - 1 bootm: Make it boot -+ 2 Just checking -''' - with terminal.capture() as (out, _): - self.run_args('series', '-n', 'scan', '-M', pwork=True) - self.assertEqual(expect + 'Dry run completed\n', out.getvalue()) - - new_pcdict = cser.get_pcommit_dict(svid).values() - self.assertEqual(list(old_pcdict), list(new_pcdict)) - - with terminal.capture() as (out, _): - self.run_args('series', 'scan', '-M', pwork=True) - self.assertEqual(expect, out.getvalue()) - - new_pcdict = cser.get_pcommit_dict(svid).values() - self.assertEqual(len(old_pcdict), len(new_pcdict)) - chk = list(new_pcdict) - self.assertNotEqual(list(old_pcdict), list(new_pcdict)) - self.assertEqual('video: Some video improvements', chk[0].subject) - self.assertEqual('bootm: Make it boot', chk[1].subject) - self.assertEqual('Just checking', chk[2].subject) - - def test_series_send(self): - """Test sending a series""" - cser, pwork = self.setup_second() - - # Create a third version - with terminal.capture(): - cser.increment('second') - series = patchstream.get_metadata_for_list('second3', self.gitdir, 3) - self.assertEqual('2:457 1:456', series.links) - self.assertEqual('3', series.version) - - with terminal.capture() as (out, err): - self.run_args('series', '-n', '-s', 'second3', 'send', - '--no-autolink', pwork=pwork) - self.assertIn('Send a total of 3 patches with a cover letter', - out.getvalue()) - self.assertIn( - 'video.c:1: warning: Missing or malformed SPDX-License-Identifier ' - 'tag in line 1', err.getvalue()) - self.assertIn( - '<patch>:19: warning: added, moved or deleted file(s), does ' - 'MAINTAINERS need updating?', err.getvalue()) - self.assertIn('bootm.c:1: check: Avoid CamelCase: <Fix>', - err.getvalue()) - self.assertIn( - 'Cc: Anatolij Gustschin <[email protected]>', out.getvalue()) - - self.assertTrue(os.path.exists(os.path.join( - self.tmpdir, '0001-video-Some-video-improvements.patch'))) - self.assertTrue(os.path.exists(os.path.join( - self.tmpdir, '0002-serial-Add-a-serial-driver.patch'))) - self.assertTrue(os.path.exists(os.path.join( - self.tmpdir, '0003-bootm-Make-it-boot.patch'))) - - def test_series_send_and_link(self): - """Test sending a series and then adding its link to the database""" - def h_sleep(time_s): - if cser.get_time() > 25: - self.autolink_extra = {'id': 500, - 'name': 'Series for my board', - 'version': 3} - cser.inc_fake_time(time_s) - - cser, pwork = self.setup_second() - - # Create a third version - with terminal.capture(): - cser.increment('second') - series = patchstream.get_metadata_for_list('second3', self.gitdir, 3) - self.assertEqual('2:457 1:456', series.links) - self.assertEqual('3', series.version) - - with terminal.capture(): - self.run_args('series', '-n', 'send', pwork=pwork) - - cser.set_fake_time(h_sleep) - with terminal.capture() as (out, _): - cser.link_auto(pwork, 'second3', 3, True, 50) - itr = iter(out.getvalue().splitlines()) - for i in range(7): - self.assertEqual( - "Possible matches for 'second' v3 desc 'Series for my board':", - next(itr), f'failed at i={i}') - self.assertEqual(' Link Version Description', next(itr)) - self.assertEqual(' 456 1 Series for my board', next(itr)) - self.assertEqual(' 457 2 Series for my board', next(itr)) - self.assertEqual('Sleeping for 5 seconds', next(itr)) - self.assertEqual('Link completed after 35 seconds', next(itr)) - self.assertRegex( - next(itr), 'Checking out upstream commit refs/heads/base: .*') - self.assertEqual( - "Processing 3 commits from branch 'second3'", next(itr)) - self.assertRegex( - next(itr), - f'- {HASH_RE} as {HASH_RE} ' - 'video: Some video improvements') - self.assertRegex( - next(itr), - f"- add links '3:500 2:457 1:456': {HASH_RE} as {HASH_RE} " - 'serial: Add a serial driver') - self.assertRegex( - next(itr), - f'- add v3: {HASH_RE} as {HASH_RE} ' - 'bootm: Make it boot') - self.assertRegex( - next(itr), - f'Updating branch second3 from {HASH_RE} to {HASH_RE}') - self.assertEqual( - "Setting link for series 'second' v3 to 500", next(itr)) - - def _check_status(self, out, has_comments, has_cover_comments): - """Check output from the status command - - Args: - itr (Iterator): Contains the output lines to check - """ - itr = iter(out.getvalue().splitlines()) - if has_cover_comments: - self.assertEqual('Cov The name of the cover letter', next(itr)) - self.assertEqual( - 'From: A user <[email protected]>: Sun 13 Apr 14:06:02 MDT 2025', - next(itr)) - self.assertEqual('some comment', next(itr)) - self.assertEqual('', next(itr)) - - self.assertEqual( - 'From: Ghenkis Khan <[email protected]>: Sun 13 Apr 13:06:02 ' - 'MDT 2025', - next(itr)) - self.assertEqual('another comment', next(itr)) - self.assertEqual('', next(itr)) - - self.assertEqual(' 1 video: Some video improvements', next(itr)) - self.assertEqual(' + Reviewed-by: Fred Bloggs <[email protected]>', - next(itr)) - if has_comments: - self.assertEqual( - 'Review: Fred Bloggs <[email protected]>', next(itr)) - self.assertEqual(' > This was my original patch', next(itr)) - self.assertEqual(' > which is being quoted', next(itr)) - self.assertEqual( - ' I like the approach here and I would love to see more ' - 'of it.', next(itr)) - self.assertEqual('', next(itr)) - - self.assertEqual(' 2 serial: Add a serial driver', next(itr)) - self.assertEqual(' 3 bootm: Make it boot', next(itr)) - self.assertEqual( - '1 new response available in patchwork (use -d to write them to ' - 'a new branch)', next(itr)) - - def test_series_status(self): - """Test getting the status of a series, including comments""" - cser, pwork = self.setup_second() - - # Use single threading for easy debugging, but the multithreaded - # version should produce the same output - with self.stage('status second2: single-threaded'): - with terminal.capture() as (out, _): - cser.status(pwork, 'second', 2, False) - self._check_status(out, False, False) - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) - - with self.stage('status second2 (normal)'): - with terminal.capture() as (out2, _): - cser.status(pwork, 'second', 2, False) - self.assertEqual(out.getvalue(), out2.getvalue()) - self._check_status(out, False, False) - - with self.stage('with comments'): - with terminal.capture() as (out, _): - cser.status(pwork, 'second', 2, show_comments=True) - self._check_status(out, True, False) - - with self.stage('with comments and cover comments'): - with terminal.capture() as (out, _): - cser.status(pwork, 'second', 2, show_comments=True, - show_cover_comments=True) - self._check_status(out, True, True) - - def test_series_status_cmdline(self): - """Test getting the status of a series, including comments""" - cser, pwork = self.setup_second() - - with self.stage('status second2'): - with terminal.capture() as (out, _): - self.run_args('series', '-s', 'second', '-V', '2', 'status', - pwork=pwork) - self._check_status(out, False, False) - - with self.stage('status second2 (normal)'): - with terminal.capture() as (out, _): - cser.status(pwork, 'second', 2, show_comments=True) - self._check_status(out, True, False) - - with self.stage('with comments and cover comments'): - with terminal.capture() as (out, _): - cser.status(pwork, 'second', 2, show_comments=True, - show_cover_comments=True) - self._check_status(out, True, True) - - def test_series_no_subcmd(self): - """Test handling of things without a subcommand""" - parsers = cmdline.setup_parser() - parsers['series'].catch_error = True - with terminal.capture() as (out, _): - cmdline.parse_args(['series'], parsers=parsers) - self.assertIn('usage: patman series', out.getvalue()) - - parsers['patchwork'].catch_error = True - with terminal.capture() as (out, _): - cmdline.parse_args(['patchwork'], parsers=parsers) - self.assertIn('usage: patman patchwork', out.getvalue()) - - parsers['upstream'].catch_error = True - with terminal.capture() as (out, _): - cmdline.parse_args(['upstream'], parsers=parsers) - self.assertIn('usage: patman upstream', out.getvalue()) - - def check_series_rename(self): - """Check renaming a series""" - cser = self.get_cser() - with self.stage('setup'): - with terminal.capture() as (out, _): - cser.add('first', 'my name', allow_unmarked=True) - - # Remember the old series - old = cser.get_series_by_name('first') - - self.assertEqual('first', gitutil.get_branch(self.gitdir)) - with terminal.capture() as (out, _): - cser.increment('first') - self.assertEqual('first2', gitutil.get_branch(self.gitdir)) - - with terminal.capture() as (out, _): - cser.increment('first') - self.assertEqual('first3', gitutil.get_branch(self.gitdir)) - - # Do the dry run - with self.stage('rename - dry run'): - with terminal.capture() as (out, _): - yield cser - lines = out.getvalue().splitlines() - itr = iter(lines) - self.assertEqual("Renaming branch 'first' to 'newname'", next(itr)) - self.assertEqual( - "Renaming branch 'first2' to 'newname2'", next(itr)) - self.assertEqual( - "Renaming branch 'first3' to 'newname3'", next(itr)) - self.assertEqual("Renamed series 'first' to 'newname'", next(itr)) - self.assertEqual("Dry run completed", next(itr)) - self.assert_finished(itr) - - # Check nothing changed - self.assertEqual('first3', gitutil.get_branch(self.gitdir)) - sdict = cser.db.series_get_dict() - self.assertIn('first', sdict) - - # Now do it for real - with self.stage('rename - real'): - with terminal.capture() as (out2, _): - yield cser - lines2 = out2.getvalue().splitlines() - self.assertEqual(lines[:-1], lines2) - - self.assertEqual('newname3', gitutil.get_branch(self.gitdir)) - - # Check the series ID did not change - ser = cser.get_series_by_name('newname') - self.assertEqual(old.idnum, ser.idnum) - - yield None - - def test_series_rename(self): - """Test renaming of a series""" - cor = self.check_series_rename() - cser = next(cor) - - # Rename (dry run) - cser.rename('first', 'newname', dry_run=True) - cser = next(cor) - - # Rename (real) - cser.rename('first', 'newname') - self.assertFalse(next(cor)) - - def test_series_rename_cmdline(self): - """Test renaming of a series with the cmdline""" - cor = self.check_series_rename() - next(cor) - - # Rename (dry run) - self.run_args('series', '-n', '-s', 'first', 'rename', '-N', 'newname', - pwork=True) - next(cor) - - # Rename (real) - self.run_args('series', '-s', 'first', 'rename', '-N', 'newname', - pwork=True) - - self.assertFalse(next(cor)) - - def test_series_rename_bad(self): - """Test renaming when it is not allowed""" - cser = self.get_cser() - with terminal.capture(): - cser.add('first', 'my name', allow_unmarked=True) - cser.increment('first') - cser.increment('first') - - with self.assertRaises(ValueError) as exc: - cser.rename('first', 'first') - self.assertEqual("Cannot rename series 'first' to itself", - str(exc.exception)) - - with self.assertRaises(ValueError) as exc: - cser.rename('first2', 'newname') - self.assertEqual( - "Invalid series name 'first2': did you use the branch name?", - str(exc.exception)) - - with self.assertRaises(ValueError) as exc: - cser.rename('first', 'newname2') - self.assertEqual( - "Invalid series name 'newname2': did you use the branch name?", - str(exc.exception)) - - with self.assertRaises(ValueError) as exc: - cser.rename('first', 'second') - self.assertEqual("Cannot rename: branches exist: second", - str(exc.exception)) - - with terminal.capture(): - cser.add('second', 'another name', allow_unmarked=True) - cser.increment('second') - - with self.assertRaises(ValueError) as exc: - cser.rename('first', 'second') - self.assertEqual("Cannot rename: series 'second' already exists", - str(exc.exception)) - - # Rename second2 so that it gets in the way of the rename - gitutil.rename_branch('second2', 'newname2', self.gitdir) - with self.assertRaises(ValueError) as exc: - cser.rename('first', 'newname') - self.assertEqual("Cannot rename: branches exist: newname2", - str(exc.exception)) - - # Rename first3 and make sure it stops the rename - gitutil.rename_branch('first3', 'tempbranch', self.gitdir) - with self.assertRaises(ValueError) as exc: - cser.rename('first', 'newname') - self.assertEqual( - "Cannot rename: branches missing: first3: branches exist: " - 'newname2', str(exc.exception)) - - def test_version_change(self): - """Test changing a version of a series to a different version number""" - cser = self.get_cser() - - with self.stage('setup'): - with terminal.capture(): - cser.add('first', 'my description', allow_unmarked=True) - - with self.stage('non-existent version'): - # Check changing a non-existent version - with self.assertRaises(ValueError) as exc: - cser.version_change('first', 2, 3, dry_run=True) - self.assertEqual("Series 'first' does not have a version 2", - str(exc.exception)) - - with self.stage('new version missing'): - with self.assertRaises(ValueError) as exc: - cser.version_change('first', None, None, dry_run=True) - self.assertEqual("Please provide a new version number", - str(exc.exception)) - - # Change v1 to v2 (dry run) - with self.stage('v1 -> 2 dry run'): - with terminal.capture(): - self.assertTrue(gitutil.check_branch('first', self.gitdir)) - cser.version_change('first', 1, 3, dry_run=True) - self.assertTrue(gitutil.check_branch('first', self.gitdir)) - self.assertFalse(gitutil.check_branch('first3', self.gitdir)) - - # Check that nothing actually happened - series = patchstream.get_metadata('first', 0, 2, - git_dir=self.gitdir) - self.assertNotIn('version', series) - - svlist = cser.get_ser_ver_list() - self.assertEqual(1, len(svlist)) - item = svlist[0] - self.assertEqual(1, item.version) - - with self.stage('increment twice'): - # Increment so that we get first3 - with terminal.capture(): - cser.increment('first') - cser.increment('first') - - with self.stage('existing version'): - # Check changing to an existing version - with self.assertRaises(ValueError) as exc: - cser.version_change('first', 1, 3, dry_run=True) - self.assertEqual("Series 'first' already has a v3: 1 2 3", - str(exc.exception)) - - # Change v1 to v4 (for real) - with self.stage('v1 -> 4'): - with terminal.capture(): - self.assertTrue(gitutil.check_branch('first', self.gitdir)) - cser.version_change('first', 1, 4) - self.assertTrue(gitutil.check_branch('first', self.gitdir)) - self.assertTrue(gitutil.check_branch('first4', self.gitdir)) - - series = patchstream.get_metadata('first4', 0, 2, - git_dir=self.gitdir) - self.assertIn('version', series) - self.assertEqual('4', series.version) - - svdict = cser.get_ser_ver_dict() - self.assertEqual(3, len(svdict)) - item = svdict[item.idnum] - self.assertEqual(4, item.version) - - with self.stage('increment'): - # Now try to increment first again - with terminal.capture(): - cser.increment('first') - - ser = cser.get_series_by_name('first') - self.assertIn(5, cser._get_version_list(ser.idnum)) - - def test_version_change_cmdline(self): - """Check changing a version on the cmdline""" - self.get_cser() - with (mock.patch.object(cseries.Cseries, 'version_change', - return_value=None) as method): - self.run_args('series', '-s', 'first', 'version-change', - pwork=True) - method.assert_called_once_with('first', None, None, dry_run=False) - - with (mock.patch.object(cseries.Cseries, 'version_change', - return_value=None) as method): - self.run_args('series', '-s', 'first', 'version-change', - '--new-version', '3', pwork=True) - method.assert_called_once_with('first', None, 3, dry_run=False) diff --git a/tools/patman/test_settings.py b/tools/patman/test_settings.py deleted file mode 100644 index c117836de31..00000000000 --- a/tools/patman/test_settings.py +++ /dev/null @@ -1,67 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0+ -# -# Copyright (c) 2022 Maxim Cournoyer <[email protected]> -# - -import argparse -import contextlib -import os -import sys -import tempfile - -from patman import settings -from u_boot_pylib import tools - - -def empty_git_repository(): - with tempfile.TemporaryDirectory() as tmpdir: - os.chdir(tmpdir) - tools.run('git', 'init', raise_on_error=True) - yield tmpdir - - -def cleared_command_line_args(): - old_value = sys.argv[:] - sys.argv = [sys.argv[0]] - try: - yield - finally: - sys.argv = old_value - - -def test_git_local_config(): - # Clearing the command line arguments is required, otherwise - # arguments passed to the test running such as in 'pytest -k - # filter' would be processed by _UpdateDefaults and fail. - with cleared_command_line_args(): - with empty_git_repository(): - with tempfile.NamedTemporaryFile() as global_config: - global_config.write(b'[settings]\n' - b'project=u-boot\n') - global_config.flush() - parser = argparse.ArgumentParser() - parser.add_argument('-p', '--project', default='unknown') - subparsers = parser.add_subparsers(dest='cmd') - send = subparsers.add_parser('send') - send.add_argument('--no-check', action='store_false', - dest='check_patch', default=True) - - # Test "global" config is used. - settings.Setup(parser, 'unknown', None, global_config.name) - args, _ = parser.parse_known_args([]) - assert args.project == 'u-boot' - send_args, _ = send.parse_known_args([]) - assert send_args.check_patch - - # Test local config can shadow it. - with open('.patman', 'w', buffering=1) as f: - f.write('[settings]\n' - 'project: guix-patches\n' - 'check_patch: False\n') - settings.Setup(parser, 'unknown', global_config.name) - args, _ = parser.parse_known_args([]) - assert args.project == 'guix-patches' - send_args, _ = send.parse_known_args([]) - assert not send_args.check_patch |
