1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
|
# SPDX-License-Identifier: GPL-2.0+
#
# Copyright 2020 Google LLC
#
"""Talks to the patchwork service to figure out what patches have been reviewed
and commented on. Provides a way to display review tags and comments.
Allows creation of a new branch based on the old but with the review tags
collected from patchwork.
"""
import asyncio
from collections import defaultdict
import concurrent.futures
from itertools import repeat
import aiohttp
import pygit2
from u_boot_pylib import terminal
from u_boot_pylib import tout
from patman import patchstream
from patman import patchwork
def process_reviews(content, comment_data, base_rtags):
"""Process and return review data
Args:
content (str): Content text of the patch itself - see pwork.get_patch()
comment_data (list of dict): Comments for the patch - see
pwork._get_patch_comments()
base_rtags (dict): base review tags (before any comments)
key: Response tag (e.g. 'Reviewed-by')
value: Set of people who gave that response, each a name/email
string
Return: tuple:
dict: new review tags (noticed since the base_rtags)
key: Response tag (e.g. 'Reviewed-by')
value: Set of people who gave that response, each a name/email
string
list of patchwork.Review: reviews received on the patch
"""
pstrm = patchstream.PatchStream.process_text(content, True)
rtags = defaultdict(set)
for response, people in pstrm.commit.rtags.items():
rtags[response].update(people)
reviews = []
for comment in comment_data:
pstrm = patchstream.PatchStream.process_text(comment['content'], True)
if pstrm.snippets:
submitter = comment['submitter']
person = f"{submitter['name']} <{submitter['email']}>"
reviews.append(patchwork.Review(person, pstrm.snippets))
for response, people in pstrm.commit.rtags.items():
rtags[response].update(people)
# Find the tags that are not in the commit
new_rtags = defaultdict(set)
for tag, people in rtags.items():
for who in people:
is_new = (tag not in base_rtags or
who not in base_rtags[tag])
if is_new:
new_rtags[tag].add(who)
return new_rtags, reviews
def compare_with_series(series, patches):
"""Compare a list of patches with a series it came from
This prints any problems as warnings
Args:
series (Series): Series to compare against
patches (list of Patch): list of Patch objects to compare with
Returns:
tuple
dict:
key: Commit number (0...n-1)
value: Patch object for that commit
dict:
key: Patch number (0...n-1)
value: Commit object for that patch
"""
# Check the names match
warnings = []
patch_for_commit = {}
all_patches = set(patches)
for seq, cmt in enumerate(series.commits):
pmatch = [p for p in all_patches if p.subject == cmt.subject]
if len(pmatch) == 1:
patch_for_commit[seq] = pmatch[0]
all_patches.remove(pmatch[0])
elif len(pmatch) > 1:
warnings.append("Multiple patches match commit %d ('%s'):\n %s" %
(seq + 1, cmt.subject,
'\n '.join([p.subject for p in pmatch])))
else:
warnings.append("Cannot find patch for commit %d ('%s')" %
(seq + 1, cmt.subject))
# Check the names match
commit_for_patch = {}
all_commits = set(series.commits)
for seq, patch in enumerate(patches):
cmatch = [c for c in all_commits if c.subject == patch.subject]
if len(cmatch) == 1:
commit_for_patch[seq] = cmatch[0]
all_commits.remove(cmatch[0])
elif len(cmatch) > 1:
warnings.append("Multiple commits match patch %d ('%s'):\n %s" %
(seq + 1, patch.subject,
'\n '.join([c.subject for c in cmatch])))
else:
warnings.append("Cannot find commit for patch %d ('%s')" %
(seq + 1, patch.subject))
return patch_for_commit, commit_for_patch, warnings
def show_responses(col, rtags, indent, is_new):
"""Show rtags collected
Args:
col (terminal.Colour): Colour object to use
rtags (dict): review tags to show
key: Response tag (e.g. 'Reviewed-by')
value: Set of people who gave that response, each a name/email string
indent (str): Indentation string to write before each line
is_new (bool): True if this output should be highlighted
Returns:
int: Number of review tags displayed
"""
count = 0
for tag in sorted(rtags.keys()):
people = rtags[tag]
for who in sorted(people):
terminal.tprint(indent + '%s %s: ' % ('+' if is_new else ' ', tag),
newline=False, colour=col.GREEN, bright=is_new,
col=col)
terminal.tprint(who, colour=col.WHITE, bright=is_new, col=col)
count += 1
return count
def create_branch(series, new_rtag_list, branch, dest_branch, overwrite,
repo=None):
"""Create a new branch with review tags added
Args:
series (Series): Series object for the existing branch
new_rtag_list (list): List of review tags to add, one for each commit,
each a dict:
key: Response tag (e.g. 'Reviewed-by')
value: Set of people who gave that response, each a name/email
string
branch (str): Existing branch to update
dest_branch (str): Name of new branch to create
overwrite (bool): True to force overwriting dest_branch if it exists
repo (pygit2.Repository): Repo to use (use None unless testing)
Returns:
int: Total number of review tags added across all commits
Raises:
ValueError: if the destination branch name is the same as the original
branch, or it already exists and @overwrite is False
"""
if branch == dest_branch:
raise ValueError(
'Destination branch must not be the same as the original branch')
if not repo:
repo = pygit2.Repository('.')
count = len(series.commits)
new_br = repo.branches.get(dest_branch)
if new_br:
if not overwrite:
raise ValueError("Branch '%s' already exists (-f to overwrite)" %
dest_branch)
new_br.delete()
if not branch:
branch = 'HEAD'
target = repo.revparse_single('%s~%d' % (branch, count))
repo.branches.local.create(dest_branch, target)
num_added = 0
for seq in range(count):
parent = repo.branches.get(dest_branch)
cherry = repo.revparse_single('%s~%d' % (branch, count - seq - 1))
repo.merge_base(cherry.oid, parent.target)
base_tree = cherry.parents[0].tree
index = repo.merge_trees(base_tree, parent, cherry)
tree_id = index.write_tree(repo)
lines = []
if new_rtag_list[seq]:
for tag, people in new_rtag_list[seq].items():
for who in people:
lines.append('%s: %s' % (tag, who))
num_added += 1
message = patchstream.insert_tags(cherry.message.rstrip(),
sorted(lines))
repo.create_commit(
parent.name, cherry.author, cherry.committer, message, tree_id,
[parent.target])
return num_added
def check_patch_count(num_commits, num_patches):
"""Check the number of commits and patches agree
Args:
num_commits (int): Number of commits
num_patches (int): Number of patches
"""
if num_patches != num_commits:
tout.warning(f'Warning: Patchwork reports {num_patches} patches, '
f'series has {num_commits}')
def do_show_status(series, cover, patches, show_comments, show_cover_comments,
col, warnings_on_stderr=True):
"""Check the status of a series on Patchwork
This finds review tags and comments for a series in Patchwork, displaying
them to show what is new compared to the local series.
Args:
series (Series): Series object for the existing branch
cover (COVER): Cover letter info, or None if none
patches (list of Patch): Patches sorted by sequence number
show_comments (bool): True to show the comments on each patch
show_cover_comments (bool): True to show the comments on the
letter
col (terminal.Colour): Colour object
Return: tuple:
int: Number of new review tags to add
list: List of review tags to add, one item for each commit, each a
dict:
key: Response tag (e.g. 'Reviewed-by')
value: Set of people who gave that response, each a name/email
string
"""
compare = []
for pw_patch in patches:
patch = patchwork.Patch(pw_patch.id)
patch.parse_subject(pw_patch.series_data['name'])
compare.append(patch)
count = len(series.commits)
new_rtag_list = [None] * count
review_list = [None] * count
with terminal.pager():
patch_for_commit, _, warnings = compare_with_series(series, compare)
for warn in warnings:
tout.do_output(tout.WARNING if warnings_on_stderr else tout.INFO,
warn)
for seq, pw_patch in enumerate(patches):
compare[seq].patch = pw_patch
for i in range(count):
pat = patch_for_commit.get(i)
if pat:
patch_data = pat.patch.data
comment_data = pat.patch.comments
new_rtag_list[i], review_list[i] = process_reviews(
patch_data['content'], comment_data,
series.commits[i].rtags)
num_to_add = _do_show_status(
series, cover, patch_for_commit, show_comments,
show_cover_comments, new_rtag_list, review_list, col)
return num_to_add, new_rtag_list
def _do_show_status(series, cover, patch_for_commit, show_comments,
show_cover_comments, new_rtag_list, review_list, col):
if cover and show_cover_comments:
terminal.tprint(f'Cov {cover.name}', colour=col.BLACK, col=col,
bright=False, back=col.YELLOW)
for seq, comment in enumerate(cover.comments):
submitter = comment['submitter']
person = '%s <%s>' % (submitter['name'], submitter['email'])
terminal.tprint(f"From: {person}: {comment['date']}",
colour=col.RED, col=col)
print(comment['content'])
print()
num_to_add = 0
for seq, cmt in enumerate(series.commits):
patch = patch_for_commit.get(seq)
if not patch:
continue
terminal.tprint('%3d %s' % (patch.seq, patch.subject[:50]),
colour=col.YELLOW, col=col)
cmt = series.commits[seq]
base_rtags = cmt.rtags
new_rtags = new_rtag_list[seq]
indent = ' ' * 2
show_responses(col, base_rtags, indent, False)
num_to_add += show_responses(col, new_rtags, indent, True)
if show_comments:
for review in review_list[seq]:
terminal.tprint('Review: %s' % review.meta, colour=col.RED,
col=col)
for snippet in review.snippets:
for line in snippet:
quoted = line.startswith('>')
terminal.tprint(
f' {line}',
colour=col.MAGENTA if quoted else None, col=col)
terminal.tprint()
return num_to_add
def show_status(series, branch, dest_branch, force, cover, patches,
show_comments, show_cover_comments, test_repo=None):
"""Check the status of a series on Patchwork
This finds review tags and comments for a series in Patchwork, displaying
them to show what is new compared to the local series.
Args:
client (aiohttp.ClientSession): Session to use
series (Series): Series object for the existing branch
branch (str): Existing branch to update, or None
dest_branch (str): Name of new branch to create, or None
force (bool): True to force overwriting dest_branch if it exists
cover (COVER): Cover letter info, or None if none
patches (list of Patch): Patches sorted by sequence number
show_comments (bool): True to show the comments on each patch
show_cover_comments (bool): True to show the comments on the letter
test_repo (pygit2.Repository): Repo to use (use None unless testing)
"""
col = terminal.Color()
check_patch_count(len(series.commits), len(patches))
num_to_add, new_rtag_list = do_show_status(
series, cover, patches, show_comments, show_cover_comments, col)
if not dest_branch and num_to_add:
msg = ' (use -d to write them to a new branch)'
else:
msg = ''
terminal.tprint(
f"{num_to_add} new response{'s' if num_to_add != 1 else ''} "
f'available in patchwork{msg}')
if dest_branch:
num_added = create_branch(series, new_rtag_list, branch,
dest_branch, force, test_repo)
terminal.tprint(
f"{num_added} response{'s' if num_added != 1 else ''} added "
f"from patchwork into new branch '{dest_branch}'")
async def check_status(link, pwork, read_comments=False,
read_cover_comments=False):
"""Set up an HTTP session and get the required state
Args:
link (str): Patch series ID number
pwork (Patchwork): Patchwork object to use for reading
read_comments (bool): True to read comments and state for each patch
Return: tuple:
COVER object, or None if none or not read_cover_comments
list of PATCH objects
"""
async with aiohttp.ClientSession() as client:
return await pwork.series_get_state(client, link, read_comments,
read_cover_comments)
def check_and_show_status(series, link, branch, dest_branch, force,
show_comments, show_cover_comments, pwork,
test_repo=None):
"""Read the series status from patchwork and show it to the user
Args:
series (Series): Series object for the existing branch
link (str): Patch series ID number
branch (str): Existing branch to update, or None
dest_branch (str): Name of new branch to create, or None
force (bool): True to force overwriting dest_branch if it exists
show_comments (bool): True to show the comments on each patch
show_cover_comments (bool): True to show the comments on the letter
pwork (Patchwork): Patchwork object to use for reading
test_repo (pygit2.Repository): Repo to use (use None unless testing)
"""
loop = asyncio.get_event_loop()
cover, patches = loop.run_until_complete(check_status(
link, pwork, True, show_cover_comments))
show_status(series, branch, dest_branch, force, cover, patches,
show_comments, show_cover_comments, test_repo=test_repo)
|