diff options
author | Konstantin Ryabitsev <konstantin@linuxfoundation.org> | 2022-06-21 16:17:22 -0400 |
---|---|---|
committer | Konstantin Ryabitsev <konstantin@linuxfoundation.org> | 2022-06-21 16:17:22 -0400 |
commit | b7d71d8de67bd238f65db6d142a22d7d5a79206d (patch) | |
tree | d125ec42dfe65d63cc066d5974b1ae008c8e5c18 /b4/__init__.py | |
parent | 409c865175bf8f103bf5633331e8a1d77a446814 (diff) | |
download | b4-b7d71d8de67bd238f65db6d142a22d7d5a79206d.tar.gz |
Initial patchwork integration support
A lot of maintainers use patchwork alongside b4, to make it easier to
track patches and rely on some CI integration. This commit adds some
basic patchwork integration:
- on "b4 am", "b4 shazam", "b4 pr" we will mark the relevant patchwork
entries as "Under Review"
- on "b4 ty" we can set these patches as "Accepted"
- on "b4 ty -d" we can set them as "Deferred"
To make it work, the following entries must be present in the repository
used with b4:
[b4]
pw-key = (your API token)
pw-url = https://patchwork.kernel.org
pw-project = (your project, e.g. linux-usb)
pw-review-state = under-review
pw-accept-state = accepted
pw-discard-state = deferred
To get your patchwork API token, go to your patchwork profile page.
The pw-accept-state and pw-discard-state can be overridden using the
--pw-set-state flag to "b4 ty". E.g. if you wanted to mark the patches
as "Not applicable":
b4 ty -d 5 --pw-set-state not-applicable
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
Diffstat (limited to 'b4/__init__.py')
-rw-r--r-- | b4/__init__.py | 86 |
1 files changed, 81 insertions, 5 deletions
diff --git a/b4/__init__.py b/b4/__init__.py index ebd4f2e..cfbe6a6 100644 --- a/b4/__init__.py +++ b/b4/__init__.py @@ -14,8 +14,8 @@ import email.header import email.generator import tempfile import pathlib +import argparse -import requests import urllib.parse import datetime import time @@ -25,6 +25,8 @@ import mailbox # noinspection PyCompatibility import pwd +import requests + from contextlib import contextmanager from typing import Optional, Tuple, Set, List, TextIO @@ -44,7 +46,8 @@ try: except ModuleNotFoundError: can_patatt = False -__VERSION__ = '0.9.0' +__VERSION__ = '0.10.0-dev' +PW_REST_API_VERSION = '1.2' def _dkim_log_filter(record): @@ -1867,7 +1870,7 @@ class LoreAttestorPatatt(LoreAttestor): self.have_key = True -def _run_command(cmdargs: list, stdin: Optional[bytes] = None) -> Tuple[int, bytes, bytes]: +def _run_command(cmdargs: List[str], stdin: Optional[bytes] = None) -> Tuple[int, bytes, bytes]: logger.debug('Running %s' % ' '.join(cmdargs)) sp = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) (output, error) = sp.communicate(input=stdin) @@ -2113,7 +2116,7 @@ def get_requests_session(): return REQSESSION -def get_msgid_from_stdin(): +def get_msgid_from_stdin() -> Optional[str]: if not sys.stdin.isatty(): from email.parser import BytesParser message = BytesParser().parsebytes( @@ -2122,7 +2125,7 @@ def get_msgid_from_stdin(): return None -def get_msgid(cmdargs) -> Optional[str]: +def get_msgid(cmdargs: argparse.Namespace) -> Optional[str]: if not cmdargs.msgid: logger.debug('Getting Message-ID from stdin') msgid = get_msgid_from_stdin() @@ -2134,6 +2137,15 @@ def get_msgid(cmdargs) -> Optional[str]: msgid = msgid.strip('<>') # Handle the case when someone pastes a full URL to the message + # Is this a patchwork URL? + matches = re.search(r'^https?://.*/project/.*/patch/([^/]+@[^/]+)', msgid, re.IGNORECASE) + if matches: + logger.debug('Looks like a patchwork URL') + chunks = matches.groups() + msgid = urllib.parse.unquote(chunks[0]) + return msgid + + # Is it a lore URL? matches = re.search(r'^https?://[^/]+/([^/]+)/([^/]+@[^/]+)', msgid, re.IGNORECASE) if matches: chunks = matches.groups() @@ -2588,3 +2600,67 @@ def get_smtp(identity: Optional[str] = None): smtp = smtplib.SMTP(server, port) return smtp, fromaddr + + +def get_patchwork_session(pwkey: str, pwurl: str) -> Tuple[requests.Session, str]: + session = requests.session() + session.headers.update({ + 'User-Agent': 'b4/%s' % __VERSION__, + 'Authorization': f'Token {pwkey}', + }) + url = '/'.join((pwurl.rstrip('/'), 'api', PW_REST_API_VERSION)) + logger.debug('pw url=%s', url) + return session, url + + +def patchwork_set_state(msgids: List[str], state: str) -> bool: + # Do we have a pw-key defined in config? + config = get_main_config() + pwkey = config.get('pw-key') + pwurl = config.get('pw-url') + pwproj = config.get('pw-project') + if not (pwkey and pwurl and pwproj): + logger.debug('Patchwork support not configured') + return False + pses, url = get_patchwork_session(pwkey, pwurl) + patches_url = '/'.join((url, 'patches')) + tochange = list() + for msgid in msgids: + # Two calls, first to look up the patch-id, second to update its state + params = [ + ('project', pwproj), + ('archived', 'false'), + ('msgid', msgid), + ] + try: + logger.debug('looking up patch_id of msgid=%s', msgid) + rsp = pses.get(patches_url, params=params, stream=False) + rsp.raise_for_status() + pdata = rsp.json() + for entry in pdata: + patch_id = entry.get('id') + if patch_id: + title = entry.get('name') + if entry.get('state') != state: + tochange.append((patch_id, title)) + except requests.exceptions.RequestException as ex: + logger.debug('Patchwork REST error: %s', ex) + + if tochange: + logger.info('---') + loc = urllib.parse.urlparse(pwurl) + logger.info('Patchwork: setting state on %s/%s', loc.netloc, pwproj) + for patch_id, title in tochange: + patchid_url = '/'.join((patches_url, str(patch_id), '')) + logger.debug('patchid_url=%s', patchid_url) + data = [ + ('state', state), + ] + try: + rsp = pses.patch(patchid_url, data=data, stream=False) + rsp.raise_for_status() + newdata = rsp.json() + if newdata.get('state') == state: + logger.info(' -> %s : %s', state, title) + except requests.exceptions.RequestException as ex: + logger.debug('Patchwork REST error: %s', ex) |