1# SPDX-License-Identifier: GPL-2.0+ 2# Copyright (c) 2011 The Chromium OS Authors. 3# 4 5from __future__ import print_function 6 7import collections 8import itertools 9import os 10 11from patman import get_maintainer 12from patman import gitutil 13from patman import settings 14from patman import terminal 15from patman import tools 16 17# Series-xxx tags that we understand 18valid_series = ['to', 'cc', 'version', 'changes', 'prefix', 'notes', 'name', 19 'cover_cc', 'process_log', 'links', 'patchwork_url', 'postfix'] 20 21class Series(dict): 22 """Holds information about a patch series, including all tags. 23 24 Vars: 25 cc: List of aliases/emails to Cc all patches to 26 commits: List of Commit objects, one for each patch 27 cover: List of lines in the cover letter 28 notes: List of lines in the notes 29 changes: (dict) List of changes for each version, The key is 30 the integer version number 31 allow_overwrite: Allow tags to overwrite an existing tag 32 """ 33 def __init__(self): 34 self.cc = [] 35 self.to = [] 36 self.cover_cc = [] 37 self.commits = [] 38 self.cover = None 39 self.notes = [] 40 self.changes = {} 41 self.allow_overwrite = False 42 43 # Written in MakeCcFile() 44 # key: name of patch file 45 # value: list of email addresses 46 self._generated_cc = {} 47 48 # These make us more like a dictionary 49 def __setattr__(self, name, value): 50 self[name] = value 51 52 def __getattr__(self, name): 53 return self[name] 54 55 def AddTag(self, commit, line, name, value): 56 """Add a new Series-xxx tag along with its value. 57 58 Args: 59 line: Source line containing tag (useful for debug/error messages) 60 name: Tag name (part after 'Series-') 61 value: Tag value (part after 'Series-xxx: ') 62 63 Returns: 64 String warning if something went wrong, else None 65 """ 66 # If we already have it, then add to our list 67 name = name.replace('-', '_') 68 if name in self and not self.allow_overwrite: 69 values = value.split(',') 70 values = [str.strip() for str in values] 71 if type(self[name]) != type([]): 72 raise ValueError("In %s: line '%s': Cannot add another value " 73 "'%s' to series '%s'" % 74 (commit.hash, line, values, self[name])) 75 self[name] += values 76 77 # Otherwise just set the value 78 elif name in valid_series: 79 if name=="notes": 80 self[name] = [value] 81 else: 82 self[name] = value 83 else: 84 return ("In %s: line '%s': Unknown 'Series-%s': valid " 85 "options are %s" % (commit.hash, line, name, 86 ', '.join(valid_series))) 87 return None 88 89 def AddCommit(self, commit): 90 """Add a commit into our list of commits 91 92 We create a list of tags in the commit subject also. 93 94 Args: 95 commit: Commit object to add 96 """ 97 commit.CheckTags() 98 self.commits.append(commit) 99 100 def ShowActions(self, args, cmd, process_tags): 101 """Show what actions we will/would perform 102 103 Args: 104 args: List of patch files we created 105 cmd: The git command we would have run 106 process_tags: Process tags as if they were aliases 107 """ 108 to_set = set(gitutil.BuildEmailList(self.to)); 109 cc_set = set(gitutil.BuildEmailList(self.cc)); 110 111 col = terminal.Color() 112 print('Dry run, so not doing much. But I would do this:') 113 print() 114 print('Send a total of %d patch%s with %scover letter.' % ( 115 len(args), '' if len(args) == 1 else 'es', 116 self.get('cover') and 'a ' or 'no ')) 117 118 # TODO: Colour the patches according to whether they passed checks 119 for upto in range(len(args)): 120 commit = self.commits[upto] 121 print(col.Color(col.GREEN, ' %s' % args[upto])) 122 cc_list = list(self._generated_cc[commit.patch]) 123 for email in sorted(set(cc_list) - to_set - cc_set): 124 if email == None: 125 email = col.Color(col.YELLOW, "<alias '%s' not found>" 126 % tag) 127 if email: 128 print(' Cc: ', email) 129 print 130 for item in sorted(to_set): 131 print('To:\t ', item) 132 for item in sorted(cc_set - to_set): 133 print('Cc:\t ', item) 134 print('Version: ', self.get('version')) 135 print('Prefix:\t ', self.get('prefix')) 136 print('Postfix:\t ', self.get('postfix')) 137 if self.cover: 138 print('Cover: %d lines' % len(self.cover)) 139 cover_cc = gitutil.BuildEmailList(self.get('cover_cc', '')) 140 all_ccs = itertools.chain(cover_cc, *self._generated_cc.values()) 141 for email in sorted(set(all_ccs) - to_set - cc_set): 142 print(' Cc: ', email) 143 if cmd: 144 print('Git command: %s' % cmd) 145 146 def MakeChangeLog(self, commit): 147 """Create a list of changes for each version. 148 149 Return: 150 The change log as a list of strings, one per line 151 152 Changes in v4: 153 - Jog the dial back closer to the widget 154 155 Changes in v2: 156 - Fix the widget 157 - Jog the dial 158 159 If there are no new changes in a patch, a note will be added 160 161 (no changes since v2) 162 163 Changes in v2: 164 - Fix the widget 165 - Jog the dial 166 """ 167 # Collect changes from the series and this commit 168 changes = collections.defaultdict(list) 169 for version, changelist in self.changes.items(): 170 changes[version] += changelist 171 if commit: 172 for version, changelist in commit.changes.items(): 173 changes[version] += [[commit, text] for text in changelist] 174 175 versions = sorted(changes, reverse=True) 176 newest_version = 1 177 if 'version' in self: 178 newest_version = max(newest_version, int(self.version)) 179 if versions: 180 newest_version = max(newest_version, versions[0]) 181 182 final = [] 183 process_it = self.get('process_log', '').split(',') 184 process_it = [item.strip() for item in process_it] 185 need_blank = False 186 for version in versions: 187 out = [] 188 for this_commit, text in changes[version]: 189 if commit and this_commit != commit: 190 continue 191 if 'uniq' not in process_it or text not in out: 192 out.append(text) 193 if 'sort' in process_it: 194 out = sorted(out) 195 have_changes = len(out) > 0 196 line = 'Changes in v%d:' % version 197 if have_changes: 198 out.insert(0, line) 199 if version < newest_version and len(final) == 0: 200 out.insert(0, '') 201 out.insert(0, '(no changes since v%d)' % version) 202 newest_version = 0 203 # Only add a new line if we output something 204 if need_blank: 205 out.insert(0, '') 206 need_blank = False 207 final += out 208 need_blank = need_blank or have_changes 209 210 if len(final) > 0: 211 final.append('') 212 elif newest_version != 1: 213 final = ['(no changes since v1)', ''] 214 return final 215 216 def DoChecks(self): 217 """Check that each version has a change log 218 219 Print an error if something is wrong. 220 """ 221 col = terminal.Color() 222 if self.get('version'): 223 changes_copy = dict(self.changes) 224 for version in range(1, int(self.version) + 1): 225 if self.changes.get(version): 226 del changes_copy[version] 227 else: 228 if version > 1: 229 str = 'Change log missing for v%d' % version 230 print(col.Color(col.RED, str)) 231 for version in changes_copy: 232 str = 'Change log for unknown version v%d' % version 233 print(col.Color(col.RED, str)) 234 elif self.changes: 235 str = 'Change log exists, but no version is set' 236 print(col.Color(col.RED, str)) 237 238 def MakeCcFile(self, process_tags, cover_fname, warn_on_error, 239 add_maintainers, limit): 240 """Make a cc file for us to use for per-commit Cc automation 241 242 Also stores in self._generated_cc to make ShowActions() faster. 243 244 Args: 245 process_tags: Process tags as if they were aliases 246 cover_fname: If non-None the name of the cover letter. 247 warn_on_error: True to print a warning when an alias fails to match, 248 False to ignore it. 249 add_maintainers: Either: 250 True/False to call the get_maintainers to CC maintainers 251 List of maintainers to include (for testing) 252 limit: Limit the length of the Cc list (None if no limit) 253 Return: 254 Filename of temp file created 255 """ 256 col = terminal.Color() 257 # Look for commit tags (of the form 'xxx:' at the start of the subject) 258 fname = '/tmp/patman.%d' % os.getpid() 259 fd = open(fname, 'w', encoding='utf-8') 260 all_ccs = [] 261 for commit in self.commits: 262 cc = [] 263 if process_tags: 264 cc += gitutil.BuildEmailList(commit.tags, 265 warn_on_error=warn_on_error) 266 cc += gitutil.BuildEmailList(commit.cc_list, 267 warn_on_error=warn_on_error) 268 if type(add_maintainers) == type(cc): 269 cc += add_maintainers 270 elif add_maintainers: 271 dir_list = [os.path.join(gitutil.GetTopLevel(), 'scripts')] 272 cc += get_maintainer.GetMaintainer(dir_list, commit.patch) 273 for x in set(cc) & set(settings.bounces): 274 print(col.Color(col.YELLOW, 'Skipping "%s"' % x)) 275 cc = list(set(cc) - set(settings.bounces)) 276 if limit is not None: 277 cc = cc[:limit] 278 all_ccs += cc 279 print(commit.patch, '\0'.join(sorted(set(cc))), file=fd) 280 self._generated_cc[commit.patch] = cc 281 282 if cover_fname: 283 cover_cc = gitutil.BuildEmailList(self.get('cover_cc', '')) 284 cover_cc = list(set(cover_cc + all_ccs)) 285 if limit is not None: 286 cover_cc = cover_cc[:limit] 287 cc_list = '\0'.join([x for x in sorted(cover_cc)]) 288 print(cover_fname, cc_list, file=fd) 289 290 fd.close() 291 return fname 292 293 def AddChange(self, version, commit, info): 294 """Add a new change line to a version. 295 296 This will later appear in the change log. 297 298 Args: 299 version: version number to add change list to 300 info: change line for this version 301 """ 302 if not self.changes.get(version): 303 self.changes[version] = [] 304 self.changes[version].append([commit, info]) 305 306 def GetPatchPrefix(self): 307 """Get the patch version string 308 309 Return: 310 Patch string, like 'RFC PATCH v5' or just 'PATCH' 311 """ 312 git_prefix = gitutil.GetDefaultSubjectPrefix() 313 if git_prefix: 314 git_prefix = '%s][' % git_prefix 315 else: 316 git_prefix = '' 317 318 version = '' 319 if self.get('version'): 320 version = ' v%s' % self['version'] 321 322 # Get patch name prefix 323 prefix = '' 324 if self.get('prefix'): 325 prefix = '%s ' % self['prefix'] 326 327 postfix = '' 328 if self.get('postfix'): 329 postfix = ' %s' % self['postfix'] 330 return '%s%sPATCH%s%s' % (git_prefix, prefix, postfix, version) 331