patman: Remove the patch-management code
Delete the command-line tool and its supporting modules, now that this functionality lives in the standalone patch-manager package. Keep the modules that buildman still imports (commit and patchstream, plus their dependencies series, get_maintainer and settings), along with the stub command. Trim __init__.py to match. Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,823 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0+
|
||||
#
|
||||
# Copyright 2025 Simon Glass <sjg@chromium.org>
|
||||
#
|
||||
"""Handles the patman database
|
||||
|
||||
This uses sqlite3 with a local file.
|
||||
|
||||
To adjsut the schema, increment LATEST, create a migrate_to_v<x>() function
|
||||
and write some code in migrate_to() to call it.
|
||||
"""
|
||||
|
||||
from collections import namedtuple, OrderedDict
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from u_boot_pylib import tools
|
||||
from u_boot_pylib import tout
|
||||
from patman.series import Series
|
||||
|
||||
# Schema version (version 0 means there is no database yet)
|
||||
LATEST = 4
|
||||
|
||||
# Information about a series/version record
|
||||
SerVer = namedtuple(
|
||||
'SER_VER',
|
||||
'idnum,series_id,version,link,cover_id,cover_num_comments,name,'
|
||||
'archive_tag')
|
||||
|
||||
# Record from the pcommit table:
|
||||
# idnum (int): record ID
|
||||
# seq (int): Patch sequence in series (0 is first)
|
||||
# subject (str): patch subject
|
||||
# svid (int): ID of series/version record in ser_ver table
|
||||
# change_id (str): Change-ID value
|
||||
# state (str): Current status in patchwork
|
||||
# patch_id (int): Patchwork's patch ID for this patch
|
||||
# num_comments (int): Number of comments attached to the commit
|
||||
Pcommit = namedtuple(
|
||||
'PCOMMIT',
|
||||
'idnum,seq,subject,svid,change_id,state,patch_id,num_comments')
|
||||
|
||||
|
||||
class Database:
|
||||
"""Database of information used by patman"""
|
||||
|
||||
# dict of databases:
|
||||
# key: filename
|
||||
# value: Database object
|
||||
instances = {}
|
||||
|
||||
def __init__(self, db_path):
|
||||
"""Set up a new database object
|
||||
|
||||
Args:
|
||||
db_path (str): Path to the database
|
||||
"""
|
||||
if db_path in Database.instances:
|
||||
# Two connections to the database can cause:
|
||||
# sqlite3.OperationalError: database is locked
|
||||
raise ValueError(f"There is already a database for '{db_path}'")
|
||||
self.con = None
|
||||
self.cur = None
|
||||
self.db_path = db_path
|
||||
self.is_open = False
|
||||
Database.instances[db_path] = self
|
||||
|
||||
@staticmethod
|
||||
def get_instance(db_path):
|
||||
"""Get the database instance for a path
|
||||
|
||||
This is provides to ensure that different callers can obtain the
|
||||
same database object when accessing the same database file.
|
||||
|
||||
Args:
|
||||
db_path (str): Path to the database
|
||||
|
||||
Return:
|
||||
Database: Database instance, which is created if necessary
|
||||
"""
|
||||
db = Database.instances.get(db_path)
|
||||
if db:
|
||||
return db, False
|
||||
return Database(db_path), True
|
||||
|
||||
def start(self):
|
||||
"""Open the database read for use, migrate to latest schema"""
|
||||
self.open_it()
|
||||
self.migrate_to(LATEST)
|
||||
|
||||
def open_it(self):
|
||||
"""Open the database, creating it if necessary"""
|
||||
if self.is_open:
|
||||
raise ValueError('Already open')
|
||||
if not os.path.exists(self.db_path):
|
||||
tout.warning(f'Creating new database {self.db_path}')
|
||||
self.con = sqlite3.connect(self.db_path)
|
||||
self.cur = self.con.cursor()
|
||||
self.is_open = True
|
||||
|
||||
def close(self):
|
||||
"""Close the database"""
|
||||
if not self.is_open:
|
||||
raise ValueError('Already closed')
|
||||
self.con.close()
|
||||
self.cur = None
|
||||
self.con = None
|
||||
self.is_open = False
|
||||
|
||||
def create_v1(self):
|
||||
"""Create a database with the v1 schema"""
|
||||
self.cur.execute(
|
||||
'CREATE TABLE series (id INTEGER PRIMARY KEY AUTOINCREMENT,'
|
||||
'name UNIQUE, desc, archived BIT)')
|
||||
|
||||
# Provides a series_id/version pair, which is used to refer to a
|
||||
# particular series version sent to patchwork. This stores the link
|
||||
# to patchwork
|
||||
self.cur.execute(
|
||||
'CREATE TABLE ser_ver (id INTEGER PRIMARY KEY AUTOINCREMENT,'
|
||||
'series_id INTEGER, version INTEGER, link,'
|
||||
'FOREIGN KEY (series_id) REFERENCES series (id))')
|
||||
|
||||
self.cur.execute(
|
||||
'CREATE TABLE upstream (name UNIQUE, url, is_default BIT)')
|
||||
|
||||
# change_id is the Change-Id
|
||||
# patch_id is the ID of the patch on the patchwork server
|
||||
self.cur.execute(
|
||||
'CREATE TABLE pcommit (id INTEGER PRIMARY KEY AUTOINCREMENT,'
|
||||
'svid INTEGER, seq INTEGER, subject, patch_id INTEGER, '
|
||||
'change_id, state, num_comments INTEGER, '
|
||||
'FOREIGN KEY (svid) REFERENCES ser_ver (id))')
|
||||
|
||||
self.cur.execute(
|
||||
'CREATE TABLE settings (name UNIQUE, proj_id INT, link_name)')
|
||||
|
||||
def _migrate_to_v2(self):
|
||||
"""Add a schema_version table"""
|
||||
self.cur.execute('CREATE TABLE schema_version (version INTEGER)')
|
||||
|
||||
def _migrate_to_v3(self):
|
||||
"""Store the number of cover-letter comments in the schema"""
|
||||
self.cur.execute('ALTER TABLE ser_ver ADD COLUMN cover_id')
|
||||
self.cur.execute('ALTER TABLE ser_ver ADD COLUMN cover_num_comments '
|
||||
'INTEGER')
|
||||
self.cur.execute('ALTER TABLE ser_ver ADD COLUMN name')
|
||||
|
||||
def _migrate_to_v4(self):
|
||||
"""Add an archive tag for each ser_ver"""
|
||||
self.cur.execute('ALTER TABLE ser_ver ADD COLUMN archive_tag')
|
||||
|
||||
def migrate_to(self, dest_version):
|
||||
"""Migrate the database to the selected version
|
||||
|
||||
Args:
|
||||
dest_version (int): Version to migrate to
|
||||
"""
|
||||
while True:
|
||||
version = self.get_schema_version()
|
||||
if version == dest_version:
|
||||
break
|
||||
|
||||
self.close()
|
||||
tools.write_file(f'{self.db_path}old.v{version}',
|
||||
tools.read_file(self.db_path))
|
||||
|
||||
version += 1
|
||||
tout.info(f'Update database to v{version}')
|
||||
self.open_it()
|
||||
if version == 1:
|
||||
self.create_v1()
|
||||
elif version == 2:
|
||||
self._migrate_to_v2()
|
||||
elif version == 3:
|
||||
self._migrate_to_v3()
|
||||
elif version == 4:
|
||||
self._migrate_to_v4()
|
||||
|
||||
# Save the new version if we have a schema_version table
|
||||
if version > 1:
|
||||
self.cur.execute('DELETE FROM schema_version')
|
||||
self.cur.execute(
|
||||
'INSERT INTO schema_version (version) VALUES (?)',
|
||||
(version,))
|
||||
self.commit()
|
||||
|
||||
def get_schema_version(self):
|
||||
"""Get the version of the database's schema
|
||||
|
||||
Return:
|
||||
int: Database version, 0 means there is no data; anything less than
|
||||
LATEST means the schema is out of date and must be updated
|
||||
"""
|
||||
# If there is no database at all, assume v0
|
||||
version = 0
|
||||
try:
|
||||
self.cur.execute('SELECT name FROM series')
|
||||
except sqlite3.OperationalError:
|
||||
return 0
|
||||
|
||||
# If there is no schema, assume v1
|
||||
try:
|
||||
self.cur.execute('SELECT version FROM schema_version')
|
||||
version = self.cur.fetchone()[0]
|
||||
except sqlite3.OperationalError:
|
||||
return 1
|
||||
return version
|
||||
|
||||
def execute(self, query, parameters=()):
|
||||
"""Execute a database query
|
||||
|
||||
Args:
|
||||
query (str): Query string
|
||||
parameters (list of values): Parameters to pass
|
||||
|
||||
Return:
|
||||
|
||||
"""
|
||||
return self.cur.execute(query, parameters)
|
||||
|
||||
def commit(self):
|
||||
"""Commit changes to the database"""
|
||||
self.con.commit()
|
||||
|
||||
def rollback(self):
|
||||
"""Roll back changes to the database"""
|
||||
self.con.rollback()
|
||||
|
||||
def lastrowid(self):
|
||||
"""Get the last row-ID reported by the database
|
||||
|
||||
Return:
|
||||
int: Value for lastrowid
|
||||
"""
|
||||
return self.cur.lastrowid
|
||||
|
||||
def rowcount(self):
|
||||
"""Get the row-count reported by the database
|
||||
|
||||
Return:
|
||||
int: Value for rowcount
|
||||
"""
|
||||
return self.cur.rowcount
|
||||
|
||||
def _get_series_list(self, include_archived):
|
||||
"""Get a list of Series objects from the database
|
||||
|
||||
Args:
|
||||
include_archived (bool): True to include archives series
|
||||
|
||||
Return:
|
||||
list of Series
|
||||
"""
|
||||
res = self.execute(
|
||||
'SELECT id, name, desc FROM series ' +
|
||||
('WHERE archived = 0' if not include_archived else ''))
|
||||
return [Series.from_fields(idnum=idnum, name=name, desc=desc)
|
||||
for idnum, name, desc in res.fetchall()]
|
||||
|
||||
# series functions
|
||||
|
||||
def series_get_dict_by_id(self, include_archived=False):
|
||||
"""Get a dict of Series objects from the database
|
||||
|
||||
Args:
|
||||
include_archived (bool): True to include archives series
|
||||
|
||||
Return:
|
||||
OrderedDict:
|
||||
key: series ID
|
||||
value: Series with idnum, name and desc filled out
|
||||
"""
|
||||
sdict = OrderedDict()
|
||||
for ser in self._get_series_list(include_archived):
|
||||
sdict[ser.idnum] = ser
|
||||
return sdict
|
||||
|
||||
def series_find_by_name(self, name, include_archived=False):
|
||||
"""Find a series and return its details
|
||||
|
||||
Args:
|
||||
name (str): Name to search for
|
||||
include_archived (bool): True to include archives series
|
||||
|
||||
Returns:
|
||||
idnum, or None if not found
|
||||
"""
|
||||
res = self.execute(
|
||||
'SELECT id FROM series WHERE name = ?' +
|
||||
('AND archived = 0' if not include_archived else ''), (name,))
|
||||
recs = res.fetchall()
|
||||
|
||||
# This shouldn't happen
|
||||
assert len(recs) <= 1, 'Expected one match, but multiple found'
|
||||
|
||||
if len(recs) != 1:
|
||||
return None
|
||||
return recs[0][0]
|
||||
|
||||
def series_get_info(self, idnum):
|
||||
"""Get information for a series from the database
|
||||
|
||||
Args:
|
||||
idnum (int): Series ID to look up
|
||||
|
||||
Return: tuple:
|
||||
str: Series name
|
||||
str: Series description
|
||||
|
||||
Raises:
|
||||
ValueError: Series is not found
|
||||
"""
|
||||
res = self.execute('SELECT name, desc FROM series WHERE id = ?',
|
||||
(idnum,))
|
||||
recs = res.fetchall()
|
||||
if len(recs) != 1:
|
||||
raise ValueError(f'No series found (id {idnum} len {len(recs)})')
|
||||
return recs[0]
|
||||
|
||||
def series_get_dict(self, include_archived=False):
|
||||
"""Get a dict of Series objects from the database
|
||||
|
||||
Args:
|
||||
include_archived (bool): True to include archives series
|
||||
|
||||
Return:
|
||||
OrderedDict:
|
||||
key: series name
|
||||
value: Series with idnum, name and desc filled out
|
||||
"""
|
||||
sdict = OrderedDict()
|
||||
for ser in self._get_series_list(include_archived):
|
||||
sdict[ser.name] = ser
|
||||
return sdict
|
||||
|
||||
def series_get_version_list(self, series_idnum):
|
||||
"""Get a list of the versions available for a series
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID of series to look up
|
||||
|
||||
Return:
|
||||
str: List of versions, which may be empty if the series is in the
|
||||
process of being added
|
||||
"""
|
||||
res = self.execute('SELECT version FROM ser_ver WHERE series_id = ?',
|
||||
(series_idnum,))
|
||||
return [x[0] for x in res.fetchall()]
|
||||
|
||||
def series_get_max_version(self, series_idnum):
|
||||
"""Get the highest version number available for a series
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID of series to look up
|
||||
|
||||
Return:
|
||||
int: Maximum version number
|
||||
"""
|
||||
res = self.execute(
|
||||
'SELECT MAX(version) FROM ser_ver WHERE series_id = ?',
|
||||
(series_idnum,))
|
||||
return res.fetchall()[0][0]
|
||||
|
||||
def series_get_all_max_versions(self):
|
||||
"""Find the latest version of all series
|
||||
|
||||
Return: list of:
|
||||
int: ser_ver ID
|
||||
int: series ID
|
||||
int: Maximum version
|
||||
"""
|
||||
res = self.execute(
|
||||
'SELECT id, series_id, MAX(version) FROM ser_ver '
|
||||
'GROUP BY series_id')
|
||||
return res.fetchall()
|
||||
|
||||
def series_add(self, name, desc):
|
||||
"""Add a new series record
|
||||
|
||||
The new record is set to not archived
|
||||
|
||||
Args:
|
||||
name (str): Series name
|
||||
desc (str): Series description
|
||||
|
||||
Return:
|
||||
int: ID num of the new series record
|
||||
"""
|
||||
self.execute(
|
||||
'INSERT INTO series (name, desc, archived) '
|
||||
f"VALUES ('{name}', '{desc}', 0)")
|
||||
return self.lastrowid()
|
||||
|
||||
def series_remove(self, idnum):
|
||||
"""Remove a series from the database
|
||||
|
||||
The series must exist
|
||||
|
||||
Args:
|
||||
idnum (int): ID num of series to remove
|
||||
"""
|
||||
self.execute('DELETE FROM series WHERE id = ?', (idnum,))
|
||||
assert self.rowcount() == 1
|
||||
|
||||
def series_remove_by_name(self, name):
|
||||
"""Remove a series from the database
|
||||
|
||||
Args:
|
||||
name (str): Name of series to remove
|
||||
|
||||
Raises:
|
||||
ValueError: Series does not exist (database is rolled back)
|
||||
"""
|
||||
self.execute('DELETE FROM series WHERE name = ?', (name,))
|
||||
if self.rowcount() != 1:
|
||||
self.rollback()
|
||||
raise ValueError(f"No such series '{name}'")
|
||||
|
||||
def series_set_archived(self, series_idnum, archived):
|
||||
"""Update archive flag for a series
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID num of the series
|
||||
archived (bool): Whether to mark the series as archived or
|
||||
unarchived
|
||||
"""
|
||||
self.execute(
|
||||
'UPDATE series SET archived = ? WHERE id = ?',
|
||||
(archived, series_idnum))
|
||||
|
||||
def series_set_name(self, series_idnum, name):
|
||||
"""Update name for a series
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID num of the series
|
||||
name (str): new name to use
|
||||
"""
|
||||
self.execute(
|
||||
'UPDATE series SET name = ? WHERE id = ?', (name, series_idnum))
|
||||
|
||||
# ser_ver functions
|
||||
|
||||
def ser_ver_get_link(self, series_idnum, version):
|
||||
"""Get the link for a series version
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID num of the series
|
||||
version (int): Version number to search for
|
||||
|
||||
Return:
|
||||
str: Patchwork link as a string, e.g. '12325', or None if none
|
||||
|
||||
Raises:
|
||||
ValueError: Multiple matches are found
|
||||
"""
|
||||
res = self.execute(
|
||||
'SELECT link FROM ser_ver WHERE '
|
||||
f"series_id = {series_idnum} AND version = '{version}'")
|
||||
recs = res.fetchall()
|
||||
if not recs:
|
||||
return None
|
||||
if len(recs) > 1:
|
||||
raise ValueError('Expected one match, but multiple matches found')
|
||||
return recs[0][0]
|
||||
|
||||
def ser_ver_set_link(self, series_idnum, version, link):
|
||||
"""Set the link for a series version
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID num of the series
|
||||
version (int): Version number to search for
|
||||
link (str): Patchwork link for the ser_ver
|
||||
|
||||
Return:
|
||||
bool: True if the record was found and updated, else False
|
||||
"""
|
||||
if link is None:
|
||||
link = ''
|
||||
self.execute(
|
||||
'UPDATE ser_ver SET link = ? WHERE series_id = ? AND version = ?',
|
||||
(str(link), series_idnum, version))
|
||||
return self.rowcount() != 0
|
||||
|
||||
def ser_ver_set_info(self, info):
|
||||
"""Set the info for a series version
|
||||
|
||||
Args:
|
||||
info (SER_VER): Info to set. Only two options are supported:
|
||||
1: svid,cover_id,cover_num_comments,name
|
||||
2: svid,name
|
||||
|
||||
Return:
|
||||
bool: True if the record was found and updated, else False
|
||||
"""
|
||||
assert info.idnum is not None
|
||||
if info.cover_id:
|
||||
assert info.series_id is None
|
||||
self.execute(
|
||||
'UPDATE ser_ver SET cover_id = ?, cover_num_comments = ?, '
|
||||
'name = ? WHERE id = ?',
|
||||
(info.cover_id, info.cover_num_comments, info.name,
|
||||
info.idnum))
|
||||
else:
|
||||
assert not info.cover_id
|
||||
assert not info.cover_num_comments
|
||||
assert not info.series_id
|
||||
assert not info.version
|
||||
assert not info.link
|
||||
self.execute('UPDATE ser_ver SET name = ? WHERE id = ?',
|
||||
(info.name, info.idnum))
|
||||
|
||||
return self.rowcount() != 0
|
||||
|
||||
def ser_ver_set_version(self, svid, version):
|
||||
"""Sets the version for a ser_ver record
|
||||
|
||||
Args:
|
||||
svid (int): Record ID to update
|
||||
version (int): Version number to add
|
||||
|
||||
Raises:
|
||||
ValueError: svid was not found
|
||||
"""
|
||||
self.execute(
|
||||
'UPDATE ser_ver SET version = ? WHERE id = ?', (version, svid))
|
||||
if self.rowcount() != 1:
|
||||
raise ValueError(f'No ser_ver updated (svid {svid})')
|
||||
|
||||
def ser_ver_set_archive_tag(self, svid, tag):
|
||||
"""Sets the archive tag for a ser_ver record
|
||||
|
||||
Args:
|
||||
svid (int): Record ID to update
|
||||
tag (tag): Tag to add
|
||||
|
||||
Raises:
|
||||
ValueError: svid was not found
|
||||
"""
|
||||
self.execute(
|
||||
'UPDATE ser_ver SET archive_tag = ? WHERE id = ?', (tag, svid))
|
||||
if self.rowcount() != 1:
|
||||
raise ValueError(f'No ser_ver updated (svid {svid})')
|
||||
|
||||
def ser_ver_add(self, series_idnum, version, link=None):
|
||||
"""Add a new ser_ver record
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID num of the series which is getting a new
|
||||
version
|
||||
version (int): Version number to add
|
||||
link (str): Patchwork link, or None if not known
|
||||
|
||||
Return:
|
||||
int: ID num of the new ser_ver record
|
||||
"""
|
||||
self.execute(
|
||||
'INSERT INTO ser_ver (series_id, version, link) VALUES (?, ?, ?)',
|
||||
(series_idnum, version, link))
|
||||
return self.lastrowid()
|
||||
|
||||
def ser_ver_get_for_series(self, series_idnum, version=None):
|
||||
"""Get a list of ser_ver records for a given series ID
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID num of the series to search
|
||||
version (int): Version number to search for, or None for all
|
||||
|
||||
Return:
|
||||
SER_VER: Requested information
|
||||
|
||||
Raises:
|
||||
ValueError: There is no matching idnum/version
|
||||
"""
|
||||
base = ('SELECT id, series_id, version, link, cover_id, '
|
||||
'cover_num_comments, name, archive_tag FROM ser_ver '
|
||||
'WHERE series_id = ?')
|
||||
if version:
|
||||
res = self.execute(base + ' AND version = ?',
|
||||
(series_idnum, version))
|
||||
else:
|
||||
res = self.execute(base, (series_idnum,))
|
||||
recs = res.fetchall()
|
||||
if not recs:
|
||||
raise ValueError(
|
||||
f'No matching series for id {series_idnum} version {version}')
|
||||
if version:
|
||||
return SerVer(*recs[0])
|
||||
return [SerVer(*x) for x in recs]
|
||||
|
||||
def ser_ver_get_ids_for_series(self, series_idnum, version=None):
|
||||
"""Get a list of ser_ver records for a given series ID
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID num of the series to search
|
||||
version (int): Version number to search for, or None for all
|
||||
|
||||
Return:
|
||||
list of int: List of svids for the matching records
|
||||
"""
|
||||
if version:
|
||||
res = self.execute(
|
||||
'SELECT id FROM ser_ver WHERE series_id = ? AND version = ?',
|
||||
(series_idnum, version))
|
||||
else:
|
||||
res = self.execute(
|
||||
'SELECT id FROM ser_ver WHERE series_id = ?', (series_idnum,))
|
||||
return list(res.fetchall()[0])
|
||||
|
||||
def ser_ver_get_list(self):
|
||||
"""Get a list of patchwork entries from the database
|
||||
|
||||
Return:
|
||||
list of SER_VER
|
||||
"""
|
||||
res = self.execute(
|
||||
'SELECT id, series_id, version, link, cover_id, '
|
||||
'cover_num_comments, name, archive_tag FROM ser_ver')
|
||||
items = res.fetchall()
|
||||
return [SerVer(*x) for x in items]
|
||||
|
||||
def ser_ver_remove(self, series_idnum, version=None, remove_pcommits=True,
|
||||
remove_series=True):
|
||||
"""Delete a ser_ver record
|
||||
|
||||
Removes the record which has the given series ID num and version
|
||||
|
||||
Args:
|
||||
series_idnum (int): ID num of the series
|
||||
version (int): Version number, or None to remove all versions
|
||||
remove_pcommits (bool): True to remove associated pcommits too
|
||||
remove_series (bool): True to remove the series if versions is None
|
||||
"""
|
||||
if remove_pcommits:
|
||||
# Figure out svids to delete
|
||||
svids = self.ser_ver_get_ids_for_series(series_idnum, version)
|
||||
|
||||
self.pcommit_delete_list(svids)
|
||||
|
||||
if version:
|
||||
self.execute(
|
||||
'DELETE FROM ser_ver WHERE series_id = ? AND version = ?',
|
||||
(series_idnum, version))
|
||||
else:
|
||||
self.execute(
|
||||
'DELETE FROM ser_ver WHERE series_id = ?',
|
||||
(series_idnum,))
|
||||
if not version and remove_series:
|
||||
self.series_remove(series_idnum)
|
||||
|
||||
# pcommit functions
|
||||
|
||||
def pcommit_get_list(self, find_svid=None):
|
||||
"""Get a dict of pcommits entries from the database
|
||||
|
||||
Args:
|
||||
find_svid (int): If not None, finds the records associated with a
|
||||
particular series and version; otherwise returns all records
|
||||
|
||||
Return:
|
||||
list of PCOMMIT: pcommit records
|
||||
"""
|
||||
query = ('SELECT id, seq, subject, svid, change_id, state, patch_id, '
|
||||
'num_comments FROM pcommit')
|
||||
if find_svid is not None:
|
||||
query += f' WHERE svid = {find_svid}'
|
||||
res = self.execute(query)
|
||||
return [Pcommit(*rec) for rec in res.fetchall()]
|
||||
|
||||
def pcommit_add_list(self, svid, pcommits):
|
||||
"""Add records to the pcommit table
|
||||
|
||||
Args:
|
||||
svid (int): ser_ver ID num
|
||||
pcommits (list of PCOMMIT): Only seq, subject, change_id are
|
||||
uses; svid comes from the argument passed in and the others
|
||||
are assumed to be obtained from patchwork later
|
||||
"""
|
||||
for pcm in pcommits:
|
||||
self.execute(
|
||||
'INSERT INTO pcommit (svid, seq, subject, change_id) VALUES '
|
||||
'(?, ?, ?, ?)', (svid, pcm.seq, pcm.subject, pcm.change_id))
|
||||
|
||||
def pcommit_delete(self, svid):
|
||||
"""Delete pcommit records for a given ser_ver ID
|
||||
|
||||
Args_:
|
||||
svid (int): ser_ver ID num of records to delete
|
||||
"""
|
||||
self.execute('DELETE FROM pcommit WHERE svid = ?', (svid,))
|
||||
|
||||
def pcommit_delete_list(self, svid_list):
|
||||
"""Delete pcommit records for a given set of ser_ver IDs
|
||||
|
||||
Args_:
|
||||
svid (list int): ser_ver ID nums of records to delete
|
||||
"""
|
||||
vals = ', '.join([str(x) for x in svid_list])
|
||||
self.execute('DELETE FROM pcommit WHERE svid IN (?)', (vals,))
|
||||
|
||||
def pcommit_update(self, pcm):
|
||||
"""Update a pcommit record
|
||||
|
||||
Args:
|
||||
pcm (PCOMMIT): Information to write; only the idnum, state,
|
||||
patch_id and num_comments are used
|
||||
|
||||
Return:
|
||||
True if the data was written
|
||||
"""
|
||||
self.execute(
|
||||
'UPDATE pcommit SET '
|
||||
'patch_id = ?, state = ?, num_comments = ? WHERE id = ?',
|
||||
(pcm.patch_id, pcm.state, pcm.num_comments, pcm.idnum))
|
||||
return self.rowcount() > 0
|
||||
|
||||
# upstream functions
|
||||
|
||||
def upstream_add(self, name, url):
|
||||
"""Add a new upstream record
|
||||
|
||||
Args:
|
||||
name (str): Name of the tree
|
||||
url (str): URL for the tree
|
||||
|
||||
Raises:
|
||||
ValueError if the name already exists in the database
|
||||
"""
|
||||
try:
|
||||
self.execute(
|
||||
'INSERT INTO upstream (name, url) VALUES (?, ?)', (name, url))
|
||||
except sqlite3.IntegrityError as exc:
|
||||
if 'UNIQUE constraint failed: upstream.name' in str(exc):
|
||||
raise ValueError(f"Upstream '{name}' already exists") from exc
|
||||
|
||||
def upstream_set_default(self, name):
|
||||
"""Mark (only) the given upstream as the default
|
||||
|
||||
Args:
|
||||
name (str): Name of the upstream remote to set as default, or None
|
||||
|
||||
Raises:
|
||||
ValueError if more than one name matches (should not happen);
|
||||
database is rolled back
|
||||
"""
|
||||
self.execute("UPDATE upstream SET is_default = 0")
|
||||
if name is not None:
|
||||
self.execute(
|
||||
'UPDATE upstream SET is_default = 1 WHERE name = ?', (name,))
|
||||
if self.rowcount() != 1:
|
||||
self.rollback()
|
||||
raise ValueError(f"No such upstream '{name}'")
|
||||
|
||||
def upstream_get_default(self):
|
||||
"""Get the name of the default upstream
|
||||
|
||||
Return:
|
||||
str: Default-upstream name, or None if there is no default
|
||||
"""
|
||||
res = self.execute(
|
||||
"SELECT name FROM upstream WHERE is_default = 1")
|
||||
recs = res.fetchall()
|
||||
if len(recs) != 1:
|
||||
return None
|
||||
return recs[0][0]
|
||||
|
||||
def upstream_delete(self, name):
|
||||
"""Delete an upstream target
|
||||
|
||||
Args:
|
||||
name (str): Name of the upstream remote to delete
|
||||
|
||||
Raises:
|
||||
ValueError: Upstream does not exist (database is rolled back)
|
||||
"""
|
||||
self.execute(f"DELETE FROM upstream WHERE name = '{name}'")
|
||||
if self.rowcount() != 1:
|
||||
self.rollback()
|
||||
raise ValueError(f"No such upstream '{name}'")
|
||||
|
||||
def upstream_get_dict(self):
|
||||
"""Get a list of upstream entries from the database
|
||||
|
||||
Return:
|
||||
OrderedDict:
|
||||
key (str): upstream name
|
||||
value (str): url
|
||||
"""
|
||||
res = self.execute('SELECT name, url, is_default FROM upstream')
|
||||
udict = OrderedDict()
|
||||
for name, url, is_default in res.fetchall():
|
||||
udict[name] = url, is_default
|
||||
return udict
|
||||
|
||||
# settings functions
|
||||
|
||||
def settings_update(self, name, proj_id, link_name):
|
||||
"""Set the patchwork settings of the project
|
||||
|
||||
Args:
|
||||
name (str): Name of the project to use in patchwork
|
||||
proj_id (int): Project ID for the project
|
||||
link_name (str): Link name for the project
|
||||
"""
|
||||
self.execute('DELETE FROM settings')
|
||||
self.execute(
|
||||
'INSERT INTO settings (name, proj_id, link_name) '
|
||||
'VALUES (?, ?, ?)', (name, proj_id, link_name))
|
||||
|
||||
def settings_get(self):
|
||||
"""Get the patchwork settings of the project
|
||||
|
||||
Returns:
|
||||
tuple or None if there are no settings:
|
||||
name (str): Project name, e.g. 'U-Boot'
|
||||
proj_id (int): Patchworks project ID for this project
|
||||
link_name (str): Patchwork's link-name for the project
|
||||
"""
|
||||
res = self.execute("SELECT name, proj_id, link_name FROM settings")
|
||||
recs = res.fetchall()
|
||||
if len(recs) != 1:
|
||||
return None
|
||||
return recs[0]
|
||||
@@ -1,852 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0+
|
||||
#
|
||||
# Copyright 2025 Simon Glass <sjg@chromium.org>
|
||||
#
|
||||
"""Provides a basic API for the patchwork server
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import aiohttp
|
||||
from collections import namedtuple
|
||||
|
||||
from u_boot_pylib import terminal
|
||||
|
||||
# Information passed to series_get_states()
|
||||
# link (str): Patchwork link for series
|
||||
# series_id (int): Series ID in database
|
||||
# series_name (str): Series name
|
||||
# version (int): Version number of series
|
||||
# show_comments (bool): True to show comments
|
||||
# show_cover_comments (bool): True to show cover-letter comments
|
||||
STATE_REQ = namedtuple(
|
||||
'state_req',
|
||||
'link,series_id,series_name,version,show_comments,show_cover_comments')
|
||||
|
||||
# Responses from series_get_states()
|
||||
# int: ser_ver ID number
|
||||
# COVER: Cover-letter info
|
||||
# list of Patch: Information on each patch in the series
|
||||
# list of dict: patches, see get_series()['patches']
|
||||
STATE_RESP = namedtuple('state_resp', 'svid,cover,patches,patch_list')
|
||||
|
||||
# Information about a cover-letter on patchwork
|
||||
# id (int): Patchwork ID of cover letter
|
||||
# state (str): Current state, e.g. 'accepted'
|
||||
# num_comments (int): Number of comments
|
||||
# name (str): Series name
|
||||
# comments (list of dict): Comments
|
||||
COVER = namedtuple('cover', 'id,num_comments,name,comments')
|
||||
|
||||
# Number of retries
|
||||
RETRIES = 3
|
||||
|
||||
# Max concurrent request
|
||||
MAX_CONCURRENT = 50
|
||||
|
||||
# Patches which are part of a multi-patch series are shown with a prefix like
|
||||
# [prefix, version, sequence], for example '[RFC, v2, 3/5]'. All but the last
|
||||
# part is optional. This decodes the string into groups. For single patches
|
||||
# the [] part is not present:
|
||||
# Groups: (ignore, ignore, ignore, prefix, version, sequence, subject)
|
||||
RE_PATCH = re.compile(r'(\[(((.*),)?(.*),)?(.*)\]\s)?(.*)$')
|
||||
|
||||
# This decodes the sequence string into a patch number and patch count
|
||||
RE_SEQ = re.compile(r'(\d+)/(\d+)')
|
||||
|
||||
|
||||
class Patch(dict):
|
||||
"""Models a patch in patchwork
|
||||
|
||||
This class records information obtained from patchwork
|
||||
|
||||
Some of this information comes from the 'Patch' column:
|
||||
|
||||
[RFC,v2,1/3] dm: Driver and uclass changes for tiny-dm
|
||||
|
||||
This shows the prefix, version, seq, count and subject.
|
||||
|
||||
The other properties come from other columns in the display.
|
||||
|
||||
Properties:
|
||||
pid (str): ID of the patch (typically an integer)
|
||||
seq (int): Sequence number within series (1=first) parsed from sequence
|
||||
string
|
||||
count (int): Number of patches in series, parsed from sequence string
|
||||
raw_subject (str): Entire subject line, e.g.
|
||||
"[1/2,v2] efi_loader: Sort header file ordering"
|
||||
prefix (str): Prefix string or None (e.g. 'RFC')
|
||||
version (str): Version string or None (e.g. 'v2')
|
||||
raw_subject (str): Raw patch subject
|
||||
subject (str): Patch subject with [..] part removed (same as commit
|
||||
subject)
|
||||
data (dict or None): Patch data:
|
||||
"""
|
||||
def __init__(self, pid, state=None, data=None, comments=None,
|
||||
series_data=None):
|
||||
super().__init__()
|
||||
self.id = pid # Use 'id' to match what the Rest API provides
|
||||
self.seq = None
|
||||
self.count = None
|
||||
self.prefix = None
|
||||
self.version = None
|
||||
self.raw_subject = None
|
||||
self.subject = None
|
||||
self.state = state
|
||||
self.data = data
|
||||
self.comments = comments
|
||||
self.series_data = series_data
|
||||
self.name = None
|
||||
|
||||
# These make us more like a dictionary
|
||||
def __setattr__(self, name, value):
|
||||
self[name] = value
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self[name]
|
||||
|
||||
def __hash__(self):
|
||||
return hash(frozenset(self.items()))
|
||||
|
||||
def __str__(self):
|
||||
return self.raw_subject
|
||||
|
||||
def parse_subject(self, raw_subject):
|
||||
"""Parse the subject of a patch into its component parts
|
||||
|
||||
See RE_PATCH for details. The parsed info is placed into seq, count,
|
||||
prefix, version, subject
|
||||
|
||||
Args:
|
||||
raw_subject (str): Subject string to parse
|
||||
|
||||
Raises:
|
||||
ValueError: the subject cannot be parsed
|
||||
"""
|
||||
self.raw_subject = raw_subject.strip()
|
||||
mat = RE_PATCH.search(raw_subject.strip())
|
||||
if not mat:
|
||||
raise ValueError(f"Cannot parse subject '{raw_subject}'")
|
||||
self.prefix, self.version, seq_info, self.subject = mat.groups()[3:]
|
||||
mat_seq = RE_SEQ.match(seq_info) if seq_info else False
|
||||
if mat_seq is None:
|
||||
self.version = seq_info
|
||||
seq_info = None
|
||||
if self.version and not self.version.startswith('v'):
|
||||
self.prefix = self.version
|
||||
self.version = None
|
||||
if seq_info:
|
||||
if mat_seq:
|
||||
self.seq = int(mat_seq.group(1))
|
||||
self.count = int(mat_seq.group(2))
|
||||
else:
|
||||
self.seq = 1
|
||||
self.count = 1
|
||||
|
||||
|
||||
class Review:
|
||||
"""Represents a single review email collected in Patchwork
|
||||
|
||||
Patches can attract multiple reviews. Each consists of an author/date and
|
||||
a variable number of 'snippets', which are groups of quoted and unquoted
|
||||
text.
|
||||
"""
|
||||
def __init__(self, meta, snippets):
|
||||
"""Create new Review object
|
||||
|
||||
Args:
|
||||
meta (str): Text containing review author and date
|
||||
snippets (list): List of snippets in th review, each a list of text
|
||||
lines
|
||||
"""
|
||||
self.meta = ' : '.join([line for line in meta.splitlines() if line])
|
||||
self.snippets = snippets
|
||||
|
||||
|
||||
class Patchwork:
|
||||
"""Class to handle communication with patchwork
|
||||
"""
|
||||
def __init__(self, url, show_progress=True, single_thread=False):
|
||||
"""Set up a new patchwork handler
|
||||
|
||||
Args:
|
||||
url (str): URL of patchwork server, e.g.
|
||||
'https://patchwork.ozlabs.org'
|
||||
"""
|
||||
self.url = url
|
||||
self.fake_request = None
|
||||
self.proj_id = None
|
||||
self.link_name = None
|
||||
self._show_progress = show_progress
|
||||
self.semaphore = asyncio.Semaphore(
|
||||
1 if single_thread else MAX_CONCURRENT)
|
||||
self.request_count = 0
|
||||
|
||||
async def _request(self, client, subpath):
|
||||
"""Call the patchwork API and return the result as JSON
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
subpath (str): URL subpath to use
|
||||
|
||||
Returns:
|
||||
dict: Json result
|
||||
|
||||
Raises:
|
||||
ValueError: the URL could not be read
|
||||
"""
|
||||
# print('subpath', subpath)
|
||||
self.request_count += 1
|
||||
if self.fake_request:
|
||||
return self.fake_request(subpath)
|
||||
|
||||
full_url = f'{self.url}/api/1.2/{subpath}'
|
||||
async with self.semaphore:
|
||||
# print('full_url', full_url)
|
||||
for i in range(RETRIES + 1):
|
||||
try:
|
||||
async with client.get(full_url) as response:
|
||||
if response.status != 200:
|
||||
raise ValueError(
|
||||
f"Could not read URL '{full_url}'")
|
||||
result = await response.json()
|
||||
# print('- done', full_url)
|
||||
return result
|
||||
break
|
||||
except aiohttp.client_exceptions.ServerDisconnectedError:
|
||||
if i == RETRIES:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def for_testing(func):
|
||||
"""Get an instance to use for testing
|
||||
|
||||
Args:
|
||||
func (function): Function to call to handle requests. The function
|
||||
is passed a URL and is expected to return a dict with the
|
||||
resulting data
|
||||
|
||||
Returns:
|
||||
Patchwork: testing instance
|
||||
"""
|
||||
pwork = Patchwork(None, show_progress=False)
|
||||
pwork.fake_request = func
|
||||
return pwork
|
||||
|
||||
class _Stats:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.request_count = 0
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.request_count = self.parent.request_count
|
||||
|
||||
def collect_stats(self):
|
||||
"""Context manager to count requests across a range of patchwork calls
|
||||
|
||||
Usage:
|
||||
pwork = Patchwork(...)
|
||||
with pwork.count_requests() as counter:
|
||||
pwork.something()
|
||||
print(f'{counter.count} requests')
|
||||
"""
|
||||
self.request_count = 0
|
||||
return self._Stats(self)
|
||||
|
||||
async def get_projects(self):
|
||||
"""Get a list of projects on the server
|
||||
|
||||
Returns:
|
||||
list of dict, one for each project
|
||||
'name' (str): Project name, e.g. 'U-Boot'
|
||||
'id' (int): Project ID, e.g. 9
|
||||
'link_name' (str): Project's link-name, e.g. 'uboot'
|
||||
"""
|
||||
async with aiohttp.ClientSession() as client:
|
||||
return await self._request(client, 'projects/')
|
||||
|
||||
async def _query_series(self, client, desc):
|
||||
"""Query series by name
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
desc: String to search for
|
||||
|
||||
Return:
|
||||
list of series matches, each a dict, see get_series()
|
||||
"""
|
||||
query = desc.replace(' ', '+')
|
||||
return await self._request(
|
||||
client, f'series/?project={self.proj_id}&q={query}')
|
||||
|
||||
async def _find_series(self, client, svid, ser_id, version, ser):
|
||||
"""Find a series on the server
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
svid (int): ser_ver ID
|
||||
ser_id (int): series ID
|
||||
version (int): Version number to search for
|
||||
ser (Series): Contains description (cover-letter title)
|
||||
|
||||
Returns:
|
||||
tuple:
|
||||
int: ser_ver ID (as passed in)
|
||||
int: series ID (as passed in)
|
||||
str: Series link, or None if not found
|
||||
list of dict, or None if found
|
||||
each dict is the server result from a possible series
|
||||
"""
|
||||
desc = ser.desc
|
||||
name_found = []
|
||||
|
||||
# Do a series query on the description
|
||||
res = await self._query_series(client, desc)
|
||||
for pws in res:
|
||||
if pws['name'] == desc:
|
||||
if int(pws['version']) == version:
|
||||
return svid, ser_id, pws['id'], None
|
||||
name_found.append(pws)
|
||||
|
||||
# When there is no cover letter, patchwork uses the first patch as the
|
||||
# series name
|
||||
cmt = ser.commits[0]
|
||||
|
||||
res = await self._query_series(client, cmt.subject)
|
||||
for pws in res:
|
||||
patch = Patch(0)
|
||||
patch.parse_subject(pws['name'])
|
||||
if patch.subject == cmt.subject:
|
||||
if int(pws['version']) == version:
|
||||
return svid, ser_id, pws['id'], None
|
||||
name_found.append(pws)
|
||||
|
||||
return svid, ser_id, None, name_found or res
|
||||
|
||||
async def find_series(self, ser, version):
|
||||
"""Find a series based on its description and version
|
||||
|
||||
Args:
|
||||
ser (Series): Contains description (cover-letter title)
|
||||
version (int): Version number
|
||||
|
||||
Return: tuple:
|
||||
tuple:
|
||||
str: Series ID, or None if not found
|
||||
list of dict, or None if found
|
||||
each dict is the server result from a possible series
|
||||
int: number of server requests done
|
||||
"""
|
||||
async with aiohttp.ClientSession() as client:
|
||||
# We don't know the svid and it isn't needed, so use -1
|
||||
_, _, link, options = await self._find_series(client, -1, -1,
|
||||
version, ser)
|
||||
return link, options
|
||||
|
||||
async def find_series_list(self, to_find):
|
||||
"""Find the link for each series in a list
|
||||
|
||||
Args:
|
||||
to_find (dict of svids to sync):
|
||||
key (int): ser_ver ID
|
||||
value (tuple):
|
||||
int: Series ID
|
||||
int: Series version
|
||||
str: Series link
|
||||
str: Series description
|
||||
|
||||
Return: tuple:
|
||||
list of tuple, one for each item in to_find:
|
||||
int: ser_ver_ID
|
||||
int: series ID
|
||||
int: Series version
|
||||
str: Series link, or None if not found
|
||||
list of dict, or None if found
|
||||
each dict is the server result from a possible series
|
||||
int: number of server requests done
|
||||
"""
|
||||
self.request_count = 0
|
||||
async with aiohttp.ClientSession() as client:
|
||||
tasks = [asyncio.create_task(
|
||||
self._find_series(client, svid, ser_id, version, desc))
|
||||
for svid, (ser_id, version, link, desc) in to_find.items()]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
return results, self.request_count
|
||||
|
||||
def project_set(self, project_id, link_name):
|
||||
"""Set the project ID
|
||||
|
||||
The patchwork server has multiple projects. This allows the ID and
|
||||
link_name of the relevant project to be selected
|
||||
|
||||
This function is used for testing
|
||||
|
||||
Args:
|
||||
project_id (int): Project ID to use, e.g. 6
|
||||
link_name (str): Name to use for project URL links, e.g. 'uboot'
|
||||
"""
|
||||
self.proj_id = project_id
|
||||
self.link_name = link_name
|
||||
|
||||
async def get_series(self, client, link):
|
||||
"""Read information about a series
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
link (str): Patchwork series ID
|
||||
|
||||
Returns: dict containing patchwork's series information
|
||||
id (int): series ID unique across patchwork instance, e.g. 3
|
||||
url (str): Full URL, e.g.
|
||||
'https://patchwork.ozlabs.org/api/1.2/series/3/'
|
||||
web_url (str): Full URL, e.g.
|
||||
'https://patchwork.ozlabs.org/project/uboot/list/?series=3
|
||||
project (dict): project information (id, url, name, link_name,
|
||||
list_id, list_email, etc.
|
||||
name (str): Series name, e.g. '[U-Boot] moveconfig: fix error'
|
||||
date (str): Date, e.g. '2017-08-27T08:00:51'
|
||||
submitter (dict): id, url, name, email, e.g.:
|
||||
"id": 6125,
|
||||
"url": "https://patchwork.ozlabs.org/api/1.2/people/6125/",
|
||||
"name": "Chris Packham",
|
||||
"email": "judge.packham@gmail.com"
|
||||
version (int): Version number
|
||||
total (int): Total number of patches based on subject
|
||||
received_total (int): Total patches received by patchwork
|
||||
received_all (bool): True if all patches were received
|
||||
mbox (str): URL of mailbox, e.g.
|
||||
'https://patchwork.ozlabs.org/series/3/mbox/'
|
||||
cover_letter (dict) or None, e.g.:
|
||||
"id": 806215,
|
||||
"url": "https://patchwork.ozlabs.org/api/1.2/covers/806215/",
|
||||
"web_url": "https://patchwork.ozlabs.org/project/uboot/cover/
|
||||
20170827094411.8583-1-judge.packham@gmail.com/",
|
||||
"msgid": "<20170827094411.8583-1-judge.packham@gmail.com>",
|
||||
"list_archive_url": null,
|
||||
"date": "2017-08-27T09:44:07",
|
||||
"name": "[U-Boot,v2,0/4] usb: net: Migrate USB Ethernet",
|
||||
"mbox": "https://patchwork.ozlabs.org/project/uboot/cover/
|
||||
20170827094411.8583-1-judge.packham@gmail.com/mbox/"
|
||||
patches (list of dict), each e.g.:
|
||||
"id": 806202,
|
||||
"url": "https://patchwork.ozlabs.org/api/1.2/patches/806202/",
|
||||
"web_url": "https://patchwork.ozlabs.org/project/uboot/patch/
|
||||
20170827080051.816-1-judge.packham@gmail.com/",
|
||||
"msgid": "<20170827080051.816-1-judge.packham@gmail.com>",
|
||||
"list_archive_url": null,
|
||||
"date": "2017-08-27T08:00:51",
|
||||
"name": "[U-Boot] moveconfig: fix error message do_autoconf()",
|
||||
"mbox": "https://patchwork.ozlabs.org/project/uboot/patch/
|
||||
20170827080051.816-1-judge.packham@gmail.com/mbox/"
|
||||
"""
|
||||
return await self._request(client, f'series/{link}/')
|
||||
|
||||
async def get_patch(self, client, patch_id):
|
||||
"""Read information about a patch
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
patch_id (str): Patchwork patch ID
|
||||
|
||||
Returns: dict containing patchwork's patch information
|
||||
"id": 185,
|
||||
"url": "https://patchwork.ozlabs.org/api/1.2/patches/185/",
|
||||
"web_url": "https://patchwork.ozlabs.org/project/cbe-oss-dev/patch/
|
||||
200809050416.27831.adetsch@br.ibm.com/",
|
||||
project (dict): project information (id, url, name, link_name,
|
||||
list_id, list_email, etc.
|
||||
"msgid": "<200809050416.27831.adetsch@br.ibm.com>",
|
||||
"list_archive_url": null,
|
||||
"date": "2008-09-05T07:16:27",
|
||||
"name": "powerpc/spufs: Fix possible scheduling of a context",
|
||||
"commit_ref": "b2e601d14deb2083e2a537b47869ab3895d23a28",
|
||||
"pull_url": null,
|
||||
"state": "accepted",
|
||||
"archived": false,
|
||||
"hash": "bc1c0b80d7cff66c0d1e5f3f8f4d10eb36176f0d",
|
||||
"submitter": {
|
||||
"id": 93,
|
||||
"url": "https://patchwork.ozlabs.org/api/1.2/people/93/",
|
||||
"name": "Andre Detsch",
|
||||
"email": "adetsch@br.ibm.com"
|
||||
},
|
||||
"delegate": {
|
||||
"id": 1,
|
||||
"url": "https://patchwork.ozlabs.org/api/1.2/users/1/",
|
||||
"username": "jk",
|
||||
"first_name": "Jeremy",
|
||||
"last_name": "Kerr",
|
||||
"email": "jk@ozlabs.org"
|
||||
},
|
||||
"mbox": "https://patchwork.ozlabs.org/project/cbe-oss-dev/patch/
|
||||
200809050416.27831.adetsch@br.ibm.com/mbox/",
|
||||
"series": [],
|
||||
"comments": "https://patchwork.ozlabs.org/api/patches/185/
|
||||
comments/",
|
||||
"check": "pending",
|
||||
"checks": "https://patchwork.ozlabs.org/api/patches/185/checks/",
|
||||
"tags": {},
|
||||
"related": [],
|
||||
"headers": {...}
|
||||
"content": "We currently have a race when scheduling a context
|
||||
after we have found a runnable context in spusched_tick, the
|
||||
context may have been scheduled by spu_activate().
|
||||
|
||||
This may result in a panic if we try to unschedule a context
|
||||
been freed in the meantime.
|
||||
|
||||
This change exits spu_schedule() if the context has already
|
||||
scheduled, so we don't end up scheduling it twice.
|
||||
|
||||
Signed-off-by: Andre Detsch <adetsch@br.ibm.com>",
|
||||
"diff": '''Index: spufs/arch/powerpc/platforms/cell/spufs/sched.c
|
||||
=======================================================
|
||||
--- spufs.orig/arch/powerpc/platforms/cell/spufs/sched.c
|
||||
+++ spufs/arch/powerpc/platforms/cell/spufs/sched.c
|
||||
@@ -727,7 +727,8 @@ static void spu_schedule(struct spu *spu
|
||||
\t/* not a candidate for interruptible because it's called
|
||||
\t from the scheduler thread or from spu_deactivate */
|
||||
\tmutex_lock(&ctx->state_mutex);
|
||||
-\t__spu_schedule(spu, ctx);
|
||||
+\tif (ctx->state == SPU_STATE_SAVED)
|
||||
+\t\t__spu_schedule(spu, ctx);
|
||||
\tspu_release(ctx);
|
||||
}
|
||||
'''
|
||||
"prefixes": ["3/3", ...]
|
||||
"""
|
||||
return await self._request(client, f'patches/{patch_id}/')
|
||||
|
||||
async def _get_patch_comments(self, client, patch_id):
|
||||
"""Read comments about a patch
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
patch_id (str): Patchwork patch ID
|
||||
|
||||
Returns: list of dict: list of comments:
|
||||
id (int): series ID unique across patchwork instance, e.g. 3331924
|
||||
web_url (str): Full URL, e.g.
|
||||
'https://patchwork.ozlabs.org/comment/3331924/'
|
||||
msgid (str): Message ID, e.g.
|
||||
'<d2526c98-8198-4b8b-ab10-20bda0151da1@gmx.de>'
|
||||
list_archive_url: (unknown?)
|
||||
date (str): Date, e.g. '2024-06-20T13:38:03'
|
||||
subject (str): email subject, e.g. 'Re: [PATCH 3/5] buildman:
|
||||
Support building within a Python venv'
|
||||
date (str): Date, e.g. '2017-08-27T08:00:51'
|
||||
submitter (dict): id, url, name, email, e.g.:
|
||||
"id": 61270,
|
||||
"url": "https://patchwork.ozlabs.org/api/people/61270/",
|
||||
"name": "Heinrich Schuchardt",
|
||||
"email": "xypron.glpk@gmx.de"
|
||||
content (str): Content of email, e.g. 'On 20.06.24 15:19,
|
||||
Simon Glass wrote:
|
||||
>...'
|
||||
headers: dict: email headers, see get_cover() for an example
|
||||
"""
|
||||
return await self._request(client, f'patches/{patch_id}/comments/')
|
||||
|
||||
async def get_cover(self, client, cover_id):
|
||||
"""Read information about a cover letter
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
cover_id (int): Patchwork cover-letter ID
|
||||
|
||||
Returns: dict containing patchwork's cover-letter information:
|
||||
id (int): series ID unique across patchwork instance, e.g. 3
|
||||
url (str): Full URL, e.g. https://patchwork.ozlabs.org/project/uboot/list/?series=3
|
||||
project (dict): project information (id, url, name, link_name,
|
||||
list_id, list_email, etc.
|
||||
url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/api/1.2/covers/2054866/'
|
||||
web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/20250304130947.109799-1-sjg@chromium.org/'
|
||||
project (dict): project information (id, url, name, link_name,
|
||||
list_id, list_email, etc.
|
||||
msgid (str): Message ID, e.g. '20250304130947.109799-1-sjg@chromium.org>'
|
||||
list_archive_url (?)
|
||||
date (str): Date, e.g. '2017-08-27T08:00:51'
|
||||
name (str): Series name, e.g. '[U-Boot] moveconfig: fix error'
|
||||
submitter (dict): id, url, name, email, e.g.:
|
||||
"id": 6170,
|
||||
"url": "https://patchwork.ozlabs.org/api/1.2/people/6170/",
|
||||
"name": "Simon Glass",
|
||||
"email": "sjg@chromium.org"
|
||||
mbox (str): URL to mailbox, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/20250304130947.109799-1-sjg@chromium.org/mbox/'
|
||||
series (list of dict) each e.g.:
|
||||
"id": 446956,
|
||||
"url": "https://patchwork.ozlabs.org/api/1.2/series/446956/",
|
||||
"web_url": "https://patchwork.ozlabs.org/project/uboot/list/?series=446956",
|
||||
"date": "2025-03-04T13:09:37",
|
||||
"name": "binman: Check code-coverage requirements",
|
||||
"version": 1,
|
||||
"mbox": "https://patchwork.ozlabs.org/series/446956/mbox/"
|
||||
comments: Web URL to comments: 'https://patchwork.ozlabs.org/api/covers/2054866/comments/'
|
||||
headers: dict: e.g.:
|
||||
"Return-Path": "<u-boot-bounces@lists.denx.de>",
|
||||
"X-Original-To": "incoming@patchwork.ozlabs.org",
|
||||
"Delivered-To": "patchwork-incoming@legolas.ozlabs.org",
|
||||
"Authentication-Results": [
|
||||
"legolas.ozlabs.org;
|
||||
\tdkim=pass (1024-bit key;
|
||||
unprotected) header.d=chromium.org header.i=@chromium.org header.a=rsa-sha256
|
||||
header.s=google header.b=dG8yqtoK;
|
||||
\tdkim-atps=neutral",
|
||||
"legolas.ozlabs.org;
|
||||
spf=pass (sender SPF authorized) smtp.mailfrom=lists.denx.de
|
||||
(client-ip=85.214.62.61; helo=phobos.denx.de;
|
||||
envelope-from=u-boot-bounces@lists.denx.de; receiver=patchwork.ozlabs.org)",
|
||||
"phobos.denx.de;
|
||||
dmarc=pass (p=none dis=none) header.from=chromium.org",
|
||||
"phobos.denx.de;
|
||||
spf=pass smtp.mailfrom=u-boot-bounces@lists.denx.de",
|
||||
"phobos.denx.de;
|
||||
\tdkim=pass (1024-bit key;
|
||||
unprotected) header.d=chromium.org header.i=@chromium.org
|
||||
header.b=\"dG8yqtoK\";
|
||||
\tdkim-atps=neutral",
|
||||
"phobos.denx.de;
|
||||
dmarc=pass (p=none dis=none) header.from=chromium.org",
|
||||
"phobos.denx.de;
|
||||
spf=pass smtp.mailfrom=sjg@chromium.org"
|
||||
],
|
||||
"Received": [
|
||||
"from phobos.denx.de (phobos.denx.de [85.214.62.61])
|
||||
\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
|
||||
\t key-exchange X25519 server-signature ECDSA (secp384r1))
|
||||
\t(No client certificate requested)
|
||||
\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4Z6bd50jLhz1yD0
|
||||
\tfor <incoming@patchwork.ozlabs.org>; Wed, 5 Mar 2025 00:10:00 +1100 (AEDT)",
|
||||
"from h2850616.stratoserver.net (localhost [IPv6:::1])
|
||||
\tby phobos.denx.de (Postfix) with ESMTP id 434E88144A;
|
||||
\tTue, 4 Mar 2025 14:09:58 +0100 (CET)",
|
||||
"by phobos.denx.de (Postfix, from userid 109)
|
||||
id 8CBF98144A; Tue, 4 Mar 2025 14:09:57 +0100 (CET)",
|
||||
"from mail-io1-xd2e.google.com (mail-io1-xd2e.google.com
|
||||
[IPv6:2607:f8b0:4864:20::d2e])
|
||||
(using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits))
|
||||
(No client certificate requested)
|
||||
by phobos.denx.de (Postfix) with ESMTPS id 48AE281426
|
||||
for <u-boot@lists.denx.de>; Tue, 4 Mar 2025 14:09:55 +0100 (CET)",
|
||||
"by mail-io1-xd2e.google.com with SMTP id
|
||||
ca18e2360f4ac-85ae33109f6so128326139f.2
|
||||
for <u-boot@lists.denx.de>; Tue, 04 Mar 2025 05:09:55 -0800 (PST)",
|
||||
"from chromium.org (c-73-203-119-151.hsd1.co.comcast.net.
|
||||
[73.203.119.151]) by smtp.gmail.com with ESMTPSA id
|
||||
ca18e2360f4ac-858753cd304sm287383839f.33.2025.03.04.05.09.49
|
||||
(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
|
||||
Tue, 04 Mar 2025 05:09:50 -0800 (PST)"
|
||||
],
|
||||
"X-Spam-Checker-Version": "SpamAssassin 3.4.2 (2018-09-13) on phobos.denx.de",
|
||||
"X-Spam-Level": "",
|
||||
"X-Spam-Status": "No, score=-2.1 required=5.0 tests=BAYES_00,DKIMWL_WL_HIGH,
|
||||
DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,
|
||||
RCVD_IN_DNSWL_BLOCKED,SPF_HELO_NONE,SPF_PASS autolearn=ham
|
||||
autolearn_force=no version=3.4.2",
|
||||
"DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=chromium.org; s=google; t=1741093792; x=1741698592; darn=lists.denx.de;
|
||||
h=content-transfer-encoding:mime-version:message-id:date:subject:cc
|
||||
:to:from:from:to:cc:subject:date:message-id:reply-to;
|
||||
bh=B2zsLws430/BEZfatNjeaNnrcxmYUstVjp1pSXgNQjc=;
|
||||
b=dG8yqtoKpSy15RHagnPcppzR8KbFCRXa2OBwXfwGoyN6M15tOJsUu2tpCdBFYiL5Mk
|
||||
hQz5iDLV8p0Bs+fP4XtNEx7KeYfTZhiqcRFvdCLwYtGray/IHtOZaNoHLajrstic/OgE
|
||||
01ymu6gOEboU32eQ8uC8pdCYQ4UCkfKJwmiiU=",
|
||||
"X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;
|
||||
d=1e100.net; s=20230601; t=1741093792; x=1741698592;
|
||||
h=content-transfer-encoding:mime-version:message-id:date:subject:cc
|
||||
:to:from:x-gm-message-state:from:to:cc:subject:date:message-id
|
||||
:reply-to;
|
||||
bh=B2zsLws430/BEZfatNjeaNnrcxmYUstVjp1pSXgNQjc=;
|
||||
b=eihzJf4i9gin9usvz4hnAvvbLV9/yB7hGPpwwW/amgnPUyWCeQstgvGL7WDLYYnukH
|
||||
161p4mt7+cCj7Hao/jSPvVZeuKiBNPkS4YCuP3QjXfdk2ziQ9IjloVmGarWZUOlYJ5iQ
|
||||
dZnxypUkuFfLcEDSwUmRO1dvLi3nH8PDlae3yT2H87LeHaxhXWdzHxQdPc86rkYyCqCr
|
||||
qBC2CTS31jqSuiaI+7qB3glvbJbSEXkunz0iDewTJDvZfmuloxTipWUjRJ1mg9UJcZt5
|
||||
9xIuTq1n9aYf1RcQlrEOQhdBAQ0/IJgvmZtzPZi9L+ppBva1ER/xm06nMA7GEUtyGwun
|
||||
c6pA==",
|
||||
"X-Gm-Message-State": "AOJu0Yybx3b1+yClf/IfIbQd9u8sxzK9ixPP2HimXF/dGZfSiS7Cb+O5
|
||||
WrAkvtp7m3KPM/Mpv0sSZ5qrfTnKnb3WZyv6Oe5Q1iUjAftGNwbSxob5eJ/0y3cgrTdzE4sIWPE
|
||||
=",
|
||||
"X-Gm-Gg": "ASbGncu5gtgpXEPGrpbTRJulqFrFj1YPAAmKk4MiXA8/3J1A+25F0Uug2KeFUrZEjkG
|
||||
KMdPg/C7e2emIvfM+Jl+mKv0ITBvhbyNCyY1q2U1s1cayZF05coZ9ewzGxXJGiEqLMG69uBmmIi
|
||||
rBEvCnkXS+HVZobDQMtOsezpc+Ju8JRA7+y1R0WIlutl1mQARct6p0zTkuZp75QyB6dm/d0KYgd
|
||||
iux/t/f0HC2CxstQlTlJYzKL6UJgkB5/UorY1lW/0NDRS6P1iemPQ7I3EPLJO8tM5ZrpJE7qgNP
|
||||
xy0jXbUv44c48qJ1VszfY5USB8fRG7nwUYxNu6N1PXv9xWbl+z2xL68qNYUrFlHsB8ILTXAyzyr
|
||||
Cdj+Sxg==",
|
||||
"X-Google-Smtp-Source": "
|
||||
AGHT+IFeVk5D4YEfJgPxOfg3ikO6Q7IhaDzABGkAPI6HA0ubK85OPhUHK08gV7enBQ8OdoE/ttqEjw==",
|
||||
"X-Received": "by 2002:a05:6602:640f:b0:855:63c8:abb5 with SMTP id
|
||||
ca18e2360f4ac-85881fdba3amr1839428939f.13.1741093792636;
|
||||
Tue, 04 Mar 2025 05:09:52 -0800 (PST)",
|
||||
"From": "Simon Glass <sjg@chromium.org>",
|
||||
"To": "U-Boot Mailing List <u-boot@lists.denx.de>",
|
||||
"Cc": "Simon Glass <sjg@chromium.org>, Alexander Kochetkov <al.kochet@gmail.com>,
|
||||
Alper Nebi Yasak <alpernebiyasak@gmail.com>,
|
||||
Brandon Maier <brandon.maier@collins.com>,
|
||||
Jerome Forissier <jerome.forissier@linaro.org>,
|
||||
Jiaxun Yang <jiaxun.yang@flygoat.com>,
|
||||
Neha Malcom Francis <n-francis@ti.com>,
|
||||
Patrick Rudolph <patrick.rudolph@9elements.com>,
|
||||
Paul HENRYS <paul.henrys_ext@softathome.com>, Peng Fan <peng.fan@nxp.com>,
|
||||
Philippe Reynes <philippe.reynes@softathome.com>,
|
||||
Stefan Herbrechtsmeier <stefan.herbrechtsmeier@weidmueller.com>,
|
||||
Tom Rini <trini@konsulko.com>",
|
||||
"Subject": "[PATCH 0/7] binman: Check code-coverage requirements",
|
||||
"Date": "Tue, 4 Mar 2025 06:09:37 -0700",
|
||||
"Message-ID": "<20250304130947.109799-1-sjg@chromium.org>",
|
||||
"X-Mailer": "git-send-email 2.43.0",
|
||||
"MIME-Version": "1.0",
|
||||
"Content-Transfer-Encoding": "8bit",
|
||||
"X-BeenThere": "u-boot@lists.denx.de",
|
||||
"X-Mailman-Version": "2.1.39",
|
||||
"Precedence": "list",
|
||||
"List-Id": "U-Boot discussion <u-boot.lists.denx.de>",
|
||||
"List-Unsubscribe": "<https://lists.denx.de/options/u-boot>,
|
||||
<mailto:u-boot-request@lists.denx.de?subject=unsubscribe>",
|
||||
"List-Archive": "<https://lists.denx.de/pipermail/u-boot/>",
|
||||
"List-Post": "<mailto:u-boot@lists.denx.de>",
|
||||
"List-Help": "<mailto:u-boot-request@lists.denx.de?subject=help>",
|
||||
"List-Subscribe": "<https://lists.denx.de/listinfo/u-boot>,
|
||||
<mailto:u-boot-request@lists.denx.de?subject=subscribe>",
|
||||
"Errors-To": "u-boot-bounces@lists.denx.de",
|
||||
"Sender": "\"U-Boot\" <u-boot-bounces@lists.denx.de>",
|
||||
"X-Virus-Scanned": "clamav-milter 0.103.8 at phobos.denx.de",
|
||||
"X-Virus-Status": "Clean"
|
||||
content (str): Email content, e.g. 'This series adds a cover-coverage check to CI for Binman. The iMX8 tests
|
||||
are still not completed,...'
|
||||
"""
|
||||
async with aiohttp.ClientSession() as client:
|
||||
return await self._request(client, f'covers/{cover_id}/')
|
||||
|
||||
async def get_cover_comments(self, client, cover_id):
|
||||
"""Read comments about a cover letter
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
cover_id (str): Patchwork cover-letter ID
|
||||
|
||||
Returns: list of dict: list of comments, each:
|
||||
id (int): series ID unique across patchwork instance, e.g. 3472068
|
||||
web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/comment/3472068/'
|
||||
list_archive_url: (unknown?)
|
||||
|
||||
project (dict): project information (id, url, name, link_name,
|
||||
list_id, list_email, etc.
|
||||
url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/api/1.2/covers/2054866/'
|
||||
web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/20250304130947.109799-1-sjg@chromium.org/'
|
||||
project (dict): project information (id, url, name, link_name,
|
||||
list_id, list_email, etc.
|
||||
date (str): Date, e.g. '2025-03-04T13:16:15'
|
||||
subject (str): 'Re: [PATCH 0/7] binman: Check code-coverage requirements'
|
||||
submitter (dict): id, url, name, email, e.g.:
|
||||
"id": 6170,
|
||||
"url": "https://patchwork.ozlabs.org/api/people/6170/",
|
||||
"name": "Simon Glass",
|
||||
"email": "sjg@chromium.org"
|
||||
content (str): Email content, e.g. 'Hi,
|
||||
|
||||
On Tue, 4 Mar 2025 at 06:09, Simon Glass <sjg@chromium.org> wrote:
|
||||
>
|
||||
> This '...
|
||||
headers: dict: email headers, see get_cover() for an example
|
||||
"""
|
||||
return await self._request(client, f'covers/{cover_id}/comments/')
|
||||
|
||||
async def get_series_url(self, link):
|
||||
"""Get the URL for a series
|
||||
|
||||
Args:
|
||||
link (str): Patchwork series ID
|
||||
|
||||
Returns:
|
||||
str: URL for the series page
|
||||
"""
|
||||
return f'{self.url}/project/{self.link_name}/list/?series={link}&state=*&archive=both'
|
||||
|
||||
async def _get_patch_status(self, client, patch_id):
|
||||
"""Get the patch status
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
patch_id (int): Patch ID to look up in patchwork
|
||||
|
||||
Return:
|
||||
PATCH: Patch information
|
||||
|
||||
Requests:
|
||||
1 for patch, 1 for patch comments
|
||||
"""
|
||||
data = await self.get_patch(client, patch_id)
|
||||
state = data['state']
|
||||
comment_data = await self._get_patch_comments(client, patch_id)
|
||||
|
||||
return Patch(patch_id, state, data, comment_data)
|
||||
|
||||
async def get_series_cover(self, client, data):
|
||||
"""Get the cover information (including comments)
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
data (dict): Return value from self.get_series()
|
||||
|
||||
Returns:
|
||||
COVER object, or None if no cover letter
|
||||
"""
|
||||
# Patchwork should always provide this, but use get() so that we don't
|
||||
# have to provide it in our fake patchwork _fake_patchwork_cser()
|
||||
cover = data.get('cover_letter')
|
||||
cover_id = None
|
||||
if cover:
|
||||
cover_id = cover['id']
|
||||
info = await self.get_cover_comments(client, cover_id)
|
||||
cover = COVER(cover_id, len(info), cover['name'], info)
|
||||
return cover
|
||||
|
||||
async def series_get_state(self, client, link, read_comments,
|
||||
read_cover_comments):
|
||||
"""Sync the series information against patchwork, to find patch status
|
||||
|
||||
Args:
|
||||
client (aiohttp.ClientSession): Session to use
|
||||
link (str): Patchwork series ID
|
||||
read_comments (bool): True to read the comments on the patches
|
||||
read_cover_comments (bool): True to read the comments on the cover
|
||||
letter
|
||||
|
||||
Return: tuple:
|
||||
COVER object, or None if none or not read_cover_comments
|
||||
list of PATCH objects
|
||||
"""
|
||||
data = await self.get_series(client, link)
|
||||
patch_list = list(data['patches'])
|
||||
|
||||
count = len(patch_list)
|
||||
patches = []
|
||||
if read_comments:
|
||||
# Returns a list of Patch objects
|
||||
tasks = [self._get_patch_status(client, patch_list[i]['id'])
|
||||
for i in range(count)]
|
||||
|
||||
patch_status = await asyncio.gather(*tasks)
|
||||
for patch_data, status in zip(patch_list, patch_status):
|
||||
status.series_data = patch_data
|
||||
patches.append(status)
|
||||
else:
|
||||
for i in range(count):
|
||||
info = patch_list[i]
|
||||
pat = Patch(info['id'], series_data=info)
|
||||
pat.raw_subject = info['name']
|
||||
patches.append(pat)
|
||||
if self._show_progress:
|
||||
terminal.print_clear()
|
||||
|
||||
if read_cover_comments:
|
||||
cover = await self.get_series_cover(client, data)
|
||||
else:
|
||||
cover = None
|
||||
|
||||
return cover, patches
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user