blob: 74d056242260e13ba346ec2f768ba85a16a4524e [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 Glass3729b8b2025-04-29 07:22:24 -060011import asyncio
Simon Glass21a23c22025-04-29 07:22:23 -060012from collections import defaultdict
Simon Glass3db916d2020-10-29 21:46:35 -060013import concurrent.futures
14from itertools import repeat
Simon Glassd0a0a582020-10-29 21:46:36 -060015
Simon Glass3729b8b2025-04-29 07:22:24 -060016import aiohttp
Simon Glassd0a0a582020-10-29 21:46:36 -060017import pygit2
Simon Glass3db916d2020-10-29 21:46:35 -060018
Simon Glass131444f2023-02-23 18:18:04 -070019from u_boot_pylib import terminal
20from u_boot_pylib import tout
Simon Glass232eefd2025-04-29 07:22:14 -060021from patman import patchstream
22from patman import patchwork
Simon Glass3db916d2020-10-29 21:46:35 -060023
Simon Glass3db916d2020-10-29 21:46:35 -060024
Simon Glass49b92292025-04-29 07:22:18 -060025def process_reviews(content, comment_data, base_rtags):
26 """Process and return review data
27
28 Args:
29 content (str): Content text of the patch itself - see pwork.get_patch()
30 comment_data (list of dict): Comments for the patch - see
31 pwork._get_patch_comments()
32 base_rtags (dict): base review tags (before any comments)
33 key: Response tag (e.g. 'Reviewed-by')
34 value: Set of people who gave that response, each a name/email
35 string
36
37 Return: tuple:
38 dict: new review tags (noticed since the base_rtags)
39 key: Response tag (e.g. 'Reviewed-by')
40 value: Set of people who gave that response, each a name/email
41 string
42 list of patchwork.Review: reviews received on the patch
43 """
44 pstrm = patchstream.PatchStream.process_text(content, True)
Simon Glass21a23c22025-04-29 07:22:23 -060045 rtags = defaultdict(set)
Simon Glass49b92292025-04-29 07:22:18 -060046 for response, people in pstrm.commit.rtags.items():
47 rtags[response].update(people)
48
49 reviews = []
50 for comment in comment_data:
51 pstrm = patchstream.PatchStream.process_text(comment['content'], True)
52 if pstrm.snippets:
53 submitter = comment['submitter']
54 person = f"{submitter['name']} <{submitter['email']}>"
55 reviews.append(patchwork.Review(person, pstrm.snippets))
56 for response, people in pstrm.commit.rtags.items():
57 rtags[response].update(people)
58
59 # Find the tags that are not in the commit
Simon Glass21a23c22025-04-29 07:22:23 -060060 new_rtags = defaultdict(set)
Simon Glass49b92292025-04-29 07:22:18 -060061 for tag, people in rtags.items():
62 for who in people:
63 is_new = (tag not in base_rtags or
64 who not in base_rtags[tag])
65 if is_new:
66 new_rtags[tag].add(who)
67 return new_rtags, reviews
68
69
Simon Glass3db916d2020-10-29 21:46:35 -060070def compare_with_series(series, patches):
71 """Compare a list of patches with a series it came from
72
73 This prints any problems as warnings
74
75 Args:
76 series (Series): Series to compare against
77 patches (:type: list of Patch): list of Patch objects to compare with
78
79 Returns:
80 tuple
81 dict:
82 key: Commit number (0...n-1)
83 value: Patch object for that commit
84 dict:
85 key: Patch number (0...n-1)
86 value: Commit object for that patch
87 """
88 # Check the names match
89 warnings = []
90 patch_for_commit = {}
91 all_patches = set(patches)
92 for seq, cmt in enumerate(series.commits):
93 pmatch = [p for p in all_patches if p.subject == cmt.subject]
94 if len(pmatch) == 1:
95 patch_for_commit[seq] = pmatch[0]
96 all_patches.remove(pmatch[0])
97 elif len(pmatch) > 1:
98 warnings.append("Multiple patches match commit %d ('%s'):\n %s" %
99 (seq + 1, cmt.subject,
100 '\n '.join([p.subject for p in pmatch])))
101 else:
102 warnings.append("Cannot find patch for commit %d ('%s')" %
103 (seq + 1, cmt.subject))
104
Simon Glass3db916d2020-10-29 21:46:35 -0600105 # Check the names match
106 commit_for_patch = {}
107 all_commits = set(series.commits)
108 for seq, patch in enumerate(patches):
109 cmatch = [c for c in all_commits if c.subject == patch.subject]
110 if len(cmatch) == 1:
111 commit_for_patch[seq] = cmatch[0]
112 all_commits.remove(cmatch[0])
113 elif len(cmatch) > 1:
114 warnings.append("Multiple commits match patch %d ('%s'):\n %s" %
115 (seq + 1, patch.subject,
116 '\n '.join([c.subject for c in cmatch])))
117 else:
118 warnings.append("Cannot find commit for patch %d ('%s')" %
119 (seq + 1, patch.subject))
120
121 return patch_for_commit, commit_for_patch, warnings
122
Simon Glass3db916d2020-10-29 21:46:35 -0600123
Simon Glassd4d3fb42025-04-29 07:22:21 -0600124def show_responses(col, rtags, indent, is_new):
Simon Glass3db916d2020-10-29 21:46:35 -0600125 """Show rtags collected
126
127 Args:
Simon Glassd4d3fb42025-04-29 07:22:21 -0600128 col (terminal.Colour): Colour object to use
Simon Glass3db916d2020-10-29 21:46:35 -0600129 rtags (dict): review tags to show
130 key: Response tag (e.g. 'Reviewed-by')
131 value: Set of people who gave that response, each a name/email string
132 indent (str): Indentation string to write before each line
133 is_new (bool): True if this output should be highlighted
134
135 Returns:
136 int: Number of review tags displayed
137 """
Simon Glass3db916d2020-10-29 21:46:35 -0600138 count = 0
Simon Glass2112d072020-10-29 21:46:38 -0600139 for tag in sorted(rtags.keys()):
140 people = rtags[tag]
141 for who in sorted(people):
Simon Glass02811582022-01-29 14:14:18 -0700142 terminal.tprint(indent + '%s %s: ' % ('+' if is_new else ' ', tag),
Simon Glassd4d3fb42025-04-29 07:22:21 -0600143 newline=False, colour=col.GREEN, bright=is_new,
144 col=col)
145 terminal.tprint(who, colour=col.WHITE, bright=is_new, col=col)
Simon Glass3db916d2020-10-29 21:46:35 -0600146 count += 1
147 return count
148
Simon Glassd0a0a582020-10-29 21:46:36 -0600149def create_branch(series, new_rtag_list, branch, dest_branch, overwrite,
150 repo=None):
151 """Create a new branch with review tags added
152
153 Args:
154 series (Series): Series object for the existing branch
155 new_rtag_list (list): List of review tags to add, one for each commit,
156 each a dict:
157 key: Response tag (e.g. 'Reviewed-by')
158 value: Set of people who gave that response, each a name/email
159 string
160 branch (str): Existing branch to update
161 dest_branch (str): Name of new branch to create
162 overwrite (bool): True to force overwriting dest_branch if it exists
163 repo (pygit2.Repository): Repo to use (use None unless testing)
164
165 Returns:
166 int: Total number of review tags added across all commits
167
168 Raises:
169 ValueError: if the destination branch name is the same as the original
170 branch, or it already exists and @overwrite is False
171 """
172 if branch == dest_branch:
173 raise ValueError(
174 'Destination branch must not be the same as the original branch')
175 if not repo:
176 repo = pygit2.Repository('.')
177 count = len(series.commits)
178 new_br = repo.branches.get(dest_branch)
179 if new_br:
180 if not overwrite:
181 raise ValueError("Branch '%s' already exists (-f to overwrite)" %
182 dest_branch)
183 new_br.delete()
184 if not branch:
185 branch = 'HEAD'
186 target = repo.revparse_single('%s~%d' % (branch, count))
187 repo.branches.local.create(dest_branch, target)
188
189 num_added = 0
190 for seq in range(count):
191 parent = repo.branches.get(dest_branch)
192 cherry = repo.revparse_single('%s~%d' % (branch, count - seq - 1))
193
194 repo.merge_base(cherry.oid, parent.target)
195 base_tree = cherry.parents[0].tree
196
197 index = repo.merge_trees(base_tree, parent, cherry)
198 tree_id = index.write_tree(repo)
199
200 lines = []
201 if new_rtag_list[seq]:
202 for tag, people in new_rtag_list[seq].items():
203 for who in people:
204 lines.append('%s: %s' % (tag, who))
205 num_added += 1
206 message = patchstream.insert_tags(cherry.message.rstrip(),
207 sorted(lines))
208
209 repo.create_commit(
210 parent.name, cherry.author, cherry.committer, message, tree_id,
211 [parent.target])
212 return num_added
213
Simon Glass27280f42025-04-29 07:22:17 -0600214
215def check_patch_count(num_commits, num_patches):
216 """Check the number of commits and patches agree
217
218 Args:
219 num_commits (int): Number of commits
220 num_patches (int): Number of patches
221 """
222 if num_patches != num_commits:
223 tout.warning(f'Warning: Patchwork reports {num_patches} patches, '
224 f'series has {num_commits}')
225
226
Simon Glass3729b8b2025-04-29 07:22:24 -0600227def do_show_status(patches, series, branch, show_comments, col,
228 warnings_on_stderr=True):
229 """Check the status of a series on Patchwork
230
231 This finds review tags and comments for a series in Patchwork, displaying
232 them to show what is new compared to the local series.
233
234 Args:
235 patches (list of Patch): Patch objects in the series
236 series (Series): Series object for the existing branch
237 branch (str): Existing branch to update, or None
238 show_comments (bool): True to show the comments on each patch
239 col (terminal.Colour): Colour object
240
241 Return: tuple:
242 int: Number of new review tags to add
243 list: List of review tags to add, one item for each commit, each a
244 dict:
245 key: Response tag (e.g. 'Reviewed-by')
246 value: Set of people who gave that response, each a name/email
247 string
248 list of PATCH objects
249 """
250 compare = []
251 for pw_patch in patches:
252 patch = patchwork.Patch(pw_patch.id)
253 patch.parse_subject(pw_patch.series_data['name'])
254 compare.append(patch)
255
256 count = len(series.commits)
257 new_rtag_list = [None] * count
258 review_list = [None] * count
259
Simon Glassfec886e2025-04-29 07:22:26 -0600260 with terminal.pager():
261 patch_for_commit, _, warnings = compare_with_series(series, compare)
262 for warn in warnings:
263 tout.do_output(tout.WARNING if warnings_on_stderr else tout.INFO,
264 warn)
Simon Glass3729b8b2025-04-29 07:22:24 -0600265
Simon Glassfec886e2025-04-29 07:22:26 -0600266 for seq, pw_patch in enumerate(patches):
267 compare[seq].patch = pw_patch
Simon Glass3729b8b2025-04-29 07:22:24 -0600268
Simon Glassfec886e2025-04-29 07:22:26 -0600269 for i in range(count):
270 pat = patch_for_commit.get(i)
271 if pat:
272 patch_data = pat.patch.data
273 comment_data = pat.patch.comments
274 new_rtag_list[i], review_list[i] = process_reviews(
275 patch_data['content'], comment_data,
276 series.commits[i].rtags)
277 num_to_add = _do_show_status(series, patch_for_commit, show_comments, new_rtag_list,
278 review_list, col)
Simon Glass3729b8b2025-04-29 07:22:24 -0600279 return num_to_add, new_rtag_list, patches
280
281
282def _do_show_status(series, patch_for_commit, show_comments, new_rtag_list,
Simon Glass27280f42025-04-29 07:22:17 -0600283 review_list, col):
Simon Glass3db916d2020-10-29 21:46:35 -0600284 num_to_add = 0
285 for seq, cmt in enumerate(series.commits):
286 patch = patch_for_commit.get(seq)
287 if not patch:
288 continue
Simon Glass02811582022-01-29 14:14:18 -0700289 terminal.tprint('%3d %s' % (patch.seq, patch.subject[:50]),
Simon Glassd4d3fb42025-04-29 07:22:21 -0600290 colour=col.YELLOW, col=col)
Simon Glass3db916d2020-10-29 21:46:35 -0600291 cmt = series.commits[seq]
292 base_rtags = cmt.rtags
293 new_rtags = new_rtag_list[seq]
294
295 indent = ' ' * 2
Simon Glassd4d3fb42025-04-29 07:22:21 -0600296 show_responses(col, base_rtags, indent, False)
297 num_to_add += show_responses(col, new_rtags, indent, True)
Simon Glass2112d072020-10-29 21:46:38 -0600298 if show_comments:
299 for review in review_list[seq]:
Simon Glass3729b8b2025-04-29 07:22:24 -0600300 terminal.tprint('Review: %s' % review.meta, colour=col.RED,
301 col=col)
Simon Glass2112d072020-10-29 21:46:38 -0600302 for snippet in review.snippets:
303 for line in snippet:
304 quoted = line.startswith('>')
Simon Glass3729b8b2025-04-29 07:22:24 -0600305 terminal.tprint(
306 f' {line}',
307 colour=col.MAGENTA if quoted else None, col=col)
Simon Glass02811582022-01-29 14:14:18 -0700308 terminal.tprint()
Simon Glass27280f42025-04-29 07:22:17 -0600309 return num_to_add
310
311
Simon Glass3729b8b2025-04-29 07:22:24 -0600312def show_status(series, branch, dest_branch, force, patches, show_comments,
313 test_repo=None):
314 """Check the status of a series on Patchwork
315
316 This finds review tags and comments for a series in Patchwork, displaying
317 them to show what is new compared to the local series.
Simon Glass27280f42025-04-29 07:22:17 -0600318
319 Args:
320 series (Series): Series object for the existing branch
321 branch (str): Existing branch to update, or None
322 dest_branch (str): Name of new branch to create, or None
323 force (bool): True to force overwriting dest_branch if it exists
324 patches (list of Patch): Patches sorted by sequence number
Simon Glass3729b8b2025-04-29 07:22:24 -0600325 show_comments (bool): True to show the comments on each patch
Simon Glass27280f42025-04-29 07:22:17 -0600326 test_repo (pygit2.Repository): Repo to use (use None unless testing)
327 """
328 col = terminal.Color()
329 check_patch_count(len(series.commits), len(patches))
Simon Glass3729b8b2025-04-29 07:22:24 -0600330 num_to_add, new_rtag_list, _ = do_show_status(
331 patches, series, branch, show_comments, col)
Simon Glass3db916d2020-10-29 21:46:35 -0600332
Simon Glass3729b8b2025-04-29 07:22:24 -0600333 if not dest_branch and num_to_add:
334 msg = ' (use -d to write them to a new branch)'
335 else:
336 msg = ''
337 terminal.tprint(
338 f"{num_to_add} new response{'s' if num_to_add != 1 else ''} "
339 f'available in patchwork{msg}')
Simon Glassd0a0a582020-10-29 21:46:36 -0600340
341 if dest_branch:
Simon Glass3729b8b2025-04-29 07:22:24 -0600342 num_added = create_branch(series, new_rtag_list, branch,
343 dest_branch, force, test_repo)
Simon Glass02811582022-01-29 14:14:18 -0700344 terminal.tprint(
Simon Glass3729b8b2025-04-29 07:22:24 -0600345 f"{num_added} response{'s' if num_added != 1 else ''} added "
346 f"from patchwork into new branch '{dest_branch}'")
Simon Glass27280f42025-04-29 07:22:17 -0600347
348
Simon Glass3729b8b2025-04-29 07:22:24 -0600349async def check_status(link, pwork, read_comments=False):
350 """Set up an HTTP session and get the required state
351
352 Args:
353 link (str): Patch series ID number
354 pwork (Patchwork): Patchwork object to use for reading
355 read_comments (bool): True to read comments and state for each patch
356
357 Return: tuple:
358 list of Patch objects
359 """
360 async with aiohttp.ClientSession() as client:
361 patches = await pwork.series_get_state(client, link, read_comments)
362 return patches
363
364
Simon Glass27280f42025-04-29 07:22:17 -0600365def check_and_show_status(series, link, branch, dest_branch, force,
Simon Glass25b91c12025-04-29 07:22:19 -0600366 show_comments, pwork, test_repo=None):
Simon Glass27280f42025-04-29 07:22:17 -0600367 """Read the series status from patchwork and show it to the user
368
369 Args:
370 series (Series): Series object for the existing branch
371 link (str): Patch series ID number
372 branch (str): Existing branch to update, or None
373 dest_branch (str): Name of new branch to create, or None
374 force (bool): True to force overwriting dest_branch if it exists
Simon Glass3729b8b2025-04-29 07:22:24 -0600375 show_comments (bool): True to show the comments on each patch
Simon Glass25b91c12025-04-29 07:22:19 -0600376 pwork (Patchwork): Patchwork object to use for reading
Simon Glass27280f42025-04-29 07:22:17 -0600377 test_repo (pygit2.Repository): Repo to use (use None unless testing)
378 """
Simon Glass3729b8b2025-04-29 07:22:24 -0600379 patches = asyncio.run(check_status(link, pwork, True))
380
381 show_status(series, branch, dest_branch, force, patches, show_comments,
382 test_repo=test_repo)