blob: c114a432bf386faca57ec34e277d26f9d72d9fa7 [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
11import collections
12import 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)
58 rtags = collections.defaultdict(set)
59 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
73 new_rtags = collections.defaultdict(set)
74 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
118
119 # Check the names match
120 commit_for_patch = {}
121 all_commits = set(series.commits)
122 for seq, patch in enumerate(patches):
123 cmatch = [c for c in all_commits if c.subject == patch.subject]
124 if len(cmatch) == 1:
125 commit_for_patch[seq] = cmatch[0]
126 all_commits.remove(cmatch[0])
127 elif len(cmatch) > 1:
128 warnings.append("Multiple commits match patch %d ('%s'):\n %s" %
129 (seq + 1, patch.subject,
130 '\n '.join([c.subject for c in cmatch])))
131 else:
132 warnings.append("Cannot find commit for patch %d ('%s')" %
133 (seq + 1, patch.subject))
134
135 return patch_for_commit, commit_for_patch, warnings
136
Simon Glass25b91c12025-04-29 07:22:19 -0600137def collect_patches(series_id, pwork):
Simon Glass3db916d2020-10-29 21:46:35 -0600138 """Collect patch information about a series from patchwork
139
140 Uses the Patchwork REST API to collect information provided by patchwork
141 about the status of each patch.
142
143 Args:
Simon Glass3db916d2020-10-29 21:46:35 -0600144 series_id (str): Patch series ID number
Simon Glass25b91c12025-04-29 07:22:19 -0600145 pwork (Patchwork): Patchwork object to use for reading
Simon Glass3db916d2020-10-29 21:46:35 -0600146
147 Returns:
Simon Glass27280f42025-04-29 07:22:17 -0600148 list of Patch: List of patches sorted by sequence number
Simon Glass3db916d2020-10-29 21:46:35 -0600149
150 Raises:
151 ValueError: if the URL could not be read or the web page does not follow
152 the expected structure
153 """
Simon Glass25b91c12025-04-29 07:22:19 -0600154 data = pwork.request('series/%s/' % series_id)
Simon Glass3db916d2020-10-29 21:46:35 -0600155
156 # Get all the rows, which are patches
157 patch_dict = data['patches']
158 count = len(patch_dict)
Simon Glass3db916d2020-10-29 21:46:35 -0600159
160 patches = []
161
162 # Work through each row (patch) one at a time, collecting the information
163 warn_count = 0
164 for pw_patch in patch_dict:
Simon Glass232eefd2025-04-29 07:22:14 -0600165 patch = patchwork.Patch(pw_patch['id'])
Simon Glass3db916d2020-10-29 21:46:35 -0600166 patch.parse_subject(pw_patch['name'])
167 patches.append(patch)
168 if warn_count > 1:
Simon Glass011f1b32022-01-29 14:14:15 -0700169 tout.warning(' (total of %d warnings)' % warn_count)
Simon Glass3db916d2020-10-29 21:46:35 -0600170
171 # Sort patches by patch number
172 patches = sorted(patches, key=lambda x: x.seq)
173 return patches
174
Simon Glass25b91c12025-04-29 07:22:19 -0600175def find_new_responses(new_rtag_list, review_list, seq, cmt, patch, pwork):
Simon Glass3db916d2020-10-29 21:46:35 -0600176 """Find new rtags collected by patchwork that we don't know about
177
178 This is designed to be run in parallel, once for each commit/patch
179
180 Args:
181 new_rtag_list (list): New rtags are written to new_rtag_list[seq]
182 list, each a dict:
183 key: Response tag (e.g. 'Reviewed-by')
184 value: Set of people who gave that response, each a name/email
185 string
Simon Glass2112d072020-10-29 21:46:38 -0600186 review_list (list): New reviews are written to review_list[seq]
187 list, each a
188 List of reviews for the patch, each a Review
Simon Glass3db916d2020-10-29 21:46:35 -0600189 seq (int): Position in new_rtag_list to update
190 cmt (Commit): Commit object for this commit
191 patch (Patch): Corresponding Patch object for this patch
Simon Glass25b91c12025-04-29 07:22:19 -0600192 pwork (Patchwork): Patchwork object to use for reading
Simon Glass3db916d2020-10-29 21:46:35 -0600193 """
194 if not patch:
195 return
196
197 # Get the content for the patch email itself as well as all comments
Simon Glass25b91c12025-04-29 07:22:19 -0600198 data = pwork.request('patches/%s/' % patch.id)
199 comment_data = pwork.request('patches/%s/comments/' % patch.id)
Simon Glass3db916d2020-10-29 21:46:35 -0600200
Simon Glass49b92292025-04-29 07:22:18 -0600201 new_rtags, reviews = process_reviews(data['content'], comment_data,
202 cmt.rtags)
Simon Glass3db916d2020-10-29 21:46:35 -0600203 new_rtag_list[seq] = new_rtags
Simon Glass2112d072020-10-29 21:46:38 -0600204 review_list[seq] = reviews
Simon Glass3db916d2020-10-29 21:46:35 -0600205
Simon Glassd4d3fb42025-04-29 07:22:21 -0600206def show_responses(col, rtags, indent, is_new):
Simon Glass3db916d2020-10-29 21:46:35 -0600207 """Show rtags collected
208
209 Args:
Simon Glassd4d3fb42025-04-29 07:22:21 -0600210 col (terminal.Colour): Colour object to use
Simon Glass3db916d2020-10-29 21:46:35 -0600211 rtags (dict): review tags to show
212 key: Response tag (e.g. 'Reviewed-by')
213 value: Set of people who gave that response, each a name/email string
214 indent (str): Indentation string to write before each line
215 is_new (bool): True if this output should be highlighted
216
217 Returns:
218 int: Number of review tags displayed
219 """
Simon Glass3db916d2020-10-29 21:46:35 -0600220 count = 0
Simon Glass2112d072020-10-29 21:46:38 -0600221 for tag in sorted(rtags.keys()):
222 people = rtags[tag]
223 for who in sorted(people):
Simon Glass02811582022-01-29 14:14:18 -0700224 terminal.tprint(indent + '%s %s: ' % ('+' if is_new else ' ', tag),
Simon Glassd4d3fb42025-04-29 07:22:21 -0600225 newline=False, colour=col.GREEN, bright=is_new,
226 col=col)
227 terminal.tprint(who, colour=col.WHITE, bright=is_new, col=col)
Simon Glass3db916d2020-10-29 21:46:35 -0600228 count += 1
229 return count
230
Simon Glassd0a0a582020-10-29 21:46:36 -0600231def create_branch(series, new_rtag_list, branch, dest_branch, overwrite,
232 repo=None):
233 """Create a new branch with review tags added
234
235 Args:
236 series (Series): Series object for the existing branch
237 new_rtag_list (list): List of review tags to add, one for each commit,
238 each a dict:
239 key: Response tag (e.g. 'Reviewed-by')
240 value: Set of people who gave that response, each a name/email
241 string
242 branch (str): Existing branch to update
243 dest_branch (str): Name of new branch to create
244 overwrite (bool): True to force overwriting dest_branch if it exists
245 repo (pygit2.Repository): Repo to use (use None unless testing)
246
247 Returns:
248 int: Total number of review tags added across all commits
249
250 Raises:
251 ValueError: if the destination branch name is the same as the original
252 branch, or it already exists and @overwrite is False
253 """
254 if branch == dest_branch:
255 raise ValueError(
256 'Destination branch must not be the same as the original branch')
257 if not repo:
258 repo = pygit2.Repository('.')
259 count = len(series.commits)
260 new_br = repo.branches.get(dest_branch)
261 if new_br:
262 if not overwrite:
263 raise ValueError("Branch '%s' already exists (-f to overwrite)" %
264 dest_branch)
265 new_br.delete()
266 if not branch:
267 branch = 'HEAD'
268 target = repo.revparse_single('%s~%d' % (branch, count))
269 repo.branches.local.create(dest_branch, target)
270
271 num_added = 0
272 for seq in range(count):
273 parent = repo.branches.get(dest_branch)
274 cherry = repo.revparse_single('%s~%d' % (branch, count - seq - 1))
275
276 repo.merge_base(cherry.oid, parent.target)
277 base_tree = cherry.parents[0].tree
278
279 index = repo.merge_trees(base_tree, parent, cherry)
280 tree_id = index.write_tree(repo)
281
282 lines = []
283 if new_rtag_list[seq]:
284 for tag, people in new_rtag_list[seq].items():
285 for who in people:
286 lines.append('%s: %s' % (tag, who))
287 num_added += 1
288 message = patchstream.insert_tags(cherry.message.rstrip(),
289 sorted(lines))
290
291 repo.create_commit(
292 parent.name, cherry.author, cherry.committer, message, tree_id,
293 [parent.target])
294 return num_added
295
Simon Glass25b91c12025-04-29 07:22:19 -0600296def check_status(series, series_id, pwork):
Simon Glass3db916d2020-10-29 21:46:35 -0600297 """Check the status of a series on Patchwork
298
299 This finds review tags and comments for a series in Patchwork, displaying
300 them to show what is new compared to the local series.
301
302 Args:
303 series (Series): Series object for the existing branch
304 series_id (str): Patch series ID number
Simon Glass25b91c12025-04-29 07:22:19 -0600305 pwork (Patchwork): Patchwork object to use for reading
Simon Glass27280f42025-04-29 07:22:17 -0600306
307 Return:
308 tuple:
309 list of Patch: List of patches sorted by sequence number
310 dict: Patches for commit
311 key: Commit number (0...n-1)
312 value: Patch object for that commit
313 list of dict: review tags:
314 key: Response tag (e.g. 'Reviewed-by')
315 value: Set of people who gave that response, each a name/email
316 string
317 list for each patch, each a:
318 list of Review objects for the patch
Simon Glass3db916d2020-10-29 21:46:35 -0600319 """
Simon Glass25b91c12025-04-29 07:22:19 -0600320 patches = collect_patches(series_id, pwork)
Simon Glass3db916d2020-10-29 21:46:35 -0600321 count = len(series.commits)
322 new_rtag_list = [None] * count
Simon Glass2112d072020-10-29 21:46:38 -0600323 review_list = [None] * count
Simon Glass3db916d2020-10-29 21:46:35 -0600324
325 patch_for_commit, _, warnings = compare_with_series(series, patches)
326 for warn in warnings:
Simon Glass011f1b32022-01-29 14:14:15 -0700327 tout.warning(warn)
Simon Glass3db916d2020-10-29 21:46:35 -0600328
329 patch_list = [patch_for_commit.get(c) for c in range(len(series.commits))]
330
331 with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
332 futures = executor.map(
Simon Glass2112d072020-10-29 21:46:38 -0600333 find_new_responses, repeat(new_rtag_list), repeat(review_list),
Simon Glass25b91c12025-04-29 07:22:19 -0600334 range(count), series.commits, patch_list, repeat(pwork))
Simon Glass3db916d2020-10-29 21:46:35 -0600335 for fresponse in futures:
336 if fresponse:
337 raise fresponse.exception()
Simon Glass27280f42025-04-29 07:22:17 -0600338 return patches, patch_for_commit, new_rtag_list, review_list
Simon Glass3db916d2020-10-29 21:46:35 -0600339
Simon Glass27280f42025-04-29 07:22:17 -0600340
341def check_patch_count(num_commits, num_patches):
342 """Check the number of commits and patches agree
343
344 Args:
345 num_commits (int): Number of commits
346 num_patches (int): Number of patches
347 """
348 if num_patches != num_commits:
349 tout.warning(f'Warning: Patchwork reports {num_patches} patches, '
350 f'series has {num_commits}')
351
352
353def do_show_status(series, patch_for_commit, show_comments, new_rtag_list,
354 review_list, col):
Simon Glass3db916d2020-10-29 21:46:35 -0600355 num_to_add = 0
356 for seq, cmt in enumerate(series.commits):
357 patch = patch_for_commit.get(seq)
358 if not patch:
359 continue
Simon Glass02811582022-01-29 14:14:18 -0700360 terminal.tprint('%3d %s' % (patch.seq, patch.subject[:50]),
Simon Glassd4d3fb42025-04-29 07:22:21 -0600361 colour=col.YELLOW, col=col)
Simon Glass3db916d2020-10-29 21:46:35 -0600362 cmt = series.commits[seq]
363 base_rtags = cmt.rtags
364 new_rtags = new_rtag_list[seq]
365
366 indent = ' ' * 2
Simon Glassd4d3fb42025-04-29 07:22:21 -0600367 show_responses(col, base_rtags, indent, False)
368 num_to_add += show_responses(col, new_rtags, indent, True)
Simon Glass2112d072020-10-29 21:46:38 -0600369 if show_comments:
370 for review in review_list[seq]:
Simon Glass02811582022-01-29 14:14:18 -0700371 terminal.tprint('Review: %s' % review.meta, colour=col.RED)
Simon Glass2112d072020-10-29 21:46:38 -0600372 for snippet in review.snippets:
373 for line in snippet:
374 quoted = line.startswith('>')
Simon Glass02811582022-01-29 14:14:18 -0700375 terminal.tprint(' %s' % line,
Simon Glass2112d072020-10-29 21:46:38 -0600376 colour=col.MAGENTA if quoted else None)
Simon Glass02811582022-01-29 14:14:18 -0700377 terminal.tprint()
Simon Glass27280f42025-04-29 07:22:17 -0600378 return num_to_add
379
380
381def show_status(series, branch, dest_branch, force, patches, patch_for_commit,
382 show_comments, new_rtag_list, review_list, test_repo=None):
383 """Show status to the user and allow a branch to be written
384
385 Args:
386 series (Series): Series object for the existing branch
387 branch (str): Existing branch to update, or None
388 dest_branch (str): Name of new branch to create, or None
389 force (bool): True to force overwriting dest_branch if it exists
390 patches (list of Patch): Patches sorted by sequence number
391 patch_for_commit (dict): Patches for commit
392 key: Commit number (0...n-1)
393 value: Patch object for that commit
394 show_comments (bool): True to show patch comments
395 new_rtag_list (list of dict) review tags for each patch:
396 key: Response tag (e.g. 'Reviewed-by')
397 value: Set of people who gave that response, each a name/email
398 string
399 review_list (list of list): list for each patch, each a:
400 list of Review objects for the patch
401 test_repo (pygit2.Repository): Repo to use (use None unless testing)
402 """
403 col = terminal.Color()
404 check_patch_count(len(series.commits), len(patches))
405 num_to_add = do_show_status(series, patch_for_commit, show_comments,
406 new_rtag_list, review_list, col)
Simon Glass3db916d2020-10-29 21:46:35 -0600407
Simon Glass02811582022-01-29 14:14:18 -0700408 terminal.tprint("%d new response%s available in patchwork%s" %
Simon Glassd0a0a582020-10-29 21:46:36 -0600409 (num_to_add, 's' if num_to_add != 1 else '',
410 '' if dest_branch
411 else ' (use -d to write them to a new branch)'))
412
413 if dest_branch:
Simon Glass27280f42025-04-29 07:22:17 -0600414 num_added = create_branch(series, new_rtag_list, branch, dest_branch,
415 force, test_repo)
Simon Glass02811582022-01-29 14:14:18 -0700416 terminal.tprint(
Simon Glassd0a0a582020-10-29 21:46:36 -0600417 "%d response%s added from patchwork into new branch '%s'" %
418 (num_added, 's' if num_added != 1 else '', dest_branch))
Simon Glass27280f42025-04-29 07:22:17 -0600419
420
421def check_and_show_status(series, link, branch, dest_branch, force,
Simon Glass25b91c12025-04-29 07:22:19 -0600422 show_comments, pwork, test_repo=None):
Simon Glass27280f42025-04-29 07:22:17 -0600423 """Read the series status from patchwork and show it to the user
424
425 Args:
426 series (Series): Series object for the existing branch
427 link (str): Patch series ID number
428 branch (str): Existing branch to update, or None
429 dest_branch (str): Name of new branch to create, or None
430 force (bool): True to force overwriting dest_branch if it exists
431 show_comments (bool): True to show patch comments
Simon Glass25b91c12025-04-29 07:22:19 -0600432 pwork (Patchwork): Patchwork object to use for reading
Simon Glass27280f42025-04-29 07:22:17 -0600433 test_repo (pygit2.Repository): Repo to use (use None unless testing)
434 """
435 patches, patch_for_commit, new_rtag_list, review_list = check_status(
Simon Glass25b91c12025-04-29 07:22:19 -0600436 series, link, pwork)
Simon Glass27280f42025-04-29 07:22:17 -0600437 show_status(series, branch, dest_branch, force, patches, patch_for_commit,
438 show_comments, new_rtag_list, review_list, test_repo)