From aef44950db4e64d8567b945850b7bfeaf0158103 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Tue, 29 Apr 2025 07:22:13 -0600 Subject: patman: Create a module for handling patchwork At present the patchwork implementation is very simple, just consisting of a function which calls the REST API. We want to create a fake patchwork for use in tests. So start a new module to encapsulate communication with the patchwork server. Use asyncio since it is easier to handle lots of concurrent requests from different parts of the code. Signed-off-by: Simon Glass --- tools/patman/patchwork.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tools/patman/patchwork.py (limited to 'tools/patman/patchwork.py') diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py new file mode 100644 index 00000000000..0456f98e83c --- /dev/null +++ b/tools/patman/patchwork.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright 2025 Simon Glass +# +"""Provides a basic API for the patchwork server +""" + +import asyncio + +import aiohttp + +# Number of retries +RETRIES = 3 + +# Max concurrent request +MAX_CONCURRENT = 50 + +class Patchwork: + """Class to handle communication with patchwork + """ + def __init__(self, url, show_progress=True): + """Set up a new patchwork handler + + Args: + url (str): URL of patchwork server, e.g. + 'https://patchwork.ozlabs.org' + """ + self.url = url + self.proj_id = None + self.link_name = None + self._show_progress = show_progress + self.semaphore = asyncio.Semaphore(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 + + 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 -- cgit v1.3.1 From 24776ab2766787d9629e7250486a0a662871b070 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Tue, 29 Apr 2025 07:22:14 -0600 Subject: patman: Move Patch and Review to patchwork module These relate to information obtained from the patchwork server, so move their definition into the new patchwork module. Signed-off-by: Simon Glass --- tools/patman/func_test.py | 21 ++++---- tools/patman/patchwork.py | 113 +++++++++++++++++++++++++++++++++++++++++++ tools/patman/status.py | 119 ++-------------------------------------------- 3 files changed, 129 insertions(+), 124 deletions(-) (limited to 'tools/patman/patchwork.py') diff --git a/tools/patman/func_test.py b/tools/patman/func_test.py index b3145612f9f..50fb53787d8 100644 --- a/tools/patman/func_test.py +++ b/tools/patman/func_test.py @@ -20,6 +20,7 @@ from patman.commit import Commit from patman import control from patman import patchstream from patman.patchstream import PatchStream +from patman import patchwork from patman import send from patman.series import Series from patman import settings @@ -807,7 +808,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c def test_parse_subject(self): """Test parsing of the patch subject""" - patch = status.Patch('1') + patch = patchwork.Patch('1') # Simple patch not in a series patch.parse_subject('Testing') @@ -876,11 +877,11 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c commit3 = Commit('3456') commit3.subject = 'Subject 2' - patch1 = status.Patch('1') + patch1 = patchwork.Patch('1') patch1.subject = 'Subject 1' - patch2 = status.Patch('2') + patch2 = patchwork.Patch('2') patch2.subject = 'Subject 2' - patch3 = status.Patch('3') + patch3 = patchwork.Patch('3') patch3.subject = 'Subject 2' series = Series() @@ -976,7 +977,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c commit2 = Commit('ef12') commit2.subject = 'Subject 2' - patch1 = status.Patch('1') + patch1 = patchwork.Patch('1') patch1.parse_subject('[1/2] Subject 1') patch1.name = patch1.raw_subject patch1.content = 'This is my patch content' @@ -984,7 +985,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c patch1.comments = [comment1a] - patch2 = status.Patch('2') + patch2 = patchwork.Patch('2') patch2.parse_subject('[2/2] Subject 2') patch2.name = patch2.raw_subject patch2.content = 'Some other patch content' @@ -1120,7 +1121,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c series = patchstream.get_metadata_for_list(branch, gitdir, count) self.assertEqual(2, len(series.commits)) - patch1 = status.Patch('1') + patch1 = patchwork.Patch('1') patch1.parse_subject('[1/2] %s' % series.commits[0].subject) patch1.name = patch1.raw_subject patch1.content = 'This is my patch content' @@ -1128,7 +1129,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c patch1.comments = [comment1a] - patch2 = status.Patch('2') + patch2 = patchwork.Patch('2') patch2.parse_subject('[2/2] %s' % series.commits[1].subject) patch2.name = patch2.raw_subject patch2.content = 'Some other patch content' @@ -1291,7 +1292,7 @@ line8 commit2 = Commit('ef12') commit2.subject = 'Subject 2' - patch1 = status.Patch('1') + patch1 = patchwork.Patch('1') patch1.parse_subject('[1/2] Subject 1') patch1.name = patch1.raw_subject patch1.content = 'This is my patch content' @@ -1312,7 +1313,7 @@ Reviewed-by: %s patch1.comments = [comment1a] - patch2 = status.Patch('2') + patch2 = patchwork.Patch('2') patch2.parse_subject('[2/2] Subject 2') patch2.name = patch2.raw_subject patch2.content = 'Some other patch content' diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py index 0456f98e83c..bb3645455fe 100644 --- a/tools/patman/patchwork.py +++ b/tools/patman/patchwork.py @@ -6,6 +6,7 @@ """ import asyncio +import re import aiohttp @@ -15,6 +16,118 @@ 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) + """ + def __init__(self, pid): + 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 + + # 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 """ diff --git a/tools/patman/status.py b/tools/patman/status.py index 5fb436e08ff..8edb4ced449 100644 --- a/tools/patman/status.py +++ b/tools/patman/status.py @@ -11,25 +11,16 @@ collected from patchwork. import collections import concurrent.futures from itertools import repeat -import re import pygit2 import requests -from patman import patchstream -from patman.patchstream import PatchStream from u_boot_pylib import terminal from u_boot_pylib import tout +from patman import patchstream +from patman import patchwork +from patman.patchstream import PatchStream -# 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+)') def to_int(vals): """Convert a list of strings into integers, using 0 if not an integer @@ -44,106 +35,6 @@ def to_int(vals): return out -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) - """ - def __init__(self, pid): - 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 - - # 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("Cannot parse subject '%s'" % 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 - def compare_with_series(series, patches): """Compare a list of patches with a series it came from @@ -253,7 +144,7 @@ def collect_patches(series, series_id, url, rest_api=call_rest_api): # Work through each row (patch) one at a time, collecting the information warn_count = 0 for pw_patch in patch_dict: - patch = Patch(pw_patch['id']) + patch = patchwork.Patch(pw_patch['id']) patch.parse_subject(pw_patch['name']) patches.append(patch) if warn_count > 1: @@ -304,7 +195,7 @@ def find_new_responses(new_rtag_list, review_list, seq, cmt, patch, url, if pstrm.snippets: submitter = comment['submitter'] person = '%s <%s>' % (submitter['name'], submitter['email']) - reviews.append(Review(person, pstrm.snippets)) + reviews.append(patchwork.Review(person, pstrm.snippets)) for response, people in pstrm.commit.rtags.items(): rtags[response].update(people) -- cgit v1.3.1 From d65490650fa5c4b16fffda00ea5a1ffdf55027b6 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Tue, 29 Apr 2025 07:22:15 -0600 Subject: patman: Add reading of series and patch status Expand the patchwork module so that it can match the current requirements of the 'patman status' command, i.e. reading the state of a series and the patches associated with it. Since the format of each patchwork response is a little tricky to understand, add examples in comments at the top of each function. Signed-off-by: Simon Glass --- tools/patman/patchwork.py | 218 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) (limited to 'tools/patman/patchwork.py') diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py index bb3645455fe..e0adbdb6481 100644 --- a/tools/patman/patchwork.py +++ b/tools/patman/patchwork.py @@ -177,3 +177,221 @@ class Patchwork: except aiohttp.client_exceptions.ServerDisconnectedError: if i == RETRIES: raise + + 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 ", + "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. + '' + 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_patch_comments(self, patch_id): + async with aiohttp.ClientSession() as client: + return await self._get_patch_comments(client, patch_id) + + 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 series_get_state(self, client, link, read_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 + + Return: tuple: + 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() + + return patches -- cgit v1.3.1 From 3fd99e2177c17539f4101151cb061e8f7700d6a1 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Tue, 29 Apr 2025 07:22:19 -0600 Subject: patman: Adjust how the fake request() function is provided Instead of passing the URL and function to each call, put the fake into the Patchwork object instead. Signed-off-by: Simon Glass --- tools/patman/control.py | 4 +++- tools/patman/func_test.py | 41 ++++++++++++++++----------------- tools/patman/patchwork.py | 26 +++++++++++++++++++++ tools/patman/status.py | 58 ++++++++++++----------------------------------- 4 files changed, 62 insertions(+), 67 deletions(-) (limited to 'tools/patman/patchwork.py') diff --git a/tools/patman/control.py b/tools/patman/control.py index 06a9dfd2bca..cb8552ed550 100644 --- a/tools/patman/control.py +++ b/tools/patman/control.py @@ -24,6 +24,7 @@ from u_boot_pylib import terminal from u_boot_pylib import tools from patman import checkpatch from patman import patchstream +from patman import patchwork from patman import send @@ -95,12 +96,13 @@ def patchwork_status(branch, count, start, end, dest_branch, force, # Allow the series to override the URL if 'patchwork_url' in series: url = series.patchwork_url + pwork = patchwork.Patchwork(url) # Import this here to avoid failing on other commands if the dependencies # are not present from patman import status status.check_and_show_status(series, found[0], branch, dest_branch, force, - show_comments, url) + show_comments, pwork) def do_patman(args): diff --git a/tools/patman/func_test.py b/tools/patman/func_test.py index ea96c508fa2..9c5e7d7dd51 100644 --- a/tools/patman/func_test.py +++ b/tools/patman/func_test.py @@ -6,6 +6,7 @@ """Functional tests for checking that patman behaves correctly""" +import asyncio import contextlib import os import pathlib @@ -767,14 +768,13 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c os.chdir(orig_dir) @staticmethod - def _fake_patchwork(url, subpath): + def _fake_patchwork(subpath): """Fake Patchwork server for the function below This handles accessing a series, providing a list consisting of a single patch Args: - url (str): URL of patchwork server subpath (str): URL subpath to use """ re_series = re.match(r'series/(\d*)/$', subpath) @@ -787,21 +787,17 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c def test_status_mismatch(self): """Test Patchwork patches not matching the series""" - series = Series() - + pwork = patchwork.Patchwork.for_testing(self._fake_patchwork) with terminal.capture() as (_, err): - patches = status.collect_patches(1234, None, self._fake_patchwork) + patches = status.collect_patches(1234, pwork) status.check_patch_count(0, len(patches)) self.assertIn('Warning: Patchwork reports 1 patches, series has 0', err.getvalue()) def test_status_read_patch(self): """Test handling a single patch in Patchwork""" - series = Series() - series.commits = [Commit('abcd')] - - patches = status.collect_patches(1234, None, - self._fake_patchwork) + pwork = patchwork.Patchwork.for_testing(self._fake_patchwork) + patches = status.collect_patches(1234, pwork) self.assertEqual(1, len(patches)) patch = patches[0] self.assertEqual('1', patch.id) @@ -944,14 +940,13 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c "Cannot find commit for patch 3 ('Subject 2')"], warnings) - def _fake_patchwork2(self, url, subpath): + def _fake_patchwork2(self, subpath): """Fake Patchwork server for the function below This handles accessing series, patches and comments, providing the data in self.patches to the caller Args: - url (str): URL of patchwork server subpath (str): URL subpath to use """ re_series = re.match(r'series/(\d*)/$', subpath) @@ -1007,13 +1002,14 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c review_list = [None, None] # Check that the tags are picked up on the first patch + pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2) status.find_new_responses(new_rtag_list, review_list, 0, commit1, - patch1, None, self._fake_patchwork2) + patch1, pwork) self.assertEqual(new_rtag_list[0], {'Reviewed-by': {self.joe}}) # Now the second patch status.find_new_responses(new_rtag_list, review_list, 1, commit2, - patch2, None, self._fake_patchwork2) + patch2, pwork) self.assertEqual(new_rtag_list[1], { 'Reviewed-by': {self.mary, self.fred}, 'Tested-by': {self.leb}}) @@ -1023,7 +1019,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c new_rtag_list = [None] * count commit1.rtags = {'Reviewed-by': {self.joe}} status.find_new_responses(new_rtag_list, review_list, 0, commit1, - patch1, None, self._fake_patchwork2) + patch1, pwork) self.assertEqual(new_rtag_list[0], {}) # For the second commit, add Ed and Fred, so only Mary should be left @@ -1031,7 +1027,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c 'Tested-by': {self.leb}, 'Reviewed-by': {self.fred}} status.find_new_responses(new_rtag_list, review_list, 1, commit2, - patch2, None, self._fake_patchwork2) + patch2, pwork) self.assertEqual(new_rtag_list[1], {'Reviewed-by': {self.mary}}) # Check that the output patches expectations: @@ -1046,8 +1042,9 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c series = Series() series.commits = [commit1, commit2] terminal.set_print_test_mode() + pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2) status.check_and_show_status(series, '1234', None, None, False, False, - None, self._fake_patchwork2) + pwork) lines = iter(terminal.get_print_test_lines()) col = terminal.Color() self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.BLUE), @@ -1082,14 +1079,13 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c '1 new response available in patchwork (use -d to write them to a new branch)', None), next(lines)) - def _fake_patchwork3(self, url, subpath): + def _fake_patchwork3(self, subpath): """Fake Patchwork server for the function below This handles accessing series, patches and comments, providing the data in self.patches to the caller Args: - url (str): URL of patchwork server subpath (str): URL subpath to use """ re_series = re.match(r'series/(\d*)/$', subpath) @@ -1160,9 +1156,9 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c # terminal.set_print_test_mode() + pwork = patchwork.Patchwork.for_testing(self._fake_patchwork3) status.check_and_show_status(series, '1234', branch, dest_branch, - False, False, None, self._fake_patchwork3, - repo) + False, False, pwork, repo) lines = terminal.get_print_test_lines() self.assertEqual(12, len(lines)) self.assertEqual( @@ -1362,8 +1358,9 @@ Reviewed-by: %s series = Series() series.commits = [commit1, commit2] terminal.set_print_test_mode() + pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2) status.check_and_show_status(series, '1234', None, None, False, True, - None, self._fake_patchwork2) + pwork) lines = iter(terminal.get_print_test_lines()) col = terminal.Color() self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.BLUE), diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py index e0adbdb6481..3ec266f073e 100644 --- a/tools/patman/patchwork.py +++ b/tools/patman/patchwork.py @@ -139,6 +139,7 @@ class Patchwork: 'https://patchwork.ozlabs.org' """ self.url = url + self.fake_request = None self.proj_id = None self.link_name = None self._show_progress = show_progress @@ -160,6 +161,8 @@ class Patchwork: """ # 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: @@ -178,6 +181,29 @@ class Patchwork: if i == RETRIES: raise + async def session_request(self, subpath): + async with aiohttp.ClientSession() as client: + return await self._request(client, subpath) + + def request(self, subpath): + return asyncio.run(self.session_request(subpath)) + + @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 + async def get_series(self, client, link): """Read information about a series diff --git a/tools/patman/status.py b/tools/patman/status.py index ed4cca6f724..5c75d7b10c6 100644 --- a/tools/patman/status.py +++ b/tools/patman/status.py @@ -134,26 +134,7 @@ def compare_with_series(series, patches): return patch_for_commit, commit_for_patch, warnings -def call_rest_api(url, subpath): - """Call the patchwork API and return the result as JSON - - Args: - url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org' - subpath (str): URL subpath to use - - Returns: - dict: Json result - - Raises: - ValueError: the URL could not be read - """ - full_url = '%s/api/1.2/%s' % (url, subpath) - response = requests.get(full_url) - if response.status_code != 200: - raise ValueError("Could not read URL '%s'" % full_url) - return response.json() - -def collect_patches(series_id, url, rest_api=call_rest_api): +def collect_patches(series_id, pwork): """Collect patch information about a series from patchwork Uses the Patchwork REST API to collect information provided by patchwork @@ -161,9 +142,7 @@ def collect_patches(series_id, url, rest_api=call_rest_api): Args: series_id (str): Patch series ID number - url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org' - rest_api (function): API function to call to access Patchwork, for - testing + pwork (Patchwork): Patchwork object to use for reading Returns: list of Patch: List of patches sorted by sequence number @@ -172,7 +151,7 @@ def collect_patches(series_id, url, rest_api=call_rest_api): ValueError: if the URL could not be read or the web page does not follow the expected structure """ - data = rest_api(url, 'series/%s/' % series_id) + data = pwork.request('series/%s/' % series_id) # Get all the rows, which are patches patch_dict = data['patches'] @@ -193,8 +172,7 @@ def collect_patches(series_id, url, rest_api=call_rest_api): patches = sorted(patches, key=lambda x: x.seq) return patches -def find_new_responses(new_rtag_list, review_list, seq, cmt, patch, url, - rest_api=call_rest_api): +def find_new_responses(new_rtag_list, review_list, seq, cmt, patch, pwork): """Find new rtags collected by patchwork that we don't know about This is designed to be run in parallel, once for each commit/patch @@ -211,16 +189,14 @@ def find_new_responses(new_rtag_list, review_list, seq, cmt, patch, url, seq (int): Position in new_rtag_list to update cmt (Commit): Commit object for this commit patch (Patch): Corresponding Patch object for this patch - url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org' - rest_api (function): API function to call to access Patchwork, for - testing + pwork (Patchwork): Patchwork object to use for reading """ if not patch: return # Get the content for the patch email itself as well as all comments - data = rest_api(url, 'patches/%s/' % patch.id) - comment_data = rest_api(url, 'patches/%s/comments/' % patch.id) + data = pwork.request('patches/%s/' % patch.id) + comment_data = pwork.request('patches/%s/comments/' % patch.id) new_rtags, reviews = process_reviews(data['content'], comment_data, cmt.rtags) @@ -316,7 +292,7 @@ def create_branch(series, new_rtag_list, branch, dest_branch, overwrite, [parent.target]) return num_added -def check_status(series, series_id, url, rest_api=call_rest_api): +def check_status(series, series_id, pwork): """Check the status of a series on Patchwork This finds review tags and comments for a series in Patchwork, displaying @@ -325,9 +301,7 @@ def check_status(series, series_id, url, rest_api=call_rest_api): Args: series (Series): Series object for the existing branch series_id (str): Patch series ID number - url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org' - rest_api (function): API function to call to access Patchwork, for - testing + pwork (Patchwork): Patchwork object to use for reading Return: tuple: @@ -342,7 +316,7 @@ def check_status(series, series_id, url, rest_api=call_rest_api): list for each patch, each a: list of Review objects for the patch """ - patches = collect_patches(series_id, url, rest_api) + patches = collect_patches(series_id, pwork) count = len(series.commits) new_rtag_list = [None] * count review_list = [None] * count @@ -356,8 +330,7 @@ def check_status(series, series_id, url, rest_api=call_rest_api): with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor: futures = executor.map( find_new_responses, repeat(new_rtag_list), repeat(review_list), - range(count), series.commits, patch_list, repeat(url), - repeat(rest_api)) + range(count), series.commits, patch_list, repeat(pwork)) for fresponse in futures: if fresponse: raise fresponse.exception() @@ -445,8 +418,7 @@ def show_status(series, branch, dest_branch, force, patches, patch_for_commit, def check_and_show_status(series, link, branch, dest_branch, force, - show_comments, url, rest_api=call_rest_api, - test_repo=None): + show_comments, pwork, test_repo=None): """Read the series status from patchwork and show it to the user Args: @@ -456,12 +428,10 @@ def check_and_show_status(series, link, branch, dest_branch, force, 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 patch comments - url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org' - rest_api (function): API function to call to access Patchwork, for - testing + pwork (Patchwork): Patchwork object to use for reading test_repo (pygit2.Repository): Repo to use (use None unless testing) """ patches, patch_for_commit, new_rtag_list, review_list = check_status( - series, link, url, rest_api) + series, link, pwork) show_status(series, branch, dest_branch, force, patches, patch_for_commit, show_comments, new_rtag_list, review_list, test_repo) -- cgit v1.3.1 From 0fb0b46200998461fd532ddd928cc0451246bd6a Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Tue, 29 Apr 2025 07:22:22 -0600 Subject: patman: Add more information to Patch The cover letter has some information on each patch, so allow this to be stored in the Patch object. Signed-off-by: Simon Glass --- tools/patman/patchwork.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'tools/patman/patchwork.py') diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py index 3ec266f073e..8b0968c5765 100644 --- a/tools/patman/patchwork.py +++ b/tools/patman/patchwork.py @@ -52,8 +52,10 @@ class Patch(dict): 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): + 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 @@ -62,6 +64,11 @@ class Patch(dict): 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): -- cgit v1.3.1 From 45f4f6219182927c34d2dc0359f4bf044d3ed432 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Tue, 29 Apr 2025 07:22:24 -0600 Subject: patman: Switch over to asyncio Now that all the pieces are in place, switch over from using an executor to using asyncio. While we are here, import defaultdict to avoid needing to specify its module. Signed-off-by: Simon Glass --- tools/patman/func_test.py | 4 +- tools/patman/patchwork.py | 8 +-- tools/patman/status.py | 137 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 105 insertions(+), 44 deletions(-) (limited to 'tools/patman/patchwork.py') diff --git a/tools/patman/func_test.py b/tools/patman/func_test.py index 47fbbe0cc3f..eee20b9b155 100644 --- a/tools/patman/func_test.py +++ b/tools/patman/func_test.py @@ -789,7 +789,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c """Test Patchwork patches not matching the series""" pwork = patchwork.Patchwork.for_testing(self._fake_patchwork) with terminal.capture() as (_, err): - patches = status.collect_patches(1234, pwork) + patches = asyncio.run(status.check_status(1234, pwork)) status.check_patch_count(0, len(patches)) self.assertIn('Warning: Patchwork reports 1 patches, series has 0', err.getvalue()) @@ -797,7 +797,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c def test_status_read_patch(self): """Test handling a single patch in Patchwork""" pwork = patchwork.Patchwork.for_testing(self._fake_patchwork) - patches = status.collect_patches(1234, pwork) + patches = asyncio.run(status.check_status(1234, pwork)) self.assertEqual(1, len(patches)) patch = patches[0] self.assertEqual('1', patch.id) diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py index 8b0968c5765..2b7734bbfe4 100644 --- a/tools/patman/patchwork.py +++ b/tools/patman/patchwork.py @@ -9,6 +9,7 @@ import asyncio import re import aiohttp +from u_boot_pylib import terminal # Number of retries RETRIES = 3 @@ -188,13 +189,6 @@ class Patchwork: if i == RETRIES: raise - async def session_request(self, subpath): - async with aiohttp.ClientSession() as client: - return await self._request(client, subpath) - - def request(self, subpath): - return asyncio.run(self.session_request(subpath)) - @staticmethod def for_testing(func): """Get an instance to use for testing diff --git a/tools/patman/status.py b/tools/patman/status.py index 8fc2a50b426..f5be51461e7 100644 --- a/tools/patman/status.py +++ b/tools/patman/status.py @@ -8,18 +8,18 @@ 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 -import requests from u_boot_pylib import terminal from u_boot_pylib import tout from patman import patchstream from patman import patchwork -from patman.patchstream import PatchStream def to_int(vals): @@ -292,7 +292,7 @@ def create_branch(series, new_rtag_list, branch, dest_branch, overwrite, [parent.target]) return num_added -def check_status(series, series_id, pwork): +def _check_status(series, series_id, pwork): """Check the status of a series on Patchwork This finds review tags and comments for a series in Patchwork, displaying @@ -349,7 +349,59 @@ def check_patch_count(num_commits, num_patches): f'series has {num_commits}') -def do_show_status(series, patch_for_commit, show_comments, new_rtag_list, +def do_show_status(patches, series, branch, show_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: + patches (list of Patch): Patch objects in the series + series (Series): Series object for the existing branch + branch (str): Existing branch to update, or None + show_comments (bool): True to show the comments on each patch + 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 + list of PATCH objects + """ + 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 + + 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, patch_for_commit, show_comments, new_rtag_list, + review_list, col) + return num_to_add, new_rtag_list, patches + + +def _do_show_status(series, patch_for_commit, show_comments, new_rtag_list, review_list, col): num_to_add = 0 for seq, cmt in enumerate(series.commits): @@ -367,19 +419,24 @@ def do_show_status(series, patch_for_commit, show_comments, new_rtag_list, 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) + 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(' %s' % line, - colour=col.MAGENTA if quoted else None) + 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, patches, patch_for_commit, - show_comments, new_rtag_list, review_list, test_repo=None): - """Show status to the user and allow a branch to be written +def show_status(series, branch, dest_branch, force, patches, show_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: series (Series): Series object for the existing branch @@ -387,34 +444,44 @@ def show_status(series, branch, dest_branch, force, patches, patch_for_commit, dest_branch (str): Name of new branch to create, or None force (bool): True to force overwriting dest_branch if it exists patches (list of Patch): Patches sorted by sequence number - patch_for_commit (dict): Patches for commit - key: Commit number (0...n-1) - value: Patch object for that commit - show_comments (bool): True to show patch comments - new_rtag_list (list of dict) review tags for each patch: - key: Response tag (e.g. 'Reviewed-by') - value: Set of people who gave that response, each a name/email - string - review_list (list of list): list for each patch, each a: - list of Review objects for the patch + show_comments (bool): True to show the comments on each patch 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 = do_show_status(series, patch_for_commit, show_comments, - new_rtag_list, review_list, col) + num_to_add, new_rtag_list, _ = do_show_status( + patches, series, branch, show_comments, col) - terminal.tprint("%d new response%s available in patchwork%s" % - (num_to_add, 's' if num_to_add != 1 else '', - '' if dest_branch - else ' (use -d to write them to a new branch)')) + 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) + num_added = create_branch(series, new_rtag_list, branch, + dest_branch, force, test_repo) terminal.tprint( - "%d response%s added from patchwork into new branch '%s'" % - (num_added, 's' if num_added != 1 else '', dest_branch)) + 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): + """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: + list of Patch objects + """ + async with aiohttp.ClientSession() as client: + patches = await pwork.series_get_state(client, link, read_comments) + return patches def check_and_show_status(series, link, branch, dest_branch, force, @@ -427,11 +494,11 @@ def check_and_show_status(series, link, branch, dest_branch, force, 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 patch comments + show_comments (bool): True to show the comments on each patch pwork (Patchwork): Patchwork object to use for reading test_repo (pygit2.Repository): Repo to use (use None unless testing) """ - patches, patch_for_commit, new_rtag_list, review_list = check_status( - series, link, pwork) - show_status(series, branch, dest_branch, force, patches, patch_for_commit, - show_comments, new_rtag_list, review_list, test_repo) + patches = asyncio.run(check_status(link, pwork, True)) + + show_status(series, branch, dest_branch, force, patches, show_comments, + test_repo=test_repo) -- cgit v1.3.1 From 52aef33f953b5864dc015448783ecd2d9415e52f Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Tue, 29 Apr 2025 07:22:25 -0600 Subject: patman: Provide an option to run in single-threaded mode Patman normally sends multiple concurrent requests to the patchwork server, as this is faster. Provide an option to disable this. Signed-off-by: Simon Glass --- tools/patman/cmdline.py | 2 ++ tools/patman/control.py | 4 ++-- tools/patman/patchwork.py | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) (limited to 'tools/patman/patchwork.py') diff --git a/tools/patman/cmdline.py b/tools/patman/cmdline.py index a7327a20665..cf32716b8e0 100644 --- a/tools/patman/cmdline.py +++ b/tools/patman/cmdline.py @@ -130,6 +130,8 @@ def parse_args(): 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') # Parse options twice: first to get the project and second to handle # defaults properly (which depends on project) diff --git a/tools/patman/control.py b/tools/patman/control.py index cb8552ed550..cfea35ea648 100644 --- a/tools/patman/control.py +++ b/tools/patman/control.py @@ -44,7 +44,7 @@ def do_send(args): def patchwork_status(branch, count, start, end, dest_branch, force, - show_comments, url): + 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 @@ -96,7 +96,7 @@ def patchwork_status(branch, count, start, end, dest_branch, force, # Allow the series to override the URL if 'patchwork_url' in series: url = series.patchwork_url - pwork = patchwork.Patchwork(url) + pwork = patchwork.Patchwork(url, single_thread=single_thread) # Import this here to avoid failing on other commands if the dependencies # are not present diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py index 2b7734bbfe4..47d7be28fdf 100644 --- a/tools/patman/patchwork.py +++ b/tools/patman/patchwork.py @@ -139,7 +139,7 @@ class Review: class Patchwork: """Class to handle communication with patchwork """ - def __init__(self, url, show_progress=True): + def __init__(self, url, show_progress=True, single_thread=False): """Set up a new patchwork handler Args: @@ -151,7 +151,8 @@ class Patchwork: self.proj_id = None self.link_name = None self._show_progress = show_progress - self.semaphore = asyncio.Semaphore(MAX_CONCURRENT) + self.semaphore = asyncio.Semaphore( + 1 if single_thread else MAX_CONCURRENT) self.request_count = 0 async def _request(self, client, subpath): -- cgit v1.3.1 From 2610699420052f4f5fb21577c8181e79475a9086 Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sat, 10 May 2025 13:05:05 +0200 Subject: patman: Update status command support cover-letter info Add support to the status module for reading and supporting cover letters, including comments. Plumb this through to the patchwork module. The actual support in the latter is not yet integrated. Signed-off-by: Simon Glass --- tools/patman/func_test.py | 16 ++++++----- tools/patman/patchwork.py | 15 +++++++--- tools/patman/status.py | 71 +++++++++++++++++++++++++++++++---------------- 3 files changed, 67 insertions(+), 35 deletions(-) (limited to 'tools/patman/patchwork.py') diff --git a/tools/patman/func_test.py b/tools/patman/func_test.py index 2faff8019f6..d029181765c 100644 --- a/tools/patman/func_test.py +++ b/tools/patman/func_test.py @@ -669,7 +669,8 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c pwork = patchwork.Patchwork.for_testing(self._fake_patchwork) with terminal.capture() as (_, err): loop = asyncio.get_event_loop() - patches = loop.run_until_complete(status.check_status(1234, pwork)) + _, patches = loop.run_until_complete(status.check_status(1234, + pwork)) status.check_patch_count(0, len(patches)) self.assertIn('Warning: Patchwork reports 1 patches, series has 0', err.getvalue()) @@ -678,7 +679,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c """Test handling a single patch in Patchwork""" pwork = patchwork.Patchwork.for_testing(self._fake_patchwork) loop = asyncio.get_event_loop() - patches = loop.run_until_complete(status.check_status(1234, pwork)) + _, patches = loop.run_until_complete(status.check_status(1234, pwork)) self.assertEqual(1, len(patches)) patch = patches[0] self.assertEqual('1', patch.id) @@ -928,7 +929,7 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c terminal.set_print_test_mode() pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2) status.check_and_show_status(series, '1234', None, None, False, False, - pwork) + False, pwork) itr = iter(terminal.get_print_test_lines()) col = terminal.Color() self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.YELLOW), @@ -1041,8 +1042,9 @@ diff --git a/lib/efi_loader/efi_memory.c b/lib/efi_loader/efi_memory.c terminal.set_print_test_mode() pwork = patchwork.Patchwork.for_testing(self._fake_patchwork3) - status.check_and_show_status(series, '1234', branch, dest_branch, - False, False, pwork, repo) + status.check_and_show_status( + series, '1234', branch, dest_branch, False, False, False, pwork, + repo) lines = terminal.get_print_test_lines() self.assertEqual(12, len(lines)) self.assertEqual( @@ -1244,8 +1246,8 @@ Reviewed-by: %s series.commits = [commit1, commit2] terminal.set_print_test_mode() pwork = patchwork.Patchwork.for_testing(self._fake_patchwork2) - status.check_and_show_status(series, '1234', None, None, False, True, - pwork) + status.check_and_show_status( + series, '1234', None, None, False, True, False, pwork) itr = iter(terminal.get_print_test_lines()) col = terminal.Color() self.assertEqual(terminal.PrintLine(' 1 Subject 1', col.YELLOW), diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py index 47d7be28fdf..afafb97ffed 100644 --- a/tools/patman/patchwork.py +++ b/tools/patman/patchwork.py @@ -388,16 +388,20 @@ class Patchwork: return Patch(patch_id, state, data, comment_data) - async def series_get_state(self, client, link, read_comments): + 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: - list of Patch objects + 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']) @@ -407,7 +411,7 @@ class Patchwork: if read_comments: # Returns a list of Patch objects tasks = [self._get_patch_status(client, patch_list[i]['id']) - for i in range(count)] + for i in range(count)] patch_status = await asyncio.gather(*tasks) for patch_data, status in zip(patch_list, patch_status): @@ -422,4 +426,7 @@ class Patchwork: if self._show_progress: terminal.print_clear() - return patches + # TODO: Implement this + cover = None + + return cover, patches diff --git a/tools/patman/status.py b/tools/patman/status.py index 74d05624226..967fef3ad6e 100644 --- a/tools/patman/status.py +++ b/tools/patman/status.py @@ -74,7 +74,7 @@ def compare_with_series(series, patches): Args: series (Series): Series to compare against - patches (:type: list of Patch): list of Patch objects to compare with + patches (list of Patch): list of Patch objects to compare with Returns: tuple @@ -224,18 +224,20 @@ def check_patch_count(num_commits, num_patches): f'series has {num_commits}') -def do_show_status(patches, series, branch, show_comments, col, - warnings_on_stderr=True): +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: - patches (list of Patch): Patch objects in the series series (Series): Series object for the existing branch - branch (str): Existing branch to update, or None + 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: @@ -245,7 +247,6 @@ def do_show_status(patches, series, branch, show_comments, col, key: Response tag (e.g. 'Reviewed-by') value: Set of people who gave that response, each a name/email string - list of PATCH objects """ compare = [] for pw_patch in patches: @@ -274,13 +275,26 @@ def do_show_status(patches, series, branch, show_comments, col, 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, patch_for_commit, show_comments, new_rtag_list, - review_list, col) - return num_to_add, new_rtag_list, patches + 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, patch_for_commit, show_comments, new_rtag_list, - review_list, col): +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) @@ -309,26 +323,29 @@ def _do_show_status(series, patch_for_commit, show_comments, new_rtag_list, return num_to_add -def show_status(series, branch, dest_branch, force, patches, show_comments, - test_repo=None): +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( - patches, series, branch, show_comments, col) + 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)' @@ -346,7 +363,8 @@ def show_status(series, branch, dest_branch, force, patches, show_comments, f"from patchwork into new branch '{dest_branch}'") -async def check_status(link, pwork, read_comments=False): +async def check_status(link, pwork, read_comments=False, + read_cover_comments=False): """Set up an HTTP session and get the required state Args: @@ -354,16 +372,18 @@ async def check_status(link, pwork, read_comments=False): pwork (Patchwork): Patchwork object to use for reading read_comments (bool): True to read comments and state for each patch - Return: tuple: - list of Patch objects + Return: tuple: + COVER object, or None if none or not read_cover_comments + list of PATCH objects """ async with aiohttp.ClientSession() as client: - patches = await pwork.series_get_state(client, link, read_comments) - return patches + 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, pwork, test_repo=None): + show_comments, show_cover_comments, pwork, + test_repo=None): """Read the series status from patchwork and show it to the user Args: @@ -373,10 +393,13 @@ def check_and_show_status(series, link, branch, dest_branch, force, 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) """ - patches = asyncio.run(check_status(link, pwork, True)) + 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, patches, show_comments, - test_repo=test_repo) + show_status(series, branch, dest_branch, force, cover, patches, + show_comments, show_cover_comments, test_repo=test_repo) -- cgit v1.3.1 From 42588591e2e7a62258cfa7b487b0accad5db3fca Mon Sep 17 00:00:00 2001 From: Simon Glass Date: Sat, 10 May 2025 13:05:06 +0200 Subject: patman: Enhance patchwork interface to support Cseries Add various new requests to the Patchwork class, so we can obtain the required information. This includes cover letters and comments. Signed-off-by: Simon Glass --- tools/patman/patchwork.py | 428 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 424 insertions(+), 4 deletions(-) (limited to 'tools/patman/patchwork.py') diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py index afafb97ffed..d485648e467 100644 --- a/tools/patman/patchwork.py +++ b/tools/patman/patchwork.py @@ -9,8 +9,36 @@ 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 @@ -206,6 +234,165 @@ class Patchwork: 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 @@ -365,9 +552,220 @@ class Patchwork: """ return await self._request(client, f'patches/{patch_id}/comments/') - async def get_patch_comments(self, patch_id): + 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": "", + "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 ; 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 ; Tue, 4 Mar 2025 14:09:55 +0100 (CET)", + "by mail-io1-xd2e.google.com with SMTP id + ca18e2360f4ac-85ae33109f6so128326139f.2 + for ; 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 ", + "To": "U-Boot Mailing List ", + "Cc": "Simon Glass , Alexander Kochetkov , + Alper Nebi Yasak , + Brandon Maier , + Jerome Forissier , + Jiaxun Yang , + Neha Malcom Francis , + Patrick Rudolph , + Paul HENRYS , Peng Fan , + Philippe Reynes , + Stefan Herbrechtsmeier , + Tom Rini ", + "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 ", + "List-Unsubscribe": ", + ", + "List-Archive": "", + "List-Post": "", + "List-Help": "", + "List-Subscribe": ", + ", + "Errors-To": "u-boot-bounces@lists.denx.de", + "Sender": "\"U-Boot\" ", + "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._get_patch_comments(client, patch_id) + 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 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 @@ -388,6 +786,26 @@ class Patchwork: 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 @@ -426,7 +844,9 @@ class Patchwork: if self._show_progress: terminal.print_clear() - # TODO: Implement this - cover = None + if read_cover_comments: + cover = await self.get_series_cover(client, data) + else: + cover = None return cover, patches -- cgit v1.3.1