blob: 6866e1dbd0898864a18cc4ca51e1e69c07c9c396 [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass0d24de92012-01-14 15:12:45 +00002# Copyright (c) 2011 The Chromium OS Authors.
3#
Simon Glass0d24de92012-01-14 15:12:45 +00004
Sean Anderson6949f702020-05-04 16:28:34 -04005from __future__ import print_function
6
7import collections
Simon Glass27409e32023-03-08 10:52:54 -08008import concurrent.futures
Doug Anderson31187252012-12-03 14:40:43 +00009import itertools
Simon Glass0d24de92012-01-14 15:12:45 +000010import os
Simon Glass27409e32023-03-08 10:52:54 -080011import sys
12import time
Simon Glass0d24de92012-01-14 15:12:45 +000013
Simon Glassbf776672020-04-17 18:09:04 -060014from patman import get_maintainer
15from patman import gitutil
16from patman import settings
Simon Glass4583c002023-02-23 18:18:04 -070017from u_boot_pylib import terminal
18from u_boot_pylib import tools
Simon Glass0d24de92012-01-14 15:12:45 +000019
20# Series-xxx tags that we understand
Simon Glassfe2f8d92013-03-20 16:43:00 +000021valid_series = ['to', 'cc', 'version', 'changes', 'prefix', 'notes', 'name',
Sean Anderson082c1192021-10-22 19:07:04 -040022 'cover_cc', 'process_log', 'links', 'patchwork_url', 'postfix']
Simon Glass0d24de92012-01-14 15:12:45 +000023
24class Series(dict):
25 """Holds information about a patch series, including all tags.
26
27 Vars:
28 cc: List of aliases/emails to Cc all patches to
29 commits: List of Commit objects, one for each patch
30 cover: List of lines in the cover letter
31 notes: List of lines in the notes
32 changes: (dict) List of changes for each version, The key is
33 the integer version number
Simon Glassf0b739f2013-05-02 14:46:02 +000034 allow_overwrite: Allow tags to overwrite an existing tag
Simon Glass0d24de92012-01-14 15:12:45 +000035 """
36 def __init__(self):
37 self.cc = []
38 self.to = []
Simon Glassfe2f8d92013-03-20 16:43:00 +000039 self.cover_cc = []
Simon Glass0d24de92012-01-14 15:12:45 +000040 self.commits = []
41 self.cover = None
42 self.notes = []
43 self.changes = {}
Simon Glassf0b739f2013-05-02 14:46:02 +000044 self.allow_overwrite = False
Simon Glass0d24de92012-01-14 15:12:45 +000045
Doug Andersond94566a2012-12-03 14:40:42 +000046 # Written in MakeCcFile()
47 # key: name of patch file
48 # value: list of email addresses
49 self._generated_cc = {}
50
Simon Glass0d24de92012-01-14 15:12:45 +000051 # These make us more like a dictionary
52 def __setattr__(self, name, value):
53 self[name] = value
54
55 def __getattr__(self, name):
56 return self[name]
57
58 def AddTag(self, commit, line, name, value):
59 """Add a new Series-xxx tag along with its value.
60
61 Args:
62 line: Source line containing tag (useful for debug/error messages)
63 name: Tag name (part after 'Series-')
64 value: Tag value (part after 'Series-xxx: ')
Simon Glassdffa42c2020-10-29 21:46:25 -060065
66 Returns:
67 String warning if something went wrong, else None
Simon Glass0d24de92012-01-14 15:12:45 +000068 """
69 # If we already have it, then add to our list
Simon Glassfe2f8d92013-03-20 16:43:00 +000070 name = name.replace('-', '_')
Simon Glassf0b739f2013-05-02 14:46:02 +000071 if name in self and not self.allow_overwrite:
Simon Glass0d24de92012-01-14 15:12:45 +000072 values = value.split(',')
73 values = [str.strip() for str in values]
74 if type(self[name]) != type([]):
75 raise ValueError("In %s: line '%s': Cannot add another value "
76 "'%s' to series '%s'" %
77 (commit.hash, line, values, self[name]))
78 self[name] += values
79
80 # Otherwise just set the value
81 elif name in valid_series:
Albert ARIBAUD070b7812016-02-02 10:24:53 +010082 if name=="notes":
83 self[name] = [value]
84 else:
85 self[name] = value
Simon Glass0d24de92012-01-14 15:12:45 +000086 else:
Simon Glassdffa42c2020-10-29 21:46:25 -060087 return ("In %s: line '%s': Unknown 'Series-%s': valid "
Simon Glassef0e9de2012-09-27 15:06:02 +000088 "options are %s" % (commit.hash, line, name,
Simon Glass0d24de92012-01-14 15:12:45 +000089 ', '.join(valid_series)))
Simon Glassdffa42c2020-10-29 21:46:25 -060090 return None
Simon Glass0d24de92012-01-14 15:12:45 +000091
92 def AddCommit(self, commit):
93 """Add a commit into our list of commits
94
95 We create a list of tags in the commit subject also.
96
97 Args:
98 commit: Commit object to add
99 """
Simon Glassa3eeadf2022-01-29 14:14:07 -0700100 commit.check_tags()
Simon Glass0d24de92012-01-14 15:12:45 +0000101 self.commits.append(commit)
102
103 def ShowActions(self, args, cmd, process_tags):
104 """Show what actions we will/would perform
105
106 Args:
107 args: List of patch files we created
108 cmd: The git command we would have run
109 process_tags: Process tags as if they were aliases
110 """
Simon Glass0157b182022-01-29 14:14:11 -0700111 to_set = set(gitutil.build_email_list(self.to));
112 cc_set = set(gitutil.build_email_list(self.cc));
Peter Tyser21818302015-01-26 11:42:21 -0600113
Simon Glass0d24de92012-01-14 15:12:45 +0000114 col = terminal.Color()
Paul Burtona920a172016-09-27 16:03:50 +0100115 print('Dry run, so not doing much. But I would do this:')
116 print()
117 print('Send a total of %d patch%s with %scover letter.' % (
Simon Glass0d24de92012-01-14 15:12:45 +0000118 len(args), '' if len(args) == 1 else 'es',
Paul Burtona920a172016-09-27 16:03:50 +0100119 self.get('cover') and 'a ' or 'no '))
Simon Glass0d24de92012-01-14 15:12:45 +0000120
121 # TODO: Colour the patches according to whether they passed checks
122 for upto in range(len(args)):
123 commit = self.commits[upto]
Simon Glass252ac582022-01-29 14:14:17 -0700124 print(col.build(col.GREEN, ' %s' % args[upto]))
Doug Andersond94566a2012-12-03 14:40:42 +0000125 cc_list = list(self._generated_cc[commit.patch])
Simon Glassb644c662019-05-14 15:53:51 -0600126 for email in sorted(set(cc_list) - to_set - cc_set):
Simon Glass0d24de92012-01-14 15:12:45 +0000127 if email == None:
Simon Glass32cc6ae2022-02-11 13:23:18 -0700128 email = col.build(col.YELLOW, '<alias not found>')
Simon Glass0d24de92012-01-14 15:12:45 +0000129 if email:
Simon Glass6f8abf72017-05-29 15:31:23 -0600130 print(' Cc: ', email)
Simon Glass0d24de92012-01-14 15:12:45 +0000131 print
Simon Glassb644c662019-05-14 15:53:51 -0600132 for item in sorted(to_set):
Paul Burtona920a172016-09-27 16:03:50 +0100133 print('To:\t ', item)
Simon Glassb644c662019-05-14 15:53:51 -0600134 for item in sorted(cc_set - to_set):
Paul Burtona920a172016-09-27 16:03:50 +0100135 print('Cc:\t ', item)
136 print('Version: ', self.get('version'))
137 print('Prefix:\t ', self.get('prefix'))
Sean Anderson082c1192021-10-22 19:07:04 -0400138 print('Postfix:\t ', self.get('postfix'))
Simon Glass0d24de92012-01-14 15:12:45 +0000139 if self.cover:
Paul Burtona920a172016-09-27 16:03:50 +0100140 print('Cover: %d lines' % len(self.cover))
Simon Glass0157b182022-01-29 14:14:11 -0700141 cover_cc = gitutil.build_email_list(self.get('cover_cc', ''))
Simon Glassfe2f8d92013-03-20 16:43:00 +0000142 all_ccs = itertools.chain(cover_cc, *self._generated_cc.values())
Simon Glassb644c662019-05-14 15:53:51 -0600143 for email in sorted(set(all_ccs) - to_set - cc_set):
Paul Burtona920a172016-09-27 16:03:50 +0100144 print(' Cc: ', email)
Simon Glass0d24de92012-01-14 15:12:45 +0000145 if cmd:
Paul Burtona920a172016-09-27 16:03:50 +0100146 print('Git command: %s' % cmd)
Simon Glass0d24de92012-01-14 15:12:45 +0000147
148 def MakeChangeLog(self, commit):
149 """Create a list of changes for each version.
150
151 Return:
152 The change log as a list of strings, one per line
153
Simon Glass27e97602012-10-30 06:15:16 +0000154 Changes in v4:
Otavio Salvador244e6f92012-08-18 07:46:04 +0000155 - Jog the dial back closer to the widget
156
Simon Glass27e97602012-10-30 06:15:16 +0000157 Changes in v2:
Simon Glass0d24de92012-01-14 15:12:45 +0000158 - Fix the widget
159 - Jog the dial
160
Sean Andersonb0436b92020-05-04 16:28:33 -0400161 If there are no new changes in a patch, a note will be added
162
163 (no changes since v2)
164
165 Changes in v2:
166 - Fix the widget
167 - Jog the dial
Simon Glass0d24de92012-01-14 15:12:45 +0000168 """
Sean Anderson6949f702020-05-04 16:28:34 -0400169 # Collect changes from the series and this commit
170 changes = collections.defaultdict(list)
171 for version, changelist in self.changes.items():
172 changes[version] += changelist
173 if commit:
174 for version, changelist in commit.changes.items():
175 changes[version] += [[commit, text] for text in changelist]
176
177 versions = sorted(changes, reverse=True)
Sean Andersonb0436b92020-05-04 16:28:33 -0400178 newest_version = 1
179 if 'version' in self:
180 newest_version = max(newest_version, int(self.version))
181 if versions:
182 newest_version = max(newest_version, versions[0])
183
Simon Glass0d24de92012-01-14 15:12:45 +0000184 final = []
Simon Glass645b2712013-03-26 13:09:44 +0000185 process_it = self.get('process_log', '').split(',')
186 process_it = [item.strip() for item in process_it]
Simon Glass0d24de92012-01-14 15:12:45 +0000187 need_blank = False
Sean Andersonb0436b92020-05-04 16:28:33 -0400188 for version in versions:
Simon Glass0d24de92012-01-14 15:12:45 +0000189 out = []
Sean Anderson6949f702020-05-04 16:28:34 -0400190 for this_commit, text in changes[version]:
Simon Glass0d24de92012-01-14 15:12:45 +0000191 if commit and this_commit != commit:
192 continue
Simon Glass645b2712013-03-26 13:09:44 +0000193 if 'uniq' not in process_it or text not in out:
194 out.append(text)
Simon Glass645b2712013-03-26 13:09:44 +0000195 if 'sort' in process_it:
196 out = sorted(out)
Sean Andersonb0436b92020-05-04 16:28:33 -0400197 have_changes = len(out) > 0
198 line = 'Changes in v%d:' % version
Simon Glass27e97602012-10-30 06:15:16 +0000199 if have_changes:
200 out.insert(0, line)
Sean Andersonb0436b92020-05-04 16:28:33 -0400201 if version < newest_version and len(final) == 0:
202 out.insert(0, '')
203 out.insert(0, '(no changes since v%d)' % version)
204 newest_version = 0
205 # Only add a new line if we output something
206 if need_blank:
207 out.insert(0, '')
208 need_blank = False
Simon Glass27e97602012-10-30 06:15:16 +0000209 final += out
Sean Andersonb0436b92020-05-04 16:28:33 -0400210 need_blank = need_blank or have_changes
211
212 if len(final) > 0:
Simon Glass0d24de92012-01-14 15:12:45 +0000213 final.append('')
Sean Andersonb0436b92020-05-04 16:28:33 -0400214 elif newest_version != 1:
215 final = ['(no changes since v1)', '']
Simon Glass0d24de92012-01-14 15:12:45 +0000216 return final
217
218 def DoChecks(self):
219 """Check that each version has a change log
220
221 Print an error if something is wrong.
222 """
223 col = terminal.Color()
224 if self.get('version'):
225 changes_copy = dict(self.changes)
Otavio Salvadord5f81d82012-08-13 10:08:22 +0000226 for version in range(1, int(self.version) + 1):
Simon Glass0d24de92012-01-14 15:12:45 +0000227 if self.changes.get(version):
228 del changes_copy[version]
229 else:
Otavio Salvadord5f81d82012-08-13 10:08:22 +0000230 if version > 1:
231 str = 'Change log missing for v%d' % version
Simon Glass252ac582022-01-29 14:14:17 -0700232 print(col.build(col.RED, str))
Simon Glass0d24de92012-01-14 15:12:45 +0000233 for version in changes_copy:
234 str = 'Change log for unknown version v%d' % version
Simon Glass252ac582022-01-29 14:14:17 -0700235 print(col.build(col.RED, str))
Simon Glass0d24de92012-01-14 15:12:45 +0000236 elif self.changes:
237 str = 'Change log exists, but no version is set'
Simon Glass252ac582022-01-29 14:14:17 -0700238 print(col.build(col.RED, str))
Simon Glass0d24de92012-01-14 15:12:45 +0000239
Simon Glassc524cd62023-03-08 10:52:53 -0800240 def GetCcForCommit(self, commit, process_tags, warn_on_error,
241 add_maintainers, limit, get_maintainer_script,
242 all_skips):
243 """Get the email CCs to use with a particular commit
244
245 Uses subject tags and get_maintainers.pl script to find people to cc
246 on a patch
247
248 Args:
249 commit (Commit): Commit to process
250 process_tags (bool): Process tags as if they were aliases
251 warn_on_error (bool): True to print a warning when an alias fails to
252 match, False to ignore it.
253 add_maintainers (bool or list of str): Either:
254 True/False to call the get_maintainers to CC maintainers
255 List of maintainers to include (for testing)
256 limit (int): Limit the length of the Cc list (None if no limit)
257 get_maintainer_script (str): The file name of the get_maintainer.pl
258 script (or compatible).
259 all_skips (set of str): Updated to include the set of bouncing email
260 addresses that were dropped from the output. This is essentially
261 a return value from this function.
262
263 Returns:
264 list of str: List of email addresses to cc
265 """
266 cc = []
267 if process_tags:
268 cc += gitutil.build_email_list(commit.tags,
269 warn_on_error=warn_on_error)
270 cc += gitutil.build_email_list(commit.cc_list,
271 warn_on_error=warn_on_error)
272 if type(add_maintainers) == type(cc):
273 cc += add_maintainers
274 elif add_maintainers:
275 cc += get_maintainer.get_maintainer(get_maintainer_script,
276 commit.patch)
277 all_skips |= set(cc) & set(settings.bounces)
278 cc = list(set(cc) - set(settings.bounces))
279 if limit is not None:
280 cc = cc[:limit]
281 return cc
282
Simon Glass0fb560d2021-01-23 08:56:15 -0700283 def MakeCcFile(self, process_tags, cover_fname, warn_on_error,
Maxim Cournoyer8c042fb2022-12-20 00:28:46 -0500284 add_maintainers, limit, get_maintainer_script):
Simon Glass0d24de92012-01-14 15:12:45 +0000285 """Make a cc file for us to use for per-commit Cc automation
286
Doug Andersond94566a2012-12-03 14:40:42 +0000287 Also stores in self._generated_cc to make ShowActions() faster.
288
Simon Glass0d24de92012-01-14 15:12:45 +0000289 Args:
Simon Glassc524cd62023-03-08 10:52:53 -0800290 process_tags (bool): Process tags as if they were aliases
291 cover_fname (str): If non-None the name of the cover letter.
292 warn_on_error (bool): True to print a warning when an alias fails to
293 match, False to ignore it.
294 add_maintainers (bool or list of str): Either:
Simon Glass1f487f82017-05-29 15:31:29 -0600295 True/False to call the get_maintainers to CC maintainers
296 List of maintainers to include (for testing)
Simon Glassc524cd62023-03-08 10:52:53 -0800297 limit (int): Limit the length of the Cc list (None if no limit)
298 get_maintainer_script (str): The file name of the get_maintainer.pl
Maxim Cournoyer8c042fb2022-12-20 00:28:46 -0500299 script (or compatible).
Simon Glass0d24de92012-01-14 15:12:45 +0000300 Return:
301 Filename of temp file created
302 """
Chris Packhame11aa602017-09-01 20:57:53 +1200303 col = terminal.Color()
Simon Glass0d24de92012-01-14 15:12:45 +0000304 # Look for commit tags (of the form 'xxx:' at the start of the subject)
305 fname = '/tmp/patman.%d' % os.getpid()
Simon Glass272cd852019-10-31 07:42:51 -0600306 fd = open(fname, 'w', encoding='utf-8')
Doug Anderson31187252012-12-03 14:40:43 +0000307 all_ccs = []
Simon Glassc524cd62023-03-08 10:52:53 -0800308 all_skips = set()
Simon Glass27409e32023-03-08 10:52:54 -0800309 with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
310 for i, commit in enumerate(self.commits):
311 commit.seq = i
312 commit.future = executor.submit(
313 self.GetCcForCommit, commit, process_tags, warn_on_error,
314 add_maintainers, limit, get_maintainer_script, all_skips)
315
316 # Show progress any commits that are taking forever
317 lastlen = 0
318 while True:
319 left = [commit for commit in self.commits
320 if not commit.future.done()]
321 if not left:
322 break
323 names = ', '.join(f'{c.seq + 1}:{c.subject}'
324 for c in left[:2])
325 out = f'\r{len(left)} remaining: {names}'[:79]
326 spaces = ' ' * (lastlen - len(out))
327 if lastlen: # Don't print anything the first time
328 print(out, spaces, end='')
329 sys.stdout.flush()
330 lastlen = len(out)
331 time.sleep(.25)
332 print(f'\rdone{" " * lastlen}\r', end='')
333 print('Cc processing complete')
334
Simon Glass0d24de92012-01-14 15:12:45 +0000335 for commit in self.commits:
Simon Glass27409e32023-03-08 10:52:54 -0800336 cc = commit.future.result()
Simon Glassa44f4fb2017-05-29 15:31:30 -0600337 all_ccs += cc
Dmitry Torokhov8ab452d2019-10-21 20:09:56 -0700338 print(commit.patch, '\0'.join(sorted(set(cc))), file=fd)
Simon Glassa44f4fb2017-05-29 15:31:30 -0600339 self._generated_cc[commit.patch] = cc
Simon Glass0d24de92012-01-14 15:12:45 +0000340
Simon Glassc524cd62023-03-08 10:52:53 -0800341 for x in sorted(all_skips):
342 print(col.build(col.YELLOW, f'Skipping "{x}"'))
343
Doug Anderson31187252012-12-03 14:40:43 +0000344 if cover_fname:
Simon Glass0157b182022-01-29 14:14:11 -0700345 cover_cc = gitutil.build_email_list(self.get('cover_cc', ''))
Simon Glasscf0ef932020-02-27 18:49:23 -0700346 cover_cc = list(set(cover_cc + all_ccs))
347 if limit is not None:
348 cover_cc = cover_cc[:limit]
Simon Glassfc0056e2020-11-08 20:36:18 -0700349 cc_list = '\0'.join([x for x in sorted(cover_cc)])
Robert Beckett677dac22019-11-13 18:39:45 +0000350 print(cover_fname, cc_list, file=fd)
Doug Anderson31187252012-12-03 14:40:43 +0000351
Simon Glass0d24de92012-01-14 15:12:45 +0000352 fd.close()
353 return fname
354
355 def AddChange(self, version, commit, info):
356 """Add a new change line to a version.
357
358 This will later appear in the change log.
359
360 Args:
361 version: version number to add change list to
362 info: change line for this version
363 """
364 if not self.changes.get(version):
365 self.changes[version] = []
366 self.changes[version].append([commit, info])
367
368 def GetPatchPrefix(self):
369 """Get the patch version string
370
371 Return:
372 Patch string, like 'RFC PATCH v5' or just 'PATCH'
373 """
Simon Glass0157b182022-01-29 14:14:11 -0700374 git_prefix = gitutil.get_default_subject_prefix()
Wu, Josh3871cd82015-04-15 10:25:18 +0800375 if git_prefix:
Paul Burton12e54762016-09-27 16:03:49 +0100376 git_prefix = '%s][' % git_prefix
Wu, Josh3871cd82015-04-15 10:25:18 +0800377 else:
378 git_prefix = ''
379
Simon Glass0d24de92012-01-14 15:12:45 +0000380 version = ''
381 if self.get('version'):
382 version = ' v%s' % self['version']
383
384 # Get patch name prefix
385 prefix = ''
386 if self.get('prefix'):
387 prefix = '%s ' % self['prefix']
Sean Anderson082c1192021-10-22 19:07:04 -0400388
389 postfix = ''
390 if self.get('postfix'):
391 postfix = ' %s' % self['postfix']
392 return '%s%sPATCH%s%s' % (git_prefix, prefix, postfix, version)