summaryrefslogtreecommitdiff
path: root/tools/patman/patchwork.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/patman/patchwork.py')
-rw-r--r--tools/patman/patchwork.py852
1 files changed, 0 insertions, 852 deletions
diff --git a/tools/patman/patchwork.py b/tools/patman/patchwork.py
deleted file mode 100644
index d485648e467..00000000000
--- a/tools/patman/patchwork.py
+++ /dev/null
@@ -1,852 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2025 Simon Glass <[email protected]>
-#
-"""Provides a basic API for the patchwork server
-"""
-
-import asyncio
-import re
-
-import aiohttp
-from collections import namedtuple
-
-from u_boot_pylib import terminal
-
-# Information passed to series_get_states()
-# link (str): Patchwork link for series
-# series_id (int): Series ID in database
-# series_name (str): Series name
-# version (int): Version number of series
-# show_comments (bool): True to show comments
-# show_cover_comments (bool): True to show cover-letter comments
-STATE_REQ = namedtuple(
- 'state_req',
- 'link,series_id,series_name,version,show_comments,show_cover_comments')
-
-# Responses from series_get_states()
-# int: ser_ver ID number
-# COVER: Cover-letter info
-# list of Patch: Information on each patch in the series
-# list of dict: patches, see get_series()['patches']
-STATE_RESP = namedtuple('state_resp', 'svid,cover,patches,patch_list')
-
-# Information about a cover-letter on patchwork
-# id (int): Patchwork ID of cover letter
-# state (str): Current state, e.g. 'accepted'
-# num_comments (int): Number of comments
-# name (str): Series name
-# comments (list of dict): Comments
-COVER = namedtuple('cover', 'id,num_comments,name,comments')
-
-# Number of retries
-RETRIES = 3
-
-# Max concurrent request
-MAX_CONCURRENT = 50
-
-# Patches which are part of a multi-patch series are shown with a prefix like
-# [prefix, version, sequence], for example '[RFC, v2, 3/5]'. All but the last
-# part is optional. This decodes the string into groups. For single patches
-# the [] part is not present:
-# Groups: (ignore, ignore, ignore, prefix, version, sequence, subject)
-RE_PATCH = re.compile(r'(\[(((.*),)?(.*),)?(.*)\]\s)?(.*)$')
-
-# This decodes the sequence string into a patch number and patch count
-RE_SEQ = re.compile(r'(\d+)/(\d+)')
-
-
-class Patch(dict):
- """Models a patch in patchwork
-
- This class records information obtained from patchwork
-
- Some of this information comes from the 'Patch' column:
-
- [RFC,v2,1/3] dm: Driver and uclass changes for tiny-dm
-
- This shows the prefix, version, seq, count and subject.
-
- The other properties come from other columns in the display.
-
- Properties:
- pid (str): ID of the patch (typically an integer)
- seq (int): Sequence number within series (1=first) parsed from sequence
- string
- count (int): Number of patches in series, parsed from sequence string
- raw_subject (str): Entire subject line, e.g.
- "[1/2,v2] efi_loader: Sort header file ordering"
- prefix (str): Prefix string or None (e.g. 'RFC')
- version (str): Version string or None (e.g. 'v2')
- raw_subject (str): Raw patch subject
- subject (str): Patch subject with [..] part removed (same as commit
- subject)
- data (dict or None): Patch data:
- """
- def __init__(self, pid, state=None, data=None, comments=None,
- series_data=None):
- super().__init__()
- self.id = pid # Use 'id' to match what the Rest API provides
- self.seq = None
- self.count = None
- self.prefix = None
- self.version = None
- self.raw_subject = None
- self.subject = None
- self.state = state
- self.data = data
- self.comments = comments
- self.series_data = series_data
- self.name = None
-
- # These make us more like a dictionary
- def __setattr__(self, name, value):
- self[name] = value
-
- def __getattr__(self, name):
- return self[name]
-
- def __hash__(self):
- return hash(frozenset(self.items()))
-
- def __str__(self):
- return self.raw_subject
-
- def parse_subject(self, raw_subject):
- """Parse the subject of a patch into its component parts
-
- See RE_PATCH for details. The parsed info is placed into seq, count,
- prefix, version, subject
-
- Args:
- raw_subject (str): Subject string to parse
-
- Raises:
- ValueError: the subject cannot be parsed
- """
- self.raw_subject = raw_subject.strip()
- mat = RE_PATCH.search(raw_subject.strip())
- if not mat:
- raise ValueError(f"Cannot parse subject '{raw_subject}'")
- self.prefix, self.version, seq_info, self.subject = mat.groups()[3:]
- mat_seq = RE_SEQ.match(seq_info) if seq_info else False
- if mat_seq is None:
- self.version = seq_info
- seq_info = None
- if self.version and not self.version.startswith('v'):
- self.prefix = self.version
- self.version = None
- if seq_info:
- if mat_seq:
- self.seq = int(mat_seq.group(1))
- self.count = int(mat_seq.group(2))
- else:
- self.seq = 1
- self.count = 1
-
-
-class Review:
- """Represents a single review email collected in Patchwork
-
- Patches can attract multiple reviews. Each consists of an author/date and
- a variable number of 'snippets', which are groups of quoted and unquoted
- text.
- """
- def __init__(self, meta, snippets):
- """Create new Review object
-
- Args:
- meta (str): Text containing review author and date
- snippets (list): List of snippets in th review, each a list of text
- lines
- """
- self.meta = ' : '.join([line for line in meta.splitlines() if line])
- self.snippets = snippets
-
-
-class Patchwork:
- """Class to handle communication with patchwork
- """
- def __init__(self, url, show_progress=True, single_thread=False):
- """Set up a new patchwork handler
-
- Args:
- url (str): URL of patchwork server, e.g.
- 'https://patchwork.ozlabs.org'
- """
- self.url = url
- self.fake_request = None
- self.proj_id = None
- self.link_name = None
- self._show_progress = show_progress
- self.semaphore = asyncio.Semaphore(
- 1 if single_thread else MAX_CONCURRENT)
- self.request_count = 0
-
- async def _request(self, client, subpath):
- """Call the patchwork API and return the result as JSON
-
- Args:
- client (aiohttp.ClientSession): Session to use
- subpath (str): URL subpath to use
-
- Returns:
- dict: Json result
-
- Raises:
- ValueError: the URL could not be read
- """
- # print('subpath', subpath)
- self.request_count += 1
- if self.fake_request:
- return self.fake_request(subpath)
-
- full_url = f'{self.url}/api/1.2/{subpath}'
- async with self.semaphore:
- # print('full_url', full_url)
- for i in range(RETRIES + 1):
- try:
- async with client.get(full_url) as response:
- if response.status != 200:
- raise ValueError(
- f"Could not read URL '{full_url}'")
- result = await response.json()
- # print('- done', full_url)
- return result
- break
- except aiohttp.client_exceptions.ServerDisconnectedError:
- if i == RETRIES:
- raise
-
- @staticmethod
- def for_testing(func):
- """Get an instance to use for testing
-
- Args:
- func (function): Function to call to handle requests. The function
- is passed a URL and is expected to return a dict with the
- resulting data
-
- Returns:
- Patchwork: testing instance
- """
- pwork = Patchwork(None, show_progress=False)
- pwork.fake_request = func
- return pwork
-
- class _Stats:
- def __init__(self, parent):
- self.parent = parent
- self.request_count = 0
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.request_count = self.parent.request_count
-
- def collect_stats(self):
- """Context manager to count requests across a range of patchwork calls
-
- Usage:
- pwork = Patchwork(...)
- with pwork.count_requests() as counter:
- pwork.something()
- print(f'{counter.count} requests')
- """
- self.request_count = 0
- return self._Stats(self)
-
- async def get_projects(self):
- """Get a list of projects on the server
-
- Returns:
- list of dict, one for each project
- 'name' (str): Project name, e.g. 'U-Boot'
- 'id' (int): Project ID, e.g. 9
- 'link_name' (str): Project's link-name, e.g. 'uboot'
- """
- async with aiohttp.ClientSession() as client:
- return await self._request(client, 'projects/')
-
- async def _query_series(self, client, desc):
- """Query series by name
-
- Args:
- client (aiohttp.ClientSession): Session to use
- desc: String to search for
-
- Return:
- list of series matches, each a dict, see get_series()
- """
- query = desc.replace(' ', '+')
- return await self._request(
- client, f'series/?project={self.proj_id}&q={query}')
-
- async def _find_series(self, client, svid, ser_id, version, ser):
- """Find a series on the server
-
- Args:
- client (aiohttp.ClientSession): Session to use
- svid (int): ser_ver ID
- ser_id (int): series ID
- version (int): Version number to search for
- ser (Series): Contains description (cover-letter title)
-
- Returns:
- tuple:
- int: ser_ver ID (as passed in)
- int: series ID (as passed in)
- str: Series link, or None if not found
- list of dict, or None if found
- each dict is the server result from a possible series
- """
- desc = ser.desc
- name_found = []
-
- # Do a series query on the description
- res = await self._query_series(client, desc)
- for pws in res:
- if pws['name'] == desc:
- if int(pws['version']) == version:
- return svid, ser_id, pws['id'], None
- name_found.append(pws)
-
- # When there is no cover letter, patchwork uses the first patch as the
- # series name
- cmt = ser.commits[0]
-
- res = await self._query_series(client, cmt.subject)
- for pws in res:
- patch = Patch(0)
- patch.parse_subject(pws['name'])
- if patch.subject == cmt.subject:
- if int(pws['version']) == version:
- return svid, ser_id, pws['id'], None
- name_found.append(pws)
-
- return svid, ser_id, None, name_found or res
-
- async def find_series(self, ser, version):
- """Find a series based on its description and version
-
- Args:
- ser (Series): Contains description (cover-letter title)
- version (int): Version number
-
- Return: tuple:
- tuple:
- str: Series ID, or None if not found
- list of dict, or None if found
- each dict is the server result from a possible series
- int: number of server requests done
- """
- async with aiohttp.ClientSession() as client:
- # We don't know the svid and it isn't needed, so use -1
- _, _, link, options = await self._find_series(client, -1, -1,
- version, ser)
- return link, options
-
- async def find_series_list(self, to_find):
- """Find the link for each series in a list
-
- Args:
- to_find (dict of svids to sync):
- key (int): ser_ver ID
- value (tuple):
- int: Series ID
- int: Series version
- str: Series link
- str: Series description
-
- Return: tuple:
- list of tuple, one for each item in to_find:
- int: ser_ver_ID
- int: series ID
- int: Series version
- str: Series link, or None if not found
- list of dict, or None if found
- each dict is the server result from a possible series
- int: number of server requests done
- """
- self.request_count = 0
- async with aiohttp.ClientSession() as client:
- tasks = [asyncio.create_task(
- self._find_series(client, svid, ser_id, version, desc))
- for svid, (ser_id, version, link, desc) in to_find.items()]
- results = await asyncio.gather(*tasks)
-
- return results, self.request_count
-
- def project_set(self, project_id, link_name):
- """Set the project ID
-
- The patchwork server has multiple projects. This allows the ID and
- link_name of the relevant project to be selected
-
- This function is used for testing
-
- Args:
- project_id (int): Project ID to use, e.g. 6
- link_name (str): Name to use for project URL links, e.g. 'uboot'
- """
- self.proj_id = project_id
- self.link_name = link_name
-
- async def get_series(self, client, link):
- """Read information about a series
-
- Args:
- client (aiohttp.ClientSession): Session to use
- link (str): Patchwork series ID
-
- Returns: dict containing patchwork's series information
- id (int): series ID unique across patchwork instance, e.g. 3
- url (str): Full URL, e.g.
- 'https://patchwork.ozlabs.org/api/1.2/series/3/'
- web_url (str): Full URL, e.g.
- 'https://patchwork.ozlabs.org/project/uboot/list/?series=3
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- name (str): Series name, e.g. '[U-Boot] moveconfig: fix error'
- date (str): Date, e.g. '2017-08-27T08:00:51'
- submitter (dict): id, url, name, email, e.g.:
- "id": 6125,
- "url": "https://patchwork.ozlabs.org/api/1.2/people/6125/",
- "name": "Chris Packham",
- "email": "[email protected]"
- version (int): Version number
- total (int): Total number of patches based on subject
- received_total (int): Total patches received by patchwork
- received_all (bool): True if all patches were received
- mbox (str): URL of mailbox, e.g.
- 'https://patchwork.ozlabs.org/series/3/mbox/'
- cover_letter (dict) or None, e.g.:
- "id": 806215,
- "url": "https://patchwork.ozlabs.org/api/1.2/covers/806215/",
- "web_url": "https://patchwork.ozlabs.org/project/uboot/cover/
- "msgid": "<[email protected]>",
- "list_archive_url": null,
- "date": "2017-08-27T09:44:07",
- "name": "[U-Boot,v2,0/4] usb: net: Migrate USB Ethernet",
- "mbox": "https://patchwork.ozlabs.org/project/uboot/cover/
- 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/
- "msgid": "<[email protected]>",
- "list_archive_url": null,
- "date": "2017-08-27T08:00:51",
- "name": "[U-Boot] moveconfig: fix error message do_autoconf()",
- "mbox": "https://patchwork.ozlabs.org/project/uboot/patch/
- """
- 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/
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- "msgid": "<[email protected]>",
- "list_archive_url": null,
- "date": "2008-09-05T07:16:27",
- "name": "powerpc/spufs: Fix possible scheduling of a context",
- "commit_ref": "b2e601d14deb2083e2a537b47869ab3895d23a28",
- "pull_url": null,
- "state": "accepted",
- "archived": false,
- "hash": "bc1c0b80d7cff66c0d1e5f3f8f4d10eb36176f0d",
- "submitter": {
- "id": 93,
- "url": "https://patchwork.ozlabs.org/api/1.2/people/93/",
- "name": "Andre Detsch",
- "email": "[email protected]"
- },
- "delegate": {
- "id": 1,
- "url": "https://patchwork.ozlabs.org/api/1.2/users/1/",
- "username": "jk",
- "first_name": "Jeremy",
- "last_name": "Kerr",
- "email": "[email protected]"
- },
- "mbox": "https://patchwork.ozlabs.org/project/cbe-oss-dev/patch/
- "series": [],
- "comments": "https://patchwork.ozlabs.org/api/patches/185/
- comments/",
- "check": "pending",
- "checks": "https://patchwork.ozlabs.org/api/patches/185/checks/",
- "tags": {},
- "related": [],
- "headers": {...}
- "content": "We currently have a race when scheduling a context
- after we have found a runnable context in spusched_tick, the
- context may have been scheduled by spu_activate().
-
- This may result in a panic if we try to unschedule a context
- been freed in the meantime.
-
- This change exits spu_schedule() if the context has already
- scheduled, so we don't end up scheduling it twice.
-
- Signed-off-by: Andre Detsch <[email protected]>",
- "diff": '''Index: spufs/arch/powerpc/platforms/cell/spufs/sched.c
- =======================================================
- --- spufs.orig/arch/powerpc/platforms/cell/spufs/sched.c
- +++ spufs/arch/powerpc/platforms/cell/spufs/sched.c
- @@ -727,7 +727,8 @@ static void spu_schedule(struct spu *spu
- \t/* not a candidate for interruptible because it's called
- \t from the scheduler thread or from spu_deactivate */
- \tmutex_lock(&ctx->state_mutex);
- -\t__spu_schedule(spu, ctx);
- +\tif (ctx->state == SPU_STATE_SAVED)
- +\t\t__spu_schedule(spu, ctx);
- \tspu_release(ctx);
- }
- '''
- "prefixes": ["3/3", ...]
- """
- return await self._request(client, f'patches/{patch_id}/')
-
- async def _get_patch_comments(self, client, patch_id):
- """Read comments about a patch
-
- Args:
- client (aiohttp.ClientSession): Session to use
- patch_id (str): Patchwork patch ID
-
- Returns: list of dict: list of comments:
- id (int): series ID unique across patchwork instance, e.g. 3331924
- web_url (str): Full URL, e.g.
- 'https://patchwork.ozlabs.org/comment/3331924/'
- msgid (str): Message ID, e.g.
- list_archive_url: (unknown?)
- date (str): Date, e.g. '2024-06-20T13:38:03'
- subject (str): email subject, e.g. 'Re: [PATCH 3/5] buildman:
- Support building within a Python venv'
- date (str): Date, e.g. '2017-08-27T08:00:51'
- submitter (dict): id, url, name, email, e.g.:
- "id": 61270,
- "url": "https://patchwork.ozlabs.org/api/people/61270/",
- "name": "Heinrich Schuchardt",
- "email": "[email protected]"
- content (str): Content of email, e.g. 'On 20.06.24 15:19,
- Simon Glass wrote:
- >...'
- headers: dict: email headers, see get_cover() for an example
- """
- return await self._request(client, f'patches/{patch_id}/comments/')
-
- async def get_cover(self, client, cover_id):
- """Read information about a cover letter
-
- Args:
- client (aiohttp.ClientSession): Session to use
- cover_id (int): Patchwork cover-letter ID
-
- Returns: dict containing patchwork's cover-letter information:
- id (int): series ID unique across patchwork instance, e.g. 3
- url (str): Full URL, e.g. https://patchwork.ozlabs.org/project/uboot/list/?series=3
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/api/1.2/covers/2054866/'
- web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/[email protected]/'
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- msgid (str): Message ID, e.g. '[email protected]>'
- list_archive_url (?)
- date (str): Date, e.g. '2017-08-27T08:00:51'
- name (str): Series name, e.g. '[U-Boot] moveconfig: fix error'
- submitter (dict): id, url, name, email, e.g.:
- "id": 6170,
- "url": "https://patchwork.ozlabs.org/api/1.2/people/6170/",
- "name": "Simon Glass",
- "email": "[email protected]"
- mbox (str): URL to mailbox, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/[email protected]/mbox/'
- series (list of dict) each e.g.:
- "id": 446956,
- "url": "https://patchwork.ozlabs.org/api/1.2/series/446956/",
- "web_url": "https://patchwork.ozlabs.org/project/uboot/list/?series=446956",
- "date": "2025-03-04T13:09:37",
- "name": "binman: Check code-coverage requirements",
- "version": 1,
- "mbox": "https://patchwork.ozlabs.org/series/446956/mbox/"
- comments: Web URL to comments: 'https://patchwork.ozlabs.org/api/covers/2054866/comments/'
- headers: dict: e.g.:
- "Return-Path": "<[email protected]>",
- "X-Original-To": "[email protected]",
- "Delivered-To": "[email protected]",
- "Authentication-Results": [
- "legolas.ozlabs.org;
-\tdkim=pass (1024-bit key;
- unprotected) header.d=chromium.org [email protected] header.a=rsa-sha256
- header.s=google header.b=dG8yqtoK;
-\tdkim-atps=neutral",
- "legolas.ozlabs.org;
- spf=pass (sender SPF authorized) smtp.mailfrom=lists.denx.de
- (client-ip=85.214.62.61; helo=phobos.denx.de;
- [email protected]; receiver=patchwork.ozlabs.org)",
- "phobos.denx.de;
- dmarc=pass (p=none dis=none) header.from=chromium.org",
- "phobos.denx.de;
- spf=pass [email protected]",
- "phobos.denx.de;
-\tdkim=pass (1024-bit key;
- unprotected) header.d=chromium.org [email protected]
- header.b=\"dG8yqtoK\";
-\tdkim-atps=neutral",
- "phobos.denx.de;
- dmarc=pass (p=none dis=none) header.from=chromium.org",
- "phobos.denx.de;
- spf=pass [email protected]"
- ],
- "Received": [
- "from phobos.denx.de (phobos.denx.de [85.214.62.61])
-\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
-\t key-exchange X25519 server-signature ECDSA (secp384r1))
-\t(No client certificate requested)
-\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4Z6bd50jLhz1yD0
-\tfor <[email protected]>; Wed, 5 Mar 2025 00:10:00 +1100 (AEDT)",
- "from h2850616.stratoserver.net (localhost [IPv6:::1])
-\tby phobos.denx.de (Postfix) with ESMTP id 434E88144A;
-\tTue, 4 Mar 2025 14:09:58 +0100 (CET)",
- "by phobos.denx.de (Postfix, from userid 109)
- id 8CBF98144A; Tue, 4 Mar 2025 14:09:57 +0100 (CET)",
- "from mail-io1-xd2e.google.com (mail-io1-xd2e.google.com
- [IPv6:2607:f8b0:4864:20::d2e])
- (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits))
- (No client certificate requested)
- by phobos.denx.de (Postfix) with ESMTPS id 48AE281426
- for <[email protected]>; Tue, 4 Mar 2025 14:09:55 +0100 (CET)",
- "by mail-io1-xd2e.google.com with SMTP id
- ca18e2360f4ac-85ae33109f6so128326139f.2
- for <[email protected]>; Tue, 04 Mar 2025 05:09:55 -0800 (PST)",
- "from chromium.org (c-73-203-119-151.hsd1.co.comcast.net.
- [73.203.119.151]) by smtp.gmail.com with ESMTPSA id
- ca18e2360f4ac-858753cd304sm287383839f.33.2025.03.04.05.09.49
- (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
- Tue, 04 Mar 2025 05:09:50 -0800 (PST)"
- ],
- "X-Spam-Checker-Version": "SpamAssassin 3.4.2 (2018-09-13) on phobos.denx.de",
- "X-Spam-Level": "",
- "X-Spam-Status": "No, score=-2.1 required=5.0 tests=BAYES_00,DKIMWL_WL_HIGH,
- DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,
- RCVD_IN_DNSWL_BLOCKED,SPF_HELO_NONE,SPF_PASS autolearn=ham
- autolearn_force=no version=3.4.2",
- "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;
- d=chromium.org; s=google; t=1741093792; x=1741698592; darn=lists.denx.de;
- h=content-transfer-encoding:mime-version:message-id:date:subject:cc
- :to:from:from:to:cc:subject:date:message-id:reply-to;
- bh=B2zsLws430/BEZfatNjeaNnrcxmYUstVjp1pSXgNQjc=;
- b=dG8yqtoKpSy15RHagnPcppzR8KbFCRXa2OBwXfwGoyN6M15tOJsUu2tpCdBFYiL5Mk
- hQz5iDLV8p0Bs+fP4XtNEx7KeYfTZhiqcRFvdCLwYtGray/IHtOZaNoHLajrstic/OgE
- 01ymu6gOEboU32eQ8uC8pdCYQ4UCkfKJwmiiU=",
- "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;
- d=1e100.net; s=20230601; t=1741093792; x=1741698592;
- h=content-transfer-encoding:mime-version:message-id:date:subject:cc
- :to:from:x-gm-message-state:from:to:cc:subject:date:message-id
- :reply-to;
- bh=B2zsLws430/BEZfatNjeaNnrcxmYUstVjp1pSXgNQjc=;
- b=eihzJf4i9gin9usvz4hnAvvbLV9/yB7hGPpwwW/amgnPUyWCeQstgvGL7WDLYYnukH
- 161p4mt7+cCj7Hao/jSPvVZeuKiBNPkS4YCuP3QjXfdk2ziQ9IjloVmGarWZUOlYJ5iQ
- dZnxypUkuFfLcEDSwUmRO1dvLi3nH8PDlae3yT2H87LeHaxhXWdzHxQdPc86rkYyCqCr
- qBC2CTS31jqSuiaI+7qB3glvbJbSEXkunz0iDewTJDvZfmuloxTipWUjRJ1mg9UJcZt5
- 9xIuTq1n9aYf1RcQlrEOQhdBAQ0/IJgvmZtzPZi9L+ppBva1ER/xm06nMA7GEUtyGwun
- c6pA==",
- "X-Gm-Message-State": "AOJu0Yybx3b1+yClf/IfIbQd9u8sxzK9ixPP2HimXF/dGZfSiS7Cb+O5
- WrAkvtp7m3KPM/Mpv0sSZ5qrfTnKnb3WZyv6Oe5Q1iUjAftGNwbSxob5eJ/0y3cgrTdzE4sIWPE
- =",
- "X-Gm-Gg": "ASbGncu5gtgpXEPGrpbTRJulqFrFj1YPAAmKk4MiXA8/3J1A+25F0Uug2KeFUrZEjkG
- KMdPg/C7e2emIvfM+Jl+mKv0ITBvhbyNCyY1q2U1s1cayZF05coZ9ewzGxXJGiEqLMG69uBmmIi
- rBEvCnkXS+HVZobDQMtOsezpc+Ju8JRA7+y1R0WIlutl1mQARct6p0zTkuZp75QyB6dm/d0KYgd
- iux/t/f0HC2CxstQlTlJYzKL6UJgkB5/UorY1lW/0NDRS6P1iemPQ7I3EPLJO8tM5ZrpJE7qgNP
- xy0jXbUv44c48qJ1VszfY5USB8fRG7nwUYxNu6N1PXv9xWbl+z2xL68qNYUrFlHsB8ILTXAyzyr
- Cdj+Sxg==",
- "X-Google-Smtp-Source": "
- AGHT+IFeVk5D4YEfJgPxOfg3ikO6Q7IhaDzABGkAPI6HA0ubK85OPhUHK08gV7enBQ8OdoE/ttqEjw==",
- "X-Received": "by 2002:a05:6602:640f:b0:855:63c8:abb5 with SMTP id
- ca18e2360f4ac-85881fdba3amr1839428939f.13.1741093792636;
- Tue, 04 Mar 2025 05:09:52 -0800 (PST)",
- "From": "Simon Glass <[email protected]>",
- "To": "U-Boot Mailing List <[email protected]>",
- "Cc": "Simon Glass <[email protected]>, Alexander Kochetkov <[email protected]>,
- Alper Nebi Yasak <[email protected]>,
- Brandon Maier <[email protected]>,
- Jerome Forissier <[email protected]>,
- Jiaxun Yang <[email protected]>,
- Neha Malcom Francis <[email protected]>,
- Patrick Rudolph <[email protected]>,
- Paul HENRYS <[email protected]>, Peng Fan <[email protected]>,
- Philippe Reynes <[email protected]>,
- Stefan Herbrechtsmeier <[email protected]>,
- Tom Rini <[email protected]>",
- "Subject": "[PATCH 0/7] binman: Check code-coverage requirements",
- "Date": "Tue, 4 Mar 2025 06:09:37 -0700",
- "Message-ID": "<[email protected]>",
- "X-Mailer": "git-send-email 2.43.0",
- "MIME-Version": "1.0",
- "Content-Transfer-Encoding": "8bit",
- "X-BeenThere": "[email protected]",
- "X-Mailman-Version": "2.1.39",
- "Precedence": "list",
- "List-Id": "U-Boot discussion <u-boot.lists.denx.de>",
- "List-Unsubscribe": "<https://lists.denx.de/options/u-boot>,
- <mailto:[email protected]?subject=unsubscribe>",
- "List-Archive": "<https://lists.denx.de/pipermail/u-boot/>",
- "List-Post": "<mailto:[email protected]>",
- "List-Help": "<mailto:[email protected]?subject=help>",
- "List-Subscribe": "<https://lists.denx.de/listinfo/u-boot>,
- <mailto:[email protected]?subject=subscribe>",
- "Errors-To": "[email protected]",
- "Sender": "\"U-Boot\" <[email protected]>",
- "X-Virus-Scanned": "clamav-milter 0.103.8 at phobos.denx.de",
- "X-Virus-Status": "Clean"
- content (str): Email content, e.g. 'This series adds a cover-coverage check to CI for Binman. The iMX8 tests
-are still not completed,...'
- """
- async with aiohttp.ClientSession() as client:
- return await self._request(client, f'covers/{cover_id}/')
-
- async def get_cover_comments(self, client, cover_id):
- """Read comments about a cover letter
-
- Args:
- client (aiohttp.ClientSession): Session to use
- cover_id (str): Patchwork cover-letter ID
-
- Returns: list of dict: list of comments, each:
- id (int): series ID unique across patchwork instance, e.g. 3472068
- web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/comment/3472068/'
- list_archive_url: (unknown?)
-
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/api/1.2/covers/2054866/'
- web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/[email protected]/'
- project (dict): project information (id, url, name, link_name,
- list_id, list_email, etc.
- date (str): Date, e.g. '2025-03-04T13:16:15'
- subject (str): 'Re: [PATCH 0/7] binman: Check code-coverage requirements'
- submitter (dict): id, url, name, email, e.g.:
- "id": 6170,
- "url": "https://patchwork.ozlabs.org/api/people/6170/",
- "name": "Simon Glass",
- "email": "[email protected]"
- content (str): Email content, e.g. 'Hi,
-
-On Tue, 4 Mar 2025 at 06:09, Simon Glass <[email protected]> wrote:
->
-> This '...
- headers: dict: email headers, see get_cover() for an example
- """
- return await self._request(client, f'covers/{cover_id}/comments/')
-
- async def get_series_url(self, link):
- """Get the URL for a series
-
- Args:
- link (str): Patchwork series ID
-
- Returns:
- str: URL for the series page
- """
- return f'{self.url}/project/{self.link_name}/list/?series={link}&state=*&archive=both'
-
- async def _get_patch_status(self, client, patch_id):
- """Get the patch status
-
- Args:
- client (aiohttp.ClientSession): Session to use
- patch_id (int): Patch ID to look up in patchwork
-
- Return:
- PATCH: Patch information
-
- Requests:
- 1 for patch, 1 for patch comments
- """
- data = await self.get_patch(client, patch_id)
- state = data['state']
- comment_data = await self._get_patch_comments(client, patch_id)
-
- return Patch(patch_id, state, data, comment_data)
-
- async def get_series_cover(self, client, data):
- """Get the cover information (including comments)
-
- Args:
- client (aiohttp.ClientSession): Session to use
- data (dict): Return value from self.get_series()
-
- Returns:
- COVER object, or None if no cover letter
- """
- # Patchwork should always provide this, but use get() so that we don't
- # have to provide it in our fake patchwork _fake_patchwork_cser()
- cover = data.get('cover_letter')
- cover_id = None
- if cover:
- cover_id = cover['id']
- info = await self.get_cover_comments(client, cover_id)
- cover = COVER(cover_id, len(info), cover['name'], info)
- return cover
-
- async def series_get_state(self, client, link, read_comments,
- read_cover_comments):
- """Sync the series information against patchwork, to find patch status
-
- Args:
- client (aiohttp.ClientSession): Session to use
- link (str): Patchwork series ID
- read_comments (bool): True to read the comments on the patches
- read_cover_comments (bool): True to read the comments on the cover
- letter
-
- Return: tuple:
- COVER object, or None if none or not read_cover_comments
- list of PATCH objects
- """
- data = await self.get_series(client, link)
- patch_list = list(data['patches'])
-
- count = len(patch_list)
- patches = []
- if read_comments:
- # Returns a list of Patch objects
- tasks = [self._get_patch_status(client, patch_list[i]['id'])
- for i in range(count)]
-
- patch_status = await asyncio.gather(*tasks)
- for patch_data, status in zip(patch_list, patch_status):
- status.series_data = patch_data
- patches.append(status)
- else:
- for i in range(count):
- info = patch_list[i]
- pat = Patch(info['id'], series_data=info)
- pat.raw_subject = info['name']
- patches.append(pat)
- if self._show_progress:
- terminal.print_clear()
-
- if read_cover_comments:
- cover = await self.get_series_cover(client, data)
- else:
- cover = None
-
- return cover, patches