1# SPDX-License-Identifier: GPL-2.0+ 2# 3# Copyright 2025 Simon Glass <sjg@chromium.org> 4# 5"""Provides a basic API for the patchwork server 6""" 7 8import asyncio 9import re 10 11import aiohttp 12from collections import namedtuple 13 14from u_boot_pylib import terminal 15 16# Information passed to series_get_states() 17# link (str): Patchwork link for series 18# series_id (int): Series ID in database 19# series_name (str): Series name 20# version (int): Version number of series 21# show_comments (bool): True to show comments 22# show_cover_comments (bool): True to show cover-letter comments 23STATE_REQ = namedtuple( 24 'state_req', 25 'link,series_id,series_name,version,show_comments,show_cover_comments') 26 27# Responses from series_get_states() 28# int: ser_ver ID number 29# COVER: Cover-letter info 30# list of Patch: Information on each patch in the series 31# list of dict: patches, see get_series()['patches'] 32STATE_RESP = namedtuple('state_resp', 'svid,cover,patches,patch_list') 33 34# Information about a cover-letter on patchwork 35# id (int): Patchwork ID of cover letter 36# state (str): Current state, e.g. 'accepted' 37# num_comments (int): Number of comments 38# name (str): Series name 39# comments (list of dict): Comments 40COVER = namedtuple('cover', 'id,num_comments,name,comments') 41 42# Number of retries 43RETRIES = 3 44 45# Max concurrent request 46MAX_CONCURRENT = 50 47 48# Patches which are part of a multi-patch series are shown with a prefix like 49# [prefix, version, sequence], for example '[RFC, v2, 3/5]'. All but the last 50# part is optional. This decodes the string into groups. For single patches 51# the [] part is not present: 52# Groups: (ignore, ignore, ignore, prefix, version, sequence, subject) 53RE_PATCH = re.compile(r'(\[(((.*),)?(.*),)?(.*)\]\s)?(.*)$') 54 55# This decodes the sequence string into a patch number and patch count 56RE_SEQ = re.compile(r'(\d+)/(\d+)') 57 58 59class Patch(dict): 60 """Models a patch in patchwork 61 62 This class records information obtained from patchwork 63 64 Some of this information comes from the 'Patch' column: 65 66 [RFC,v2,1/3] dm: Driver and uclass changes for tiny-dm 67 68 This shows the prefix, version, seq, count and subject. 69 70 The other properties come from other columns in the display. 71 72 Properties: 73 pid (str): ID of the patch (typically an integer) 74 seq (int): Sequence number within series (1=first) parsed from sequence 75 string 76 count (int): Number of patches in series, parsed from sequence string 77 raw_subject (str): Entire subject line, e.g. 78 "[1/2,v2] efi_loader: Sort header file ordering" 79 prefix (str): Prefix string or None (e.g. 'RFC') 80 version (str): Version string or None (e.g. 'v2') 81 raw_subject (str): Raw patch subject 82 subject (str): Patch subject with [..] part removed (same as commit 83 subject) 84 data (dict or None): Patch data: 85 """ 86 def __init__(self, pid, state=None, data=None, comments=None, 87 series_data=None): 88 super().__init__() 89 self.id = pid # Use 'id' to match what the Rest API provides 90 self.seq = None 91 self.count = None 92 self.prefix = None 93 self.version = None 94 self.raw_subject = None 95 self.subject = None 96 self.state = state 97 self.data = data 98 self.comments = comments 99 self.series_data = series_data 100 self.name = None 101 102 # These make us more like a dictionary 103 def __setattr__(self, name, value): 104 self[name] = value 105 106 def __getattr__(self, name): 107 return self[name] 108 109 def __hash__(self): 110 return hash(frozenset(self.items())) 111 112 def __str__(self): 113 return self.raw_subject 114 115 def parse_subject(self, raw_subject): 116 """Parse the subject of a patch into its component parts 117 118 See RE_PATCH for details. The parsed info is placed into seq, count, 119 prefix, version, subject 120 121 Args: 122 raw_subject (str): Subject string to parse 123 124 Raises: 125 ValueError: the subject cannot be parsed 126 """ 127 self.raw_subject = raw_subject.strip() 128 mat = RE_PATCH.search(raw_subject.strip()) 129 if not mat: 130 raise ValueError(f"Cannot parse subject '{raw_subject}'") 131 self.prefix, self.version, seq_info, self.subject = mat.groups()[3:] 132 mat_seq = RE_SEQ.match(seq_info) if seq_info else False 133 if mat_seq is None: 134 self.version = seq_info 135 seq_info = None 136 if self.version and not self.version.startswith('v'): 137 self.prefix = self.version 138 self.version = None 139 if seq_info: 140 if mat_seq: 141 self.seq = int(mat_seq.group(1)) 142 self.count = int(mat_seq.group(2)) 143 else: 144 self.seq = 1 145 self.count = 1 146 147 148class Review: 149 """Represents a single review email collected in Patchwork 150 151 Patches can attract multiple reviews. Each consists of an author/date and 152 a variable number of 'snippets', which are groups of quoted and unquoted 153 text. 154 """ 155 def __init__(self, meta, snippets): 156 """Create new Review object 157 158 Args: 159 meta (str): Text containing review author and date 160 snippets (list): List of snippets in th review, each a list of text 161 lines 162 """ 163 self.meta = ' : '.join([line for line in meta.splitlines() if line]) 164 self.snippets = snippets 165 166 167class Patchwork: 168 """Class to handle communication with patchwork 169 """ 170 def __init__(self, url, show_progress=True, single_thread=False): 171 """Set up a new patchwork handler 172 173 Args: 174 url (str): URL of patchwork server, e.g. 175 'https://patchwork.ozlabs.org' 176 """ 177 self.url = url 178 self.fake_request = None 179 self.proj_id = None 180 self.link_name = None 181 self._show_progress = show_progress 182 self.semaphore = asyncio.Semaphore( 183 1 if single_thread else MAX_CONCURRENT) 184 self.request_count = 0 185 186 async def _request(self, client, subpath): 187 """Call the patchwork API and return the result as JSON 188 189 Args: 190 client (aiohttp.ClientSession): Session to use 191 subpath (str): URL subpath to use 192 193 Returns: 194 dict: Json result 195 196 Raises: 197 ValueError: the URL could not be read 198 """ 199 # print('subpath', subpath) 200 self.request_count += 1 201 if self.fake_request: 202 return self.fake_request(subpath) 203 204 full_url = f'{self.url}/api/1.2/{subpath}' 205 async with self.semaphore: 206 # print('full_url', full_url) 207 for i in range(RETRIES + 1): 208 try: 209 async with client.get(full_url) as response: 210 if response.status != 200: 211 raise ValueError( 212 f"Could not read URL '{full_url}'") 213 result = await response.json() 214 # print('- done', full_url) 215 return result 216 break 217 except aiohttp.client_exceptions.ServerDisconnectedError: 218 if i == RETRIES: 219 raise 220 221 @staticmethod 222 def for_testing(func): 223 """Get an instance to use for testing 224 225 Args: 226 func (function): Function to call to handle requests. The function 227 is passed a URL and is expected to return a dict with the 228 resulting data 229 230 Returns: 231 Patchwork: testing instance 232 """ 233 pwork = Patchwork(None, show_progress=False) 234 pwork.fake_request = func 235 return pwork 236 237 class _Stats: 238 def __init__(self, parent): 239 self.parent = parent 240 self.request_count = 0 241 242 def __enter__(self): 243 return self 244 245 def __exit__(self, exc_type, exc_val, exc_tb): 246 self.request_count = self.parent.request_count 247 248 def collect_stats(self): 249 """Context manager to count requests across a range of patchwork calls 250 251 Usage: 252 pwork = Patchwork(...) 253 with pwork.count_requests() as counter: 254 pwork.something() 255 print(f'{counter.count} requests') 256 """ 257 self.request_count = 0 258 return self._Stats(self) 259 260 async def get_projects(self): 261 """Get a list of projects on the server 262 263 Returns: 264 list of dict, one for each project 265 'name' (str): Project name, e.g. 'U-Boot' 266 'id' (int): Project ID, e.g. 9 267 'link_name' (str): Project's link-name, e.g. 'uboot' 268 """ 269 async with aiohttp.ClientSession() as client: 270 return await self._request(client, 'projects/') 271 272 async def _query_series(self, client, desc): 273 """Query series by name 274 275 Args: 276 client (aiohttp.ClientSession): Session to use 277 desc: String to search for 278 279 Return: 280 list of series matches, each a dict, see get_series() 281 """ 282 query = desc.replace(' ', '+') 283 return await self._request( 284 client, f'series/?project={self.proj_id}&q={query}') 285 286 async def _find_series(self, client, svid, ser_id, version, ser): 287 """Find a series on the server 288 289 Args: 290 client (aiohttp.ClientSession): Session to use 291 svid (int): ser_ver ID 292 ser_id (int): series ID 293 version (int): Version number to search for 294 ser (Series): Contains description (cover-letter title) 295 296 Returns: 297 tuple: 298 int: ser_ver ID (as passed in) 299 int: series ID (as passed in) 300 str: Series link, or None if not found 301 list of dict, or None if found 302 each dict is the server result from a possible series 303 """ 304 desc = ser.desc 305 name_found = [] 306 307 # Do a series query on the description 308 res = await self._query_series(client, desc) 309 for pws in res: 310 if pws['name'] == desc: 311 if int(pws['version']) == version: 312 return svid, ser_id, pws['id'], None 313 name_found.append(pws) 314 315 # When there is no cover letter, patchwork uses the first patch as the 316 # series name 317 cmt = ser.commits[0] 318 319 res = await self._query_series(client, cmt.subject) 320 for pws in res: 321 patch = Patch(0) 322 patch.parse_subject(pws['name']) 323 if patch.subject == cmt.subject: 324 if int(pws['version']) == version: 325 return svid, ser_id, pws['id'], None 326 name_found.append(pws) 327 328 return svid, ser_id, None, name_found or res 329 330 async def find_series(self, ser, version): 331 """Find a series based on its description and version 332 333 Args: 334 ser (Series): Contains description (cover-letter title) 335 version (int): Version number 336 337 Return: tuple: 338 tuple: 339 str: Series ID, or None if not found 340 list of dict, or None if found 341 each dict is the server result from a possible series 342 int: number of server requests done 343 """ 344 async with aiohttp.ClientSession() as client: 345 # We don't know the svid and it isn't needed, so use -1 346 _, _, link, options = await self._find_series(client, -1, -1, 347 version, ser) 348 return link, options 349 350 async def find_series_list(self, to_find): 351 """Find the link for each series in a list 352 353 Args: 354 to_find (dict of svids to sync): 355 key (int): ser_ver ID 356 value (tuple): 357 int: Series ID 358 int: Series version 359 str: Series link 360 str: Series description 361 362 Return: tuple: 363 list of tuple, one for each item in to_find: 364 int: ser_ver_ID 365 int: series ID 366 int: Series version 367 str: Series link, or None if not found 368 list of dict, or None if found 369 each dict is the server result from a possible series 370 int: number of server requests done 371 """ 372 self.request_count = 0 373 async with aiohttp.ClientSession() as client: 374 tasks = [asyncio.create_task( 375 self._find_series(client, svid, ser_id, version, desc)) 376 for svid, (ser_id, version, link, desc) in to_find.items()] 377 results = await asyncio.gather(*tasks) 378 379 return results, self.request_count 380 381 def project_set(self, project_id, link_name): 382 """Set the project ID 383 384 The patchwork server has multiple projects. This allows the ID and 385 link_name of the relevant project to be selected 386 387 This function is used for testing 388 389 Args: 390 project_id (int): Project ID to use, e.g. 6 391 link_name (str): Name to use for project URL links, e.g. 'uboot' 392 """ 393 self.proj_id = project_id 394 self.link_name = link_name 395 396 async def get_series(self, client, link): 397 """Read information about a series 398 399 Args: 400 client (aiohttp.ClientSession): Session to use 401 link (str): Patchwork series ID 402 403 Returns: dict containing patchwork's series information 404 id (int): series ID unique across patchwork instance, e.g. 3 405 url (str): Full URL, e.g. 406 'https://patchwork.ozlabs.org/api/1.2/series/3/' 407 web_url (str): Full URL, e.g. 408 'https://patchwork.ozlabs.org/project/uboot/list/?series=3 409 project (dict): project information (id, url, name, link_name, 410 list_id, list_email, etc. 411 name (str): Series name, e.g. '[U-Boot] moveconfig: fix error' 412 date (str): Date, e.g. '2017-08-27T08:00:51' 413 submitter (dict): id, url, name, email, e.g.: 414 "id": 6125, 415 "url": "https://patchwork.ozlabs.org/api/1.2/people/6125/", 416 "name": "Chris Packham", 417 "email": "judge.packham@gmail.com" 418 version (int): Version number 419 total (int): Total number of patches based on subject 420 received_total (int): Total patches received by patchwork 421 received_all (bool): True if all patches were received 422 mbox (str): URL of mailbox, e.g. 423 'https://patchwork.ozlabs.org/series/3/mbox/' 424 cover_letter (dict) or None, e.g.: 425 "id": 806215, 426 "url": "https://patchwork.ozlabs.org/api/1.2/covers/806215/", 427 "web_url": "https://patchwork.ozlabs.org/project/uboot/cover/ 428 20170827094411.8583-1-judge.packham@gmail.com/", 429 "msgid": "<20170827094411.8583-1-judge.packham@gmail.com>", 430 "list_archive_url": null, 431 "date": "2017-08-27T09:44:07", 432 "name": "[U-Boot,v2,0/4] usb: net: Migrate USB Ethernet", 433 "mbox": "https://patchwork.ozlabs.org/project/uboot/cover/ 434 20170827094411.8583-1-judge.packham@gmail.com/mbox/" 435 patches (list of dict), each e.g.: 436 "id": 806202, 437 "url": "https://patchwork.ozlabs.org/api/1.2/patches/806202/", 438 "web_url": "https://patchwork.ozlabs.org/project/uboot/patch/ 439 20170827080051.816-1-judge.packham@gmail.com/", 440 "msgid": "<20170827080051.816-1-judge.packham@gmail.com>", 441 "list_archive_url": null, 442 "date": "2017-08-27T08:00:51", 443 "name": "[U-Boot] moveconfig: fix error message do_autoconf()", 444 "mbox": "https://patchwork.ozlabs.org/project/uboot/patch/ 445 20170827080051.816-1-judge.packham@gmail.com/mbox/" 446 """ 447 return await self._request(client, f'series/{link}/') 448 449 async def get_patch(self, client, patch_id): 450 """Read information about a patch 451 452 Args: 453 client (aiohttp.ClientSession): Session to use 454 patch_id (str): Patchwork patch ID 455 456 Returns: dict containing patchwork's patch information 457 "id": 185, 458 "url": "https://patchwork.ozlabs.org/api/1.2/patches/185/", 459 "web_url": "https://patchwork.ozlabs.org/project/cbe-oss-dev/patch/ 460 200809050416.27831.adetsch@br.ibm.com/", 461 project (dict): project information (id, url, name, link_name, 462 list_id, list_email, etc. 463 "msgid": "<200809050416.27831.adetsch@br.ibm.com>", 464 "list_archive_url": null, 465 "date": "2008-09-05T07:16:27", 466 "name": "powerpc/spufs: Fix possible scheduling of a context", 467 "commit_ref": "b2e601d14deb2083e2a537b47869ab3895d23a28", 468 "pull_url": null, 469 "state": "accepted", 470 "archived": false, 471 "hash": "bc1c0b80d7cff66c0d1e5f3f8f4d10eb36176f0d", 472 "submitter": { 473 "id": 93, 474 "url": "https://patchwork.ozlabs.org/api/1.2/people/93/", 475 "name": "Andre Detsch", 476 "email": "adetsch@br.ibm.com" 477 }, 478 "delegate": { 479 "id": 1, 480 "url": "https://patchwork.ozlabs.org/api/1.2/users/1/", 481 "username": "jk", 482 "first_name": "Jeremy", 483 "last_name": "Kerr", 484 "email": "jk@ozlabs.org" 485 }, 486 "mbox": "https://patchwork.ozlabs.org/project/cbe-oss-dev/patch/ 487 200809050416.27831.adetsch@br.ibm.com/mbox/", 488 "series": [], 489 "comments": "https://patchwork.ozlabs.org/api/patches/185/ 490 comments/", 491 "check": "pending", 492 "checks": "https://patchwork.ozlabs.org/api/patches/185/checks/", 493 "tags": {}, 494 "related": [], 495 "headers": {...} 496 "content": "We currently have a race when scheduling a context 497 after we have found a runnable context in spusched_tick, the 498 context may have been scheduled by spu_activate(). 499 500 This may result in a panic if we try to unschedule a context 501 been freed in the meantime. 502 503 This change exits spu_schedule() if the context has already 504 scheduled, so we don't end up scheduling it twice. 505 506 Signed-off-by: Andre Detsch <adetsch@br.ibm.com>", 507 "diff": '''Index: spufs/arch/powerpc/platforms/cell/spufs/sched.c 508 ======================================================= 509 --- spufs.orig/arch/powerpc/platforms/cell/spufs/sched.c 510 +++ spufs/arch/powerpc/platforms/cell/spufs/sched.c 511 @@ -727,7 +727,8 @@ static void spu_schedule(struct spu *spu 512 \t/* not a candidate for interruptible because it's called 513 \t from the scheduler thread or from spu_deactivate */ 514 \tmutex_lock(&ctx->state_mutex); 515 -\t__spu_schedule(spu, ctx); 516 +\tif (ctx->state == SPU_STATE_SAVED) 517 +\t\t__spu_schedule(spu, ctx); 518 \tspu_release(ctx); 519 } 520 ''' 521 "prefixes": ["3/3", ...] 522 """ 523 return await self._request(client, f'patches/{patch_id}/') 524 525 async def _get_patch_comments(self, client, patch_id): 526 """Read comments about a patch 527 528 Args: 529 client (aiohttp.ClientSession): Session to use 530 patch_id (str): Patchwork patch ID 531 532 Returns: list of dict: list of comments: 533 id (int): series ID unique across patchwork instance, e.g. 3331924 534 web_url (str): Full URL, e.g. 535 'https://patchwork.ozlabs.org/comment/3331924/' 536 msgid (str): Message ID, e.g. 537 '<d2526c98-8198-4b8b-ab10-20bda0151da1@gmx.de>' 538 list_archive_url: (unknown?) 539 date (str): Date, e.g. '2024-06-20T13:38:03' 540 subject (str): email subject, e.g. 'Re: [PATCH 3/5] buildman: 541 Support building within a Python venv' 542 date (str): Date, e.g. '2017-08-27T08:00:51' 543 submitter (dict): id, url, name, email, e.g.: 544 "id": 61270, 545 "url": "https://patchwork.ozlabs.org/api/people/61270/", 546 "name": "Heinrich Schuchardt", 547 "email": "xypron.glpk@gmx.de" 548 content (str): Content of email, e.g. 'On 20.06.24 15:19, 549 Simon Glass wrote: 550 >...' 551 headers: dict: email headers, see get_cover() for an example 552 """ 553 return await self._request(client, f'patches/{patch_id}/comments/') 554 555 async def get_cover(self, client, cover_id): 556 """Read information about a cover letter 557 558 Args: 559 client (aiohttp.ClientSession): Session to use 560 cover_id (int): Patchwork cover-letter ID 561 562 Returns: dict containing patchwork's cover-letter information: 563 id (int): series ID unique across patchwork instance, e.g. 3 564 url (str): Full URL, e.g. https://patchwork.ozlabs.org/project/uboot/list/?series=3 565 project (dict): project information (id, url, name, link_name, 566 list_id, list_email, etc. 567 url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/api/1.2/covers/2054866/' 568 web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/20250304130947.109799-1-sjg@chromium.org/' 569 project (dict): project information (id, url, name, link_name, 570 list_id, list_email, etc. 571 msgid (str): Message ID, e.g. '20250304130947.109799-1-sjg@chromium.org>' 572 list_archive_url (?) 573 date (str): Date, e.g. '2017-08-27T08:00:51' 574 name (str): Series name, e.g. '[U-Boot] moveconfig: fix error' 575 submitter (dict): id, url, name, email, e.g.: 576 "id": 6170, 577 "url": "https://patchwork.ozlabs.org/api/1.2/people/6170/", 578 "name": "Simon Glass", 579 "email": "sjg@chromium.org" 580 mbox (str): URL to mailbox, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/20250304130947.109799-1-sjg@chromium.org/mbox/' 581 series (list of dict) each e.g.: 582 "id": 446956, 583 "url": "https://patchwork.ozlabs.org/api/1.2/series/446956/", 584 "web_url": "https://patchwork.ozlabs.org/project/uboot/list/?series=446956", 585 "date": "2025-03-04T13:09:37", 586 "name": "binman: Check code-coverage requirements", 587 "version": 1, 588 "mbox": "https://patchwork.ozlabs.org/series/446956/mbox/" 589 comments: Web URL to comments: 'https://patchwork.ozlabs.org/api/covers/2054866/comments/' 590 headers: dict: e.g.: 591 "Return-Path": "<u-boot-bounces@lists.denx.de>", 592 "X-Original-To": "incoming@patchwork.ozlabs.org", 593 "Delivered-To": "patchwork-incoming@legolas.ozlabs.org", 594 "Authentication-Results": [ 595 "legolas.ozlabs.org; 596\tdkim=pass (1024-bit key; 597 unprotected) header.d=chromium.org header.i=@chromium.org header.a=rsa-sha256 598 header.s=google header.b=dG8yqtoK; 599\tdkim-atps=neutral", 600 "legolas.ozlabs.org; 601 spf=pass (sender SPF authorized) smtp.mailfrom=lists.denx.de 602 (client-ip=85.214.62.61; helo=phobos.denx.de; 603 envelope-from=u-boot-bounces@lists.denx.de; receiver=patchwork.ozlabs.org)", 604 "phobos.denx.de; 605 dmarc=pass (p=none dis=none) header.from=chromium.org", 606 "phobos.denx.de; 607 spf=pass smtp.mailfrom=u-boot-bounces@lists.denx.de", 608 "phobos.denx.de; 609\tdkim=pass (1024-bit key; 610 unprotected) header.d=chromium.org header.i=@chromium.org 611 header.b=\"dG8yqtoK\"; 612\tdkim-atps=neutral", 613 "phobos.denx.de; 614 dmarc=pass (p=none dis=none) header.from=chromium.org", 615 "phobos.denx.de; 616 spf=pass smtp.mailfrom=sjg@chromium.org" 617 ], 618 "Received": [ 619 "from phobos.denx.de (phobos.denx.de [85.214.62.61]) 620\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) 621\t key-exchange X25519 server-signature ECDSA (secp384r1)) 622\t(No client certificate requested) 623\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4Z6bd50jLhz1yD0 624\tfor <incoming@patchwork.ozlabs.org>; Wed, 5 Mar 2025 00:10:00 +1100 (AEDT)", 625 "from h2850616.stratoserver.net (localhost [IPv6:::1]) 626\tby phobos.denx.de (Postfix) with ESMTP id 434E88144A; 627\tTue, 4 Mar 2025 14:09:58 +0100 (CET)", 628 "by phobos.denx.de (Postfix, from userid 109) 629 id 8CBF98144A; Tue, 4 Mar 2025 14:09:57 +0100 (CET)", 630 "from mail-io1-xd2e.google.com (mail-io1-xd2e.google.com 631 [IPv6:2607:f8b0:4864:20::d2e]) 632 (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits)) 633 (No client certificate requested) 634 by phobos.denx.de (Postfix) with ESMTPS id 48AE281426 635 for <u-boot@lists.denx.de>; Tue, 4 Mar 2025 14:09:55 +0100 (CET)", 636 "by mail-io1-xd2e.google.com with SMTP id 637 ca18e2360f4ac-85ae33109f6so128326139f.2 638 for <u-boot@lists.denx.de>; Tue, 04 Mar 2025 05:09:55 -0800 (PST)", 639 "from chromium.org (c-73-203-119-151.hsd1.co.comcast.net. 640 [73.203.119.151]) by smtp.gmail.com with ESMTPSA id 641 ca18e2360f4ac-858753cd304sm287383839f.33.2025.03.04.05.09.49 642 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); 643 Tue, 04 Mar 2025 05:09:50 -0800 (PST)" 644 ], 645 "X-Spam-Checker-Version": "SpamAssassin 3.4.2 (2018-09-13) on phobos.denx.de", 646 "X-Spam-Level": "", 647 "X-Spam-Status": "No, score=-2.1 required=5.0 tests=BAYES_00,DKIMWL_WL_HIGH, 648 DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF, 649 RCVD_IN_DNSWL_BLOCKED,SPF_HELO_NONE,SPF_PASS autolearn=ham 650 autolearn_force=no version=3.4.2", 651 "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; 652 d=chromium.org; s=google; t=1741093792; x=1741698592; darn=lists.denx.de; 653 h=content-transfer-encoding:mime-version:message-id:date:subject:cc 654 :to:from:from:to:cc:subject:date:message-id:reply-to; 655 bh=B2zsLws430/BEZfatNjeaNnrcxmYUstVjp1pSXgNQjc=; 656 b=dG8yqtoKpSy15RHagnPcppzR8KbFCRXa2OBwXfwGoyN6M15tOJsUu2tpCdBFYiL5Mk 657 hQz5iDLV8p0Bs+fP4XtNEx7KeYfTZhiqcRFvdCLwYtGray/IHtOZaNoHLajrstic/OgE 658 01ymu6gOEboU32eQ8uC8pdCYQ4UCkfKJwmiiU=", 659 "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; 660 d=1e100.net; s=20230601; t=1741093792; x=1741698592; 661 h=content-transfer-encoding:mime-version:message-id:date:subject:cc 662 :to:from:x-gm-message-state:from:to:cc:subject:date:message-id 663 :reply-to; 664 bh=B2zsLws430/BEZfatNjeaNnrcxmYUstVjp1pSXgNQjc=; 665 b=eihzJf4i9gin9usvz4hnAvvbLV9/yB7hGPpwwW/amgnPUyWCeQstgvGL7WDLYYnukH 666 161p4mt7+cCj7Hao/jSPvVZeuKiBNPkS4YCuP3QjXfdk2ziQ9IjloVmGarWZUOlYJ5iQ 667 dZnxypUkuFfLcEDSwUmRO1dvLi3nH8PDlae3yT2H87LeHaxhXWdzHxQdPc86rkYyCqCr 668 qBC2CTS31jqSuiaI+7qB3glvbJbSEXkunz0iDewTJDvZfmuloxTipWUjRJ1mg9UJcZt5 669 9xIuTq1n9aYf1RcQlrEOQhdBAQ0/IJgvmZtzPZi9L+ppBva1ER/xm06nMA7GEUtyGwun 670 c6pA==", 671 "X-Gm-Message-State": "AOJu0Yybx3b1+yClf/IfIbQd9u8sxzK9ixPP2HimXF/dGZfSiS7Cb+O5 672 WrAkvtp7m3KPM/Mpv0sSZ5qrfTnKnb3WZyv6Oe5Q1iUjAftGNwbSxob5eJ/0y3cgrTdzE4sIWPE 673 =", 674 "X-Gm-Gg": "ASbGncu5gtgpXEPGrpbTRJulqFrFj1YPAAmKk4MiXA8/3J1A+25F0Uug2KeFUrZEjkG 675 KMdPg/C7e2emIvfM+Jl+mKv0ITBvhbyNCyY1q2U1s1cayZF05coZ9ewzGxXJGiEqLMG69uBmmIi 676 rBEvCnkXS+HVZobDQMtOsezpc+Ju8JRA7+y1R0WIlutl1mQARct6p0zTkuZp75QyB6dm/d0KYgd 677 iux/t/f0HC2CxstQlTlJYzKL6UJgkB5/UorY1lW/0NDRS6P1iemPQ7I3EPLJO8tM5ZrpJE7qgNP 678 xy0jXbUv44c48qJ1VszfY5USB8fRG7nwUYxNu6N1PXv9xWbl+z2xL68qNYUrFlHsB8ILTXAyzyr 679 Cdj+Sxg==", 680 "X-Google-Smtp-Source": " 681 AGHT+IFeVk5D4YEfJgPxOfg3ikO6Q7IhaDzABGkAPI6HA0ubK85OPhUHK08gV7enBQ8OdoE/ttqEjw==", 682 "X-Received": "by 2002:a05:6602:640f:b0:855:63c8:abb5 with SMTP id 683 ca18e2360f4ac-85881fdba3amr1839428939f.13.1741093792636; 684 Tue, 04 Mar 2025 05:09:52 -0800 (PST)", 685 "From": "Simon Glass <sjg@chromium.org>", 686 "To": "U-Boot Mailing List <u-boot@lists.denx.de>", 687 "Cc": "Simon Glass <sjg@chromium.org>, Alexander Kochetkov <al.kochet@gmail.com>, 688 Alper Nebi Yasak <alpernebiyasak@gmail.com>, 689 Brandon Maier <brandon.maier@collins.com>, 690 Jerome Forissier <jerome.forissier@linaro.org>, 691 Jiaxun Yang <jiaxun.yang@flygoat.com>, 692 Neha Malcom Francis <n-francis@ti.com>, 693 Patrick Rudolph <patrick.rudolph@9elements.com>, 694 Paul HENRYS <paul.henrys_ext@softathome.com>, Peng Fan <peng.fan@nxp.com>, 695 Philippe Reynes <philippe.reynes@softathome.com>, 696 Stefan Herbrechtsmeier <stefan.herbrechtsmeier@weidmueller.com>, 697 Tom Rini <trini@konsulko.com>", 698 "Subject": "[PATCH 0/7] binman: Check code-coverage requirements", 699 "Date": "Tue, 4 Mar 2025 06:09:37 -0700", 700 "Message-ID": "<20250304130947.109799-1-sjg@chromium.org>", 701 "X-Mailer": "git-send-email 2.43.0", 702 "MIME-Version": "1.0", 703 "Content-Transfer-Encoding": "8bit", 704 "X-BeenThere": "u-boot@lists.denx.de", 705 "X-Mailman-Version": "2.1.39", 706 "Precedence": "list", 707 "List-Id": "U-Boot discussion <u-boot.lists.denx.de>", 708 "List-Unsubscribe": "<https://lists.denx.de/options/u-boot>, 709 <mailto:u-boot-request@lists.denx.de?subject=unsubscribe>", 710 "List-Archive": "<https://lists.denx.de/pipermail/u-boot/>", 711 "List-Post": "<mailto:u-boot@lists.denx.de>", 712 "List-Help": "<mailto:u-boot-request@lists.denx.de?subject=help>", 713 "List-Subscribe": "<https://lists.denx.de/listinfo/u-boot>, 714 <mailto:u-boot-request@lists.denx.de?subject=subscribe>", 715 "Errors-To": "u-boot-bounces@lists.denx.de", 716 "Sender": "\"U-Boot\" <u-boot-bounces@lists.denx.de>", 717 "X-Virus-Scanned": "clamav-milter 0.103.8 at phobos.denx.de", 718 "X-Virus-Status": "Clean" 719 content (str): Email content, e.g. 'This series adds a cover-coverage check to CI for Binman. The iMX8 tests 720are still not completed,...' 721 """ 722 async with aiohttp.ClientSession() as client: 723 return await self._request(client, f'covers/{cover_id}/') 724 725 async def get_cover_comments(self, client, cover_id): 726 """Read comments about a cover letter 727 728 Args: 729 client (aiohttp.ClientSession): Session to use 730 cover_id (str): Patchwork cover-letter ID 731 732 Returns: list of dict: list of comments, each: 733 id (int): series ID unique across patchwork instance, e.g. 3472068 734 web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/comment/3472068/' 735 list_archive_url: (unknown?) 736 737 project (dict): project information (id, url, name, link_name, 738 list_id, list_email, etc. 739 url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/api/1.2/covers/2054866/' 740 web_url (str): Full URL, e.g. 'https://patchwork.ozlabs.org/project/uboot/cover/20250304130947.109799-1-sjg@chromium.org/' 741 project (dict): project information (id, url, name, link_name, 742 list_id, list_email, etc. 743 date (str): Date, e.g. '2025-03-04T13:16:15' 744 subject (str): 'Re: [PATCH 0/7] binman: Check code-coverage requirements' 745 submitter (dict): id, url, name, email, e.g.: 746 "id": 6170, 747 "url": "https://patchwork.ozlabs.org/api/people/6170/", 748 "name": "Simon Glass", 749 "email": "sjg@chromium.org" 750 content (str): Email content, e.g. 'Hi, 751 752On Tue, 4 Mar 2025 at 06:09, Simon Glass <sjg@chromium.org> wrote: 753> 754> This '... 755 headers: dict: email headers, see get_cover() for an example 756 """ 757 return await self._request(client, f'covers/{cover_id}/comments/') 758 759 async def get_series_url(self, link): 760 """Get the URL for a series 761 762 Args: 763 link (str): Patchwork series ID 764 765 Returns: 766 str: URL for the series page 767 """ 768 return f'{self.url}/project/{self.link_name}/list/?series={link}&state=*&archive=both' 769 770 async def _get_patch_status(self, client, patch_id): 771 """Get the patch status 772 773 Args: 774 client (aiohttp.ClientSession): Session to use 775 patch_id (int): Patch ID to look up in patchwork 776 777 Return: 778 PATCH: Patch information 779 780 Requests: 781 1 for patch, 1 for patch comments 782 """ 783 data = await self.get_patch(client, patch_id) 784 state = data['state'] 785 comment_data = await self._get_patch_comments(client, patch_id) 786 787 return Patch(patch_id, state, data, comment_data) 788 789 async def get_series_cover(self, client, data): 790 """Get the cover information (including comments) 791 792 Args: 793 client (aiohttp.ClientSession): Session to use 794 data (dict): Return value from self.get_series() 795 796 Returns: 797 COVER object, or None if no cover letter 798 """ 799 # Patchwork should always provide this, but use get() so that we don't 800 # have to provide it in our fake patchwork _fake_patchwork_cser() 801 cover = data.get('cover_letter') 802 cover_id = None 803 if cover: 804 cover_id = cover['id'] 805 info = await self.get_cover_comments(client, cover_id) 806 cover = COVER(cover_id, len(info), cover['name'], info) 807 return cover 808 809 async def series_get_state(self, client, link, read_comments, 810 read_cover_comments): 811 """Sync the series information against patchwork, to find patch status 812 813 Args: 814 client (aiohttp.ClientSession): Session to use 815 link (str): Patchwork series ID 816 read_comments (bool): True to read the comments on the patches 817 read_cover_comments (bool): True to read the comments on the cover 818 letter 819 820 Return: tuple: 821 COVER object, or None if none or not read_cover_comments 822 list of PATCH objects 823 """ 824 data = await self.get_series(client, link) 825 patch_list = list(data['patches']) 826 827 count = len(patch_list) 828 patches = [] 829 if read_comments: 830 # Returns a list of Patch objects 831 tasks = [self._get_patch_status(client, patch_list[i]['id']) 832 for i in range(count)] 833 834 patch_status = await asyncio.gather(*tasks) 835 for patch_data, status in zip(patch_list, patch_status): 836 status.series_data = patch_data 837 patches.append(status) 838 else: 839 for i in range(count): 840 info = patch_list[i] 841 pat = Patch(info['id'], series_data=info) 842 pat.raw_subject = info['name'] 843 patches.append(pat) 844 if self._show_progress: 845 terminal.print_clear() 846 847 if read_cover_comments: 848 cover = await self.get_series_cover(client, data) 849 else: 850 cover = None 851 852 return cover, patches 853