blob: 57786e496be4ad05318aeacdc40e70231bf49ae7 [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 Glass3db916d2020-10-29 21:46:35 -060038def compare_with_series(series, patches):
39 """Compare a list of patches with a series it came from
40
41 This prints any problems as warnings
42
43 Args:
44 series (Series): Series to compare against
45 patches (:type: list of Patch): list of Patch objects to compare with
46
47 Returns:
48 tuple
49 dict:
50 key: Commit number (0...n-1)
51 value: Patch object for that commit
52 dict:
53 key: Patch number (0...n-1)
54 value: Commit object for that patch
55 """
56 # Check the names match
57 warnings = []
58 patch_for_commit = {}
59 all_patches = set(patches)
60 for seq, cmt in enumerate(series.commits):
61 pmatch = [p for p in all_patches if p.subject == cmt.subject]
62 if len(pmatch) == 1:
63 patch_for_commit[seq] = pmatch[0]
64 all_patches.remove(pmatch[0])
65 elif len(pmatch) > 1:
66 warnings.append("Multiple patches match commit %d ('%s'):\n %s" %
67 (seq + 1, cmt.subject,
68 '\n '.join([p.subject for p in pmatch])))
69 else:
70 warnings.append("Cannot find patch for commit %d ('%s')" %
71 (seq + 1, cmt.subject))
72
73
74 # Check the names match
75 commit_for_patch = {}
76 all_commits = set(series.commits)
77 for seq, patch in enumerate(patches):
78 cmatch = [c for c in all_commits if c.subject == patch.subject]
79 if len(cmatch) == 1:
80 commit_for_patch[seq] = cmatch[0]
81 all_commits.remove(cmatch[0])
82 elif len(cmatch) > 1:
83 warnings.append("Multiple commits match patch %d ('%s'):\n %s" %
84 (seq + 1, patch.subject,
85 '\n '.join([c.subject for c in cmatch])))
86 else:
87 warnings.append("Cannot find commit for patch %d ('%s')" %
88 (seq + 1, patch.subject))
89
90 return patch_for_commit, commit_for_patch, warnings
91
Simon Glassf9b03cf2020-11-03 13:54:14 -070092def call_rest_api(url, subpath):
Simon Glass3db916d2020-10-29 21:46:35 -060093 """Call the patchwork API and return the result as JSON
94
95 Args:
Simon Glassf9b03cf2020-11-03 13:54:14 -070096 url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org'
Simon Glass3db916d2020-10-29 21:46:35 -060097 subpath (str): URL subpath to use
98
99 Returns:
100 dict: Json result
101
102 Raises:
103 ValueError: the URL could not be read
104 """
Simon Glassf9b03cf2020-11-03 13:54:14 -0700105 full_url = '%s/api/1.2/%s' % (url, subpath)
106 response = requests.get(full_url)
Simon Glass3db916d2020-10-29 21:46:35 -0600107 if response.status_code != 200:
Simon Glassf9b03cf2020-11-03 13:54:14 -0700108 raise ValueError("Could not read URL '%s'" % full_url)
Simon Glass3db916d2020-10-29 21:46:35 -0600109 return response.json()
110
Simon Glassf9b03cf2020-11-03 13:54:14 -0700111def collect_patches(series, series_id, url, rest_api=call_rest_api):
Simon Glass3db916d2020-10-29 21:46:35 -0600112 """Collect patch information about a series from patchwork
113
114 Uses the Patchwork REST API to collect information provided by patchwork
115 about the status of each patch.
116
117 Args:
118 series (Series): Series object corresponding to the local branch
119 containing the series
120 series_id (str): Patch series ID number
Simon Glassf9b03cf2020-11-03 13:54:14 -0700121 url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org'
Simon Glass3db916d2020-10-29 21:46:35 -0600122 rest_api (function): API function to call to access Patchwork, for
123 testing
124
125 Returns:
126 list: List of patches sorted by sequence number, each a Patch object
127
128 Raises:
129 ValueError: if the URL could not be read or the web page does not follow
130 the expected structure
131 """
Simon Glassf9b03cf2020-11-03 13:54:14 -0700132 data = rest_api(url, 'series/%s/' % series_id)
Simon Glass3db916d2020-10-29 21:46:35 -0600133
134 # Get all the rows, which are patches
135 patch_dict = data['patches']
136 count = len(patch_dict)
137 num_commits = len(series.commits)
138 if count != num_commits:
Simon Glass011f1b32022-01-29 14:14:15 -0700139 tout.warning('Warning: Patchwork reports %d patches, series has %d' %
Simon Glass3db916d2020-10-29 21:46:35 -0600140 (count, num_commits))
141
142 patches = []
143
144 # Work through each row (patch) one at a time, collecting the information
145 warn_count = 0
146 for pw_patch in patch_dict:
Simon Glass232eefd2025-04-29 07:22:14 -0600147 patch = patchwork.Patch(pw_patch['id'])
Simon Glass3db916d2020-10-29 21:46:35 -0600148 patch.parse_subject(pw_patch['name'])
149 patches.append(patch)
150 if warn_count > 1:
Simon Glass011f1b32022-01-29 14:14:15 -0700151 tout.warning(' (total of %d warnings)' % warn_count)
Simon Glass3db916d2020-10-29 21:46:35 -0600152
153 # Sort patches by patch number
154 patches = sorted(patches, key=lambda x: x.seq)
155 return patches
156
Simon Glassf9b03cf2020-11-03 13:54:14 -0700157def find_new_responses(new_rtag_list, review_list, seq, cmt, patch, url,
Simon Glass2112d072020-10-29 21:46:38 -0600158 rest_api=call_rest_api):
Simon Glass3db916d2020-10-29 21:46:35 -0600159 """Find new rtags collected by patchwork that we don't know about
160
161 This is designed to be run in parallel, once for each commit/patch
162
163 Args:
164 new_rtag_list (list): New rtags are written to new_rtag_list[seq]
165 list, each a dict:
166 key: Response tag (e.g. 'Reviewed-by')
167 value: Set of people who gave that response, each a name/email
168 string
Simon Glass2112d072020-10-29 21:46:38 -0600169 review_list (list): New reviews are written to review_list[seq]
170 list, each a
171 List of reviews for the patch, each a Review
Simon Glass3db916d2020-10-29 21:46:35 -0600172 seq (int): Position in new_rtag_list to update
173 cmt (Commit): Commit object for this commit
174 patch (Patch): Corresponding Patch object for this patch
Simon Glassf9b03cf2020-11-03 13:54:14 -0700175 url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org'
Simon Glass3db916d2020-10-29 21:46:35 -0600176 rest_api (function): API function to call to access Patchwork, for
177 testing
178 """
179 if not patch:
180 return
181
182 # Get the content for the patch email itself as well as all comments
Simon Glassf9b03cf2020-11-03 13:54:14 -0700183 data = rest_api(url, 'patches/%s/' % patch.id)
Simon Glass3db916d2020-10-29 21:46:35 -0600184 pstrm = PatchStream.process_text(data['content'], True)
185
186 rtags = collections.defaultdict(set)
187 for response, people in pstrm.commit.rtags.items():
188 rtags[response].update(people)
189
Simon Glassf9b03cf2020-11-03 13:54:14 -0700190 data = rest_api(url, 'patches/%s/comments/' % patch.id)
Simon Glass3db916d2020-10-29 21:46:35 -0600191
Simon Glass2112d072020-10-29 21:46:38 -0600192 reviews = []
Simon Glass3db916d2020-10-29 21:46:35 -0600193 for comment in data:
194 pstrm = PatchStream.process_text(comment['content'], True)
Simon Glass2112d072020-10-29 21:46:38 -0600195 if pstrm.snippets:
196 submitter = comment['submitter']
197 person = '%s <%s>' % (submitter['name'], submitter['email'])
Simon Glass232eefd2025-04-29 07:22:14 -0600198 reviews.append(patchwork.Review(person, pstrm.snippets))
Simon Glass3db916d2020-10-29 21:46:35 -0600199 for response, people in pstrm.commit.rtags.items():
200 rtags[response].update(people)
201
202 # Find the tags that are not in the commit
203 new_rtags = collections.defaultdict(set)
204 base_rtags = cmt.rtags
205 for tag, people in rtags.items():
206 for who in people:
207 is_new = (tag not in base_rtags or
208 who not in base_rtags[tag])
209 if is_new:
210 new_rtags[tag].add(who)
211 new_rtag_list[seq] = new_rtags
Simon Glass2112d072020-10-29 21:46:38 -0600212 review_list[seq] = reviews
Simon Glass3db916d2020-10-29 21:46:35 -0600213
214def show_responses(rtags, indent, is_new):
215 """Show rtags collected
216
217 Args:
218 rtags (dict): review tags to show
219 key: Response tag (e.g. 'Reviewed-by')
220 value: Set of people who gave that response, each a name/email string
221 indent (str): Indentation string to write before each line
222 is_new (bool): True if this output should be highlighted
223
224 Returns:
225 int: Number of review tags displayed
226 """
227 col = terminal.Color()
228 count = 0
Simon Glass2112d072020-10-29 21:46:38 -0600229 for tag in sorted(rtags.keys()):
230 people = rtags[tag]
231 for who in sorted(people):
Simon Glass02811582022-01-29 14:14:18 -0700232 terminal.tprint(indent + '%s %s: ' % ('+' if is_new else ' ', tag),
Simon Glass3db916d2020-10-29 21:46:35 -0600233 newline=False, colour=col.GREEN, bright=is_new)
Simon Glass02811582022-01-29 14:14:18 -0700234 terminal.tprint(who, colour=col.WHITE, bright=is_new)
Simon Glass3db916d2020-10-29 21:46:35 -0600235 count += 1
236 return count
237
Simon Glassd0a0a582020-10-29 21:46:36 -0600238def create_branch(series, new_rtag_list, branch, dest_branch, overwrite,
239 repo=None):
240 """Create a new branch with review tags added
241
242 Args:
243 series (Series): Series object for the existing branch
244 new_rtag_list (list): List of review tags to add, one for each commit,
245 each a dict:
246 key: Response tag (e.g. 'Reviewed-by')
247 value: Set of people who gave that response, each a name/email
248 string
249 branch (str): Existing branch to update
250 dest_branch (str): Name of new branch to create
251 overwrite (bool): True to force overwriting dest_branch if it exists
252 repo (pygit2.Repository): Repo to use (use None unless testing)
253
254 Returns:
255 int: Total number of review tags added across all commits
256
257 Raises:
258 ValueError: if the destination branch name is the same as the original
259 branch, or it already exists and @overwrite is False
260 """
261 if branch == dest_branch:
262 raise ValueError(
263 'Destination branch must not be the same as the original branch')
264 if not repo:
265 repo = pygit2.Repository('.')
266 count = len(series.commits)
267 new_br = repo.branches.get(dest_branch)
268 if new_br:
269 if not overwrite:
270 raise ValueError("Branch '%s' already exists (-f to overwrite)" %
271 dest_branch)
272 new_br.delete()
273 if not branch:
274 branch = 'HEAD'
275 target = repo.revparse_single('%s~%d' % (branch, count))
276 repo.branches.local.create(dest_branch, target)
277
278 num_added = 0
279 for seq in range(count):
280 parent = repo.branches.get(dest_branch)
281 cherry = repo.revparse_single('%s~%d' % (branch, count - seq - 1))
282
283 repo.merge_base(cherry.oid, parent.target)
284 base_tree = cherry.parents[0].tree
285
286 index = repo.merge_trees(base_tree, parent, cherry)
287 tree_id = index.write_tree(repo)
288
289 lines = []
290 if new_rtag_list[seq]:
291 for tag, people in new_rtag_list[seq].items():
292 for who in people:
293 lines.append('%s: %s' % (tag, who))
294 num_added += 1
295 message = patchstream.insert_tags(cherry.message.rstrip(),
296 sorted(lines))
297
298 repo.create_commit(
299 parent.name, cherry.author, cherry.committer, message, tree_id,
300 [parent.target])
301 return num_added
302
Simon Glassc100b262025-04-29 07:22:16 -0600303def check_and_show_status(series, series_id, branch, dest_branch, force,
304 show_comments, url, rest_api=call_rest_api,
305 test_repo=None):
Simon Glass3db916d2020-10-29 21:46:35 -0600306 """Check the status of a series on Patchwork
307
308 This finds review tags and comments for a series in Patchwork, displaying
309 them to show what is new compared to the local series.
310
311 Args:
312 series (Series): Series object for the existing branch
313 series_id (str): Patch series ID number
Simon Glassd0a0a582020-10-29 21:46:36 -0600314 branch (str): Existing branch to update, or None
315 dest_branch (str): Name of new branch to create, or None
316 force (bool): True to force overwriting dest_branch if it exists
Simon Glass2112d072020-10-29 21:46:38 -0600317 show_comments (bool): True to show the comments on each patch
Simon Glassf9b03cf2020-11-03 13:54:14 -0700318 url (str): URL of patchwork server, e.g. 'https://patchwork.ozlabs.org'
Simon Glass3db916d2020-10-29 21:46:35 -0600319 rest_api (function): API function to call to access Patchwork, for
320 testing
Simon Glassd0a0a582020-10-29 21:46:36 -0600321 test_repo (pygit2.Repository): Repo to use (use None unless testing)
Simon Glass3db916d2020-10-29 21:46:35 -0600322 """
Simon Glassf9b03cf2020-11-03 13:54:14 -0700323 patches = collect_patches(series, series_id, url, rest_api)
Simon Glass3db916d2020-10-29 21:46:35 -0600324 col = terminal.Color()
325 count = len(series.commits)
326 new_rtag_list = [None] * count
Simon Glass2112d072020-10-29 21:46:38 -0600327 review_list = [None] * count
Simon Glass3db916d2020-10-29 21:46:35 -0600328
329 patch_for_commit, _, warnings = compare_with_series(series, patches)
330 for warn in warnings:
Simon Glass011f1b32022-01-29 14:14:15 -0700331 tout.warning(warn)
Simon Glass3db916d2020-10-29 21:46:35 -0600332
333 patch_list = [patch_for_commit.get(c) for c in range(len(series.commits))]
334
335 with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
336 futures = executor.map(
Simon Glass2112d072020-10-29 21:46:38 -0600337 find_new_responses, repeat(new_rtag_list), repeat(review_list),
Simon Glassf9b03cf2020-11-03 13:54:14 -0700338 range(count), series.commits, patch_list, repeat(url),
339 repeat(rest_api))
Simon Glass3db916d2020-10-29 21:46:35 -0600340 for fresponse in futures:
341 if fresponse:
342 raise fresponse.exception()
343
344 num_to_add = 0
345 for seq, cmt in enumerate(series.commits):
346 patch = patch_for_commit.get(seq)
347 if not patch:
348 continue
Simon Glass02811582022-01-29 14:14:18 -0700349 terminal.tprint('%3d %s' % (patch.seq, patch.subject[:50]),
Simon Glass3db916d2020-10-29 21:46:35 -0600350 colour=col.BLUE)
351 cmt = series.commits[seq]
352 base_rtags = cmt.rtags
353 new_rtags = new_rtag_list[seq]
354
355 indent = ' ' * 2
356 show_responses(base_rtags, indent, False)
357 num_to_add += show_responses(new_rtags, indent, True)
Simon Glass2112d072020-10-29 21:46:38 -0600358 if show_comments:
359 for review in review_list[seq]:
Simon Glass02811582022-01-29 14:14:18 -0700360 terminal.tprint('Review: %s' % review.meta, colour=col.RED)
Simon Glass2112d072020-10-29 21:46:38 -0600361 for snippet in review.snippets:
362 for line in snippet:
363 quoted = line.startswith('>')
Simon Glass02811582022-01-29 14:14:18 -0700364 terminal.tprint(' %s' % line,
Simon Glass2112d072020-10-29 21:46:38 -0600365 colour=col.MAGENTA if quoted else None)
Simon Glass02811582022-01-29 14:14:18 -0700366 terminal.tprint()
Simon Glass3db916d2020-10-29 21:46:35 -0600367
Simon Glass02811582022-01-29 14:14:18 -0700368 terminal.tprint("%d new response%s available in patchwork%s" %
Simon Glassd0a0a582020-10-29 21:46:36 -0600369 (num_to_add, 's' if num_to_add != 1 else '',
370 '' if dest_branch
371 else ' (use -d to write them to a new branch)'))
372
373 if dest_branch:
374 num_added = create_branch(series, new_rtag_list, branch,
375 dest_branch, force, test_repo)
Simon Glass02811582022-01-29 14:14:18 -0700376 terminal.tprint(
Simon Glassd0a0a582020-10-29 21:46:36 -0600377 "%d response%s added from patchwork into new branch '%s'" %
378 (num_added, 's' if num_added != 1 else '', dest_branch))