blob: 8fc2a50b426f52bc0380f98952d362c2a5258888 [file] [log] [blame]
Simon Glass3db916d2020-10-29 21:46:35 -06001# SPDX-License-Identifier: GPL-2.0+
2#
3# Copyright 2020 Google LLC
4#
5"""Talks to the patchwork service to figure out what patches have been reviewed
Simon Glass2112d072020-10-29 21:46:38 -06006and commented on. Provides a way to display review tags and comments.
7Allows creation of a new branch based on the old but with the review tags
8collected from patchwork.
Simon Glass3db916d2020-10-29 21:46:35 -06009"""
10
Simon Glass21a23c22025-04-29 07:22:23 -060011from collections import defaultdict
Simon Glass3db916d2020-10-29 21:46:35 -060012import concurrent.futures
13from itertools import repeat
Simon Glassd0a0a582020-10-29 21:46:36 -060014
15import pygit2
Simon Glass3db916d2020-10-29 21:46:35 -060016import requests
17
Simon Glass131444f2023-02-23 18:18:04 -070018from u_boot_pylib import terminal
19from u_boot_pylib import tout
Simon Glass232eefd2025-04-29 07:22:14 -060020from patman import patchstream
21from patman import patchwork
22from patman.patchstream import PatchStream
Simon Glass3db916d2020-10-29 21:46:35 -060023
Simon Glass3db916d2020-10-29 21:46:35 -060024
25def to_int(vals):
26 """Convert a list of strings into integers, using 0 if not an integer
27
28 Args:
29 vals (list): List of strings
30
31 Returns:
32 list: List of integers, one for each input string
33 """
34 out = [int(val) if val.isdigit() else 0 for val in vals]
35 return out
36
Simon Glass2112d072020-10-29 21:46:38 -060037
Simon Glass49b92292025-04-29 07:22:18 -060038def process_reviews(content, comment_data, base_rtags):
39 """Process and return review data
40
41 Args:
42 content (str): Content text of the patch itself - see pwork.get_patch()
43 comment_data (list of dict): Comments for the patch - see
44 pwork._get_patch_comments()
45 base_rtags (dict): base review tags (before any comments)
46 key: Response tag (e.g. 'Reviewed-by')
47 value: Set of people who gave that response, each a name/email
48 string
49
50 Return: tuple:
51 dict: new review tags (noticed since the base_rtags)
52 key: Response tag (e.g. 'Reviewed-by')
53 value: Set of people who gave that response, each a name/email
54 string
55 list of patchwork.Review: reviews received on the patch
56 """
57 pstrm = patchstream.PatchStream.process_text(content, True)
Simon Glass21a23c22025-04-29 07:22:23 -060058 rtags = defaultdict(set)
Simon Glass49b92292025-04-29 07:22:18 -060059 for response, people in pstrm.commit.rtags.items():
60 rtags[response].update(people)
61
62 reviews = []
63 for comment in comment_data:
64 pstrm = patchstream.PatchStream.process_text(comment['content'], True)
65 if pstrm.snippets:
66 submitter = comment['submitter']
67 person = f"{submitter['name']} <{submitter['email']}>"
68 reviews.append(patchwork.Review(person, pstrm.snippets))
69 for response, people in pstrm.commit.rtags.items():
70 rtags[response].update(people)
71
72 # Find the tags that are not in the commit
Simon Glass21a23c22025-04-29 07:22:23 -060073 new_rtags = defaultdict(set)
Simon Glass49b92292025-04-29 07:22:18 -060074 for tag, people in rtags.items():
75 for who in people:
76 is_new = (tag not in base_rtags or
77 who not in base_rtags[tag])
78 if is_new:
79 new_rtags[tag].add(who)
80 return new_rtags, reviews
81
82
Simon Glass3db916d2020-10-29 21:46:35 -060083def compare_with_series(series, patches):
84 """Compare a list of patches with a series it came from
85
86 This prints any problems as warnings
87
88 Args:
89 series (Series): Series to compare against
90 patches (:type: list of Patch): list of Patch objects to compare with
91
92 Returns:
93 tuple
94 dict:
95 key: Commit number (0...n-1)
96 value: Patch object for that commit
97 dict:
98 key: Patch number (0...n-1)
99 value: Commit object for that patch
100 """
101 # Check the names match
102 warnings = []
103 patch_for_commit = {}
104 all_patches = set(patches)
105 for seq, cmt in enumerate(series.commits):
106 pmatch = [p for p in all_patches if p.subject == cmt.subject]
107 if len(pmatch) == 1:
108 patch_for_commit[seq] = pmatch[0]
109 all_patches.remove(pmatch[0])
110 elif len(pmatch) > 1:
111 warnings.append("Multiple patches match commit %d ('%s'):\n %s" %
112 (seq + 1, cmt.subject,
113 '\n '.join([p.subject for p in pmatch])))
114 else:
115 warnings.append("Cannot find patch for commit %d ('%s')" %
116 (seq + 1, cmt.subject))
117
Simon Glass3db916d2020-10-29 21:46:35 -0600118 # Check the names match
119 commit_for_patch = {}
120 all_commits = set(series.commits)
121 for seq, patch in enumerate(patches):
122 cmatch = [c for c in all_commits if c.subject == patch.subject]
123 if len(cmatch) == 1:
124 commit_for_patch[seq] = cmatch[0]
125 all_commits.remove(cmatch[0])
126 elif len(cmatch) > 1:
127 warnings.append("Multiple commits match patch %d ('%s'):\n %s" %
128 (seq + 1, patch.subject,
129 '\n '.join([c.subject for c in cmatch])))
130 else:
131 warnings.append("Cannot find commit for patch %d ('%s')" %
132 (seq + 1, patch.subject))
133
134 return patch_for_commit, commit_for_patch, warnings
135
Simon Glass25b91c12025-04-29 07:22:19 -0600136def collect_patches(series_id, pwork):
Simon Glass3db916d2020-10-29 21:46:35 -0600137 """Collect patch information about a series from patchwork
138
139 Uses the Patchwork REST API to collect information provided by patchwork
140 about the status of each patch.
141
142 Args:
Simon Glass3db916d2020-10-29 21:46:35 -0600143 series_id (str): Patch series ID number
Simon Glass25b91c12025-04-29 07:22:19 -0600144 pwork (Patchwork): Patchwork object to use for reading
Simon Glass3db916d2020-10-29 21:46:35 -0600145
146 Returns:
Simon Glass27280f42025-04-29 07:22:17 -0600147 list of Patch: List of patches sorted by sequence number
Simon Glass3db916d2020-10-29 21:46:35 -0600148
149 Raises:
150 ValueError: if the URL could not be read or the web page does not follow
151 the expected structure
152 """
Simon Glass25b91c12025-04-29 07:22:19 -0600153 data = pwork.request('series/%s/' % series_id)
Simon Glass3db916d2020-10-29 21:46:35 -0600154
155 # Get all the rows, which are patches
156 patch_dict = data['patches']
157 count = len(patch_dict)
Simon Glass3db916d2020-10-29 21:46:35 -0600158
159 patches = []
160
161 # Work through each row (patch) one at a time, collecting the information
162 warn_count = 0
163 for pw_patch in patch_dict:
Simon Glass232eefd2025-04-29 07:22:14 -0600164 patch = patchwork.Patch(pw_patch['id'])
Simon Glass3db916d2020-10-29 21:46:35 -0600165 patch.parse_subject(pw_patch['name'])
166 patches.append(patch)
167 if warn_count > 1:
Simon Glass011f1b32022-01-29 14:14:15 -0700168 tout.warning(' (total of %d warnings)' % warn_count)
Simon Glass3db916d2020-10-29 21:46:35 -0600169
170 # Sort patches by patch number
171 patches = sorted(patches, key=lambda x: x.seq)
172 return patches
173
Simon Glass25b91c12025-04-29 07:22:19 -0600174def find_new_responses(new_rtag_list, review_list, seq, cmt, patch, pwork):
Simon Glass3db916d2020-10-29 21:46:35 -0600175 """Find new rtags collected by patchwork that we don't know about
176
177 This is designed to be run in parallel, once for each commit/patch
178
179 Args:
180 new_rtag_list (list): New rtags are written to new_rtag_list[seq]
181 list, each a dict:
182 key: Response tag (e.g. 'Reviewed-by')
183 value: Set of people who gave that response, each a name/email
184 string
Simon Glass2112d072020-10-29 21:46:38 -0600185 review_list (list): New reviews are written to review_list[seq]
186 list, each a
187 List of reviews for the patch, each a Review
Simon Glass3db916d2020-10-29 21:46:35 -0600188 seq (int): Position in new_rtag_list to update
189 cmt (Commit): Commit object for this commit
190 patch (Patch): Corresponding Patch object for this patch
Simon Glass25b91c12025-04-29 07:22:19 -0600191 pwork (Patchwork): Patchwork object to use for reading
Simon Glass3db916d2020-10-29 21:46:35 -0600192 """
193 if not patch:
194 return
195
196 # Get the content for the patch email itself as well as all comments
Simon Glass25b91c12025-04-29 07:22:19 -0600197 data = pwork.request('patches/%s/' % patch.id)
198 comment_data = pwork.request('patches/%s/comments/' % patch.id)
Simon Glass3db916d2020-10-29 21:46:35 -0600199
Simon Glass49b92292025-04-29 07:22:18 -0600200 new_rtags, reviews = process_reviews(data['content'], comment_data,
201 cmt.rtags)
Simon Glass3db916d2020-10-29 21:46:35 -0600202 new_rtag_list[seq] = new_rtags
Simon Glass2112d072020-10-29 21:46:38 -0600203 review_list[seq] = reviews
Simon Glass3db916d2020-10-29 21:46:35 -0600204
Simon Glassd4d3fb42025-04-29 07:22:21 -0600205def show_responses(col, rtags, indent, is_new):
Simon Glass3db916d2020-10-29 21:46:35 -0600206 """Show rtags collected
207
208 Args:
Simon Glassd4d3fb42025-04-29 07:22:21 -0600209 col (terminal.Colour): Colour object to use
Simon Glass3db916d2020-10-29 21:46:35 -0600210 rtags (dict): review tags to show
211 key: Response tag (e.g. 'Reviewed-by')
212 value: Set of people who gave that response, each a name/email string
213 indent (str): Indentation string to write before each line
214 is_new (bool): True if this output should be highlighted
215
216 Returns:
217 int: Number of review tags displayed
218 """
Simon Glass3db916d2020-10-29 21:46:35 -0600219 count = 0
Simon Glass2112d072020-10-29 21:46:38 -0600220 for tag in sorted(rtags.keys()):
221 people = rtags[tag]
222 for who in sorted(people):
Simon Glass02811582022-01-29 14:14:18 -0700223 terminal.tprint(indent + '%s %s: ' % ('+' if is_new else ' ', tag),
Simon Glassd4d3fb42025-04-29 07:22:21 -0600224 newline=False, colour=col.GREEN, bright=is_new,
225 col=col)
226 terminal.tprint(who, colour=col.WHITE, bright=is_new, col=col)
Simon Glass3db916d2020-10-29 21:46:35 -0600227 count += 1
228 return count
229
Simon Glassd0a0a582020-10-29 21:46:36 -0600230def create_branch(series, new_rtag_list, branch, dest_branch, overwrite,
231 repo=None):
232 """Create a new branch with review tags added
233
234 Args:
235 series (Series): Series object for the existing branch
236 new_rtag_list (list): List of review tags to add, one for each commit,
237 each a dict:
238 key: Response tag (e.g. 'Reviewed-by')
239 value: Set of people who gave that response, each a name/email
240 string
241 branch (str): Existing branch to update
242 dest_branch (str): Name of new branch to create
243 overwrite (bool): True to force overwriting dest_branch if it exists
244 repo (pygit2.Repository): Repo to use (use None unless testing)
245
246 Returns:
247 int: Total number of review tags added across all commits
248
249 Raises:
250 ValueError: if the destination branch name is the same as the original
251 branch, or it already exists and @overwrite is False
252 """
253 if branch == dest_branch:
254 raise ValueError(
255 'Destination branch must not be the same as the original branch')
256 if not repo:
257 repo = pygit2.Repository('.')
258 count = len(series.commits)
259 new_br = repo.branches.get(dest_branch)
260 if new_br:
261 if not overwrite:
262 raise ValueError("Branch '%s' already exists (-f to overwrite)" %
263 dest_branch)
264 new_br.delete()
265 if not branch:
266 branch = 'HEAD'
267 target = repo.revparse_single('%s~%d' % (branch, count))
268 repo.branches.local.create(dest_branch, target)
269
270 num_added = 0
271 for seq in range(count):
272 parent = repo.branches.get(dest_branch)
273 cherry = repo.revparse_single('%s~%d' % (branch, count - seq - 1))
274
275 repo.merge_base(cherry.oid, parent.target)
276 base_tree = cherry.parents[0].tree
277
278 index = repo.merge_trees(base_tree, parent, cherry)
279 tree_id = index.write_tree(repo)
280
281 lines = []
282 if new_rtag_list[seq]:
283 for tag, people in new_rtag_list[seq].items():
284 for who in people:
285 lines.append('%s: %s' % (tag, who))
286 num_added += 1
287 message = patchstream.insert_tags(cherry.message.rstrip(),
288 sorted(lines))
289
290 repo.create_commit(
291 parent.name, cherry.author, cherry.committer, message, tree_id,
292 [parent.target])
293 return num_added
294
Simon Glass25b91c12025-04-29 07:22:19 -0600295def check_status(series, series_id, pwork):
Simon Glass3db916d2020-10-29 21:46:35 -0600296 """Check the status of a series on Patchwork
297
298 This finds review tags and comments for a series in Patchwork, displaying
299 them to show what is new compared to the local series.
300
301 Args:
302 series (Series): Series object for the existing branch
303 series_id (str): Patch series ID number
Simon Glass25b91c12025-04-29 07:22:19 -0600304 pwork (Patchwork): Patchwork object to use for reading
Simon Glass27280f42025-04-29 07:22:17 -0600305
306 Return:
307 tuple:
308 list of Patch: List of patches sorted by sequence number
309 dict: Patches for commit
310 key: Commit number (0...n-1)
311 value: Patch object for that commit
312 list of dict: review tags:
313 key: Response tag (e.g. 'Reviewed-by')
314 value: Set of people who gave that response, each a name/email
315 string
316 list for each patch, each a:
317 list of Review objects for the patch
Simon Glass3db916d2020-10-29 21:46:35 -0600318 """
Simon Glass25b91c12025-04-29 07:22:19 -0600319 patches = collect_patches(series_id, pwork)
Simon Glass3db916d2020-10-29 21:46:35 -0600320 count = len(series.commits)
321 new_rtag_list = [None] * count
Simon Glass2112d072020-10-29 21:46:38 -0600322 review_list = [None] * count
Simon Glass3db916d2020-10-29 21:46:35 -0600323
324 patch_for_commit, _, warnings = compare_with_series(series, patches)
325 for warn in warnings:
Simon Glass011f1b32022-01-29 14:14:15 -0700326 tout.warning(warn)
Simon Glass3db916d2020-10-29 21:46:35 -0600327
328 patch_list = [patch_for_commit.get(c) for c in range(len(series.commits))]
329
330 with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
331 futures = executor.map(
Simon Glass2112d072020-10-29 21:46:38 -0600332 find_new_responses, repeat(new_rtag_list), repeat(review_list),
Simon Glass25b91c12025-04-29 07:22:19 -0600333 range(count), series.commits, patch_list, repeat(pwork))
Simon Glass3db916d2020-10-29 21:46:35 -0600334 for fresponse in futures:
335 if fresponse:
336 raise fresponse.exception()
Simon Glass27280f42025-04-29 07:22:17 -0600337 return patches, patch_for_commit, new_rtag_list, review_list
Simon Glass3db916d2020-10-29 21:46:35 -0600338
Simon Glass27280f42025-04-29 07:22:17 -0600339
340def check_patch_count(num_commits, num_patches):
341 """Check the number of commits and patches agree
342
343 Args:
344 num_commits (int): Number of commits
345 num_patches (int): Number of patches
346 """
347 if num_patches != num_commits:
348 tout.warning(f'Warning: Patchwork reports {num_patches} patches, '
349 f'series has {num_commits}')
350
351
352def do_show_status(series, patch_for_commit, show_comments, new_rtag_list,
353 review_list, col):
Simon Glass3db916d2020-10-29 21:46:35 -0600354 num_to_add = 0
355 for seq, cmt in enumerate(series.commits):
356 patch = patch_for_commit.get(seq)
357 if not patch:
358 continue
Simon Glass02811582022-01-29 14:14:18 -0700359 terminal.tprint('%3d %s' % (patch.seq, patch.subject[:50]),
Simon Glassd4d3fb42025-04-29 07:22:21 -0600360 colour=col.YELLOW, col=col)
Simon Glass3db916d2020-10-29 21:46:35 -0600361 cmt = series.commits[seq]
362 base_rtags = cmt.rtags
363 new_rtags = new_rtag_list[seq]
364
365 indent = ' ' * 2
Simon Glassd4d3fb42025-04-29 07:22:21 -0600366 show_responses(col, base_rtags, indent, False)
367 num_to_add += show_responses(col, new_rtags, indent, True)
Simon Glass2112d072020-10-29 21:46:38 -0600368 if show_comments:
369 for review in review_list[seq]:
Simon Glass02811582022-01-29 14:14:18 -0700370 terminal.tprint('Review: %s' % review.meta, colour=col.RED)
Simon Glass2112d072020-10-29 21:46:38 -0600371 for snippet in review.snippets:
372 for line in snippet:
373 quoted = line.startswith('>')
Simon Glass02811582022-01-29 14:14:18 -0700374 terminal.tprint(' %s' % line,
Simon Glass2112d072020-10-29 21:46:38 -0600375 colour=col.MAGENTA if quoted else None)
Simon Glass02811582022-01-29 14:14:18 -0700376 terminal.tprint()
Simon Glass27280f42025-04-29 07:22:17 -0600377 return num_to_add
378
379
380def show_status(series, branch, dest_branch, force, patches, patch_for_commit,
381 show_comments, new_rtag_list, review_list, test_repo=None):
382 """Show status to the user and allow a branch to be written
383
384 Args:
385 series (Series): Series object for the existing branch
386 branch (str): Existing branch to update, or None
387 dest_branch (str): Name of new branch to create, or None
388 force (bool): True to force overwriting dest_branch if it exists
389 patches (list of Patch): Patches sorted by sequence number
390 patch_for_commit (dict): Patches for commit
391 key: Commit number (0...n-1)
392 value: Patch object for that commit
393 show_comments (bool): True to show patch comments
394 new_rtag_list (list of dict) review tags for each patch:
395 key: Response tag (e.g. 'Reviewed-by')
396 value: Set of people who gave that response, each a name/email
397 string
398 review_list (list of list): list for each patch, each a:
399 list of Review objects for the patch
400 test_repo (pygit2.Repository): Repo to use (use None unless testing)
401 """
402 col = terminal.Color()
403 check_patch_count(len(series.commits), len(patches))
404 num_to_add = do_show_status(series, patch_for_commit, show_comments,
405 new_rtag_list, review_list, col)
Simon Glass3db916d2020-10-29 21:46:35 -0600406
Simon Glass02811582022-01-29 14:14:18 -0700407 terminal.tprint("%d new response%s available in patchwork%s" %
Simon Glassd0a0a582020-10-29 21:46:36 -0600408 (num_to_add, 's' if num_to_add != 1 else '',
409 '' if dest_branch
410 else ' (use -d to write them to a new branch)'))
411
412 if dest_branch:
Simon Glass27280f42025-04-29 07:22:17 -0600413 num_added = create_branch(series, new_rtag_list, branch, dest_branch,
414 force, test_repo)
Simon Glass02811582022-01-29 14:14:18 -0700415 terminal.tprint(
Simon Glassd0a0a582020-10-29 21:46:36 -0600416 "%d response%s added from patchwork into new branch '%s'" %
417 (num_added, 's' if num_added != 1 else '', dest_branch))
Simon Glass27280f42025-04-29 07:22:17 -0600418
419
420def check_and_show_status(series, link, branch, dest_branch, force,
Simon Glass25b91c12025-04-29 07:22:19 -0600421 show_comments, pwork, test_repo=None):
Simon Glass27280f42025-04-29 07:22:17 -0600422 """Read the series status from patchwork and show it to the user
423
424 Args:
425 series (Series): Series object for the existing branch
426 link (str): Patch series ID number
427 branch (str): Existing branch to update, or None
428 dest_branch (str): Name of new branch to create, or None
429 force (bool): True to force overwriting dest_branch if it exists
430 show_comments (bool): True to show patch comments
Simon Glass25b91c12025-04-29 07:22:19 -0600431 pwork (Patchwork): Patchwork object to use for reading
Simon Glass27280f42025-04-29 07:22:17 -0600432 test_repo (pygit2.Repository): Repo to use (use None unless testing)
433 """
434 patches, patch_for_commit, new_rtag_list, review_list = check_status(
Simon Glass25b91c12025-04-29 07:22:19 -0600435 series, link, pwork)
Simon Glass27280f42025-04-29 07:22:17 -0600436 show_status(series, branch, dest_branch, force, patches, patch_for_commit,
437 show_comments, new_rtag_list, review_list, test_repo)