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'] 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 if self.cover: 137 print('Cover: %d lines' % len(self.cover)) 138 cover_cc = gitutil.BuildEmailList(self.get('cover_cc', '')) 139 all_ccs = itertools.chain(cover_cc, *self._generated_cc.values()) 140 for email in sorted(set(all_ccs) - to_set - cc_set): 141 print(' Cc: ', email) 142 if cmd: 143 print('Git command: %s' % cmd) 144 145 def MakeChangeLog(self, commit): 146 """Create a list of changes for each version. 147 148 Return: 149 The change log as a list of strings, one per line 150 151 Changes in v4: 152 - Jog the dial back closer to the widget 153 154 Changes in v2: 155 - Fix the widget 156 - Jog the dial 157 158 If there are no new changes in a patch, a note will be added 159 160 (no changes since v2) 161 162 Changes in v2: 163 - Fix the widget 164 - Jog the dial 165 """ 166 # Collect changes from the series and this commit 167 changes = collections.defaultdict(list) 168 for version, changelist in self.changes.items(): 169 changes[version] += changelist 170 if commit: 171 for version, changelist in commit.changes.items(): 172 changes[version] += [[commit, text] for text in changelist] 173 174 versions = sorted(changes, reverse=True) 175 newest_version = 1 176 if 'version' in self: 177 newest_version = max(newest_version, int(self.version)) 178 if versions: 179 newest_version = max(newest_version, versions[0]) 180 181 final = [] 182 process_it = self.get('process_log', '').split(',') 183 process_it = [item.strip() for item in process_it] 184 need_blank = False 185 for version in versions: 186 out = [] 187 for this_commit, text in changes[version]: 188 if commit and this_commit != commit: 189 continue 190 if 'uniq' not in process_it or text not in out: 191 out.append(text) 192 if 'sort' in process_it: 193 out = sorted(out) 194 have_changes = len(out) > 0 195 line = 'Changes in v%d:' % version 196 if have_changes: 197 out.insert(0, line) 198 if version < newest_version and len(final) == 0: 199 out.insert(0, '') 200 out.insert(0, '(no changes since v%d)' % version) 201 newest_version = 0 202 # Only add a new line if we output something 203 if need_blank: 204 out.insert(0, '') 205 need_blank = False 206 final += out 207 need_blank = need_blank or have_changes 208 209 if len(final) > 0: 210 final.append('') 211 elif newest_version != 1: 212 final = ['(no changes since v1)', ''] 213 return final 214 215 def DoChecks(self): 216 """Check that each version has a change log 217 218 Print an error if something is wrong. 219 """ 220 col = terminal.Color() 221 if self.get('version'): 222 changes_copy = dict(self.changes) 223 for version in range(1, int(self.version) + 1): 224 if self.changes.get(version): 225 del changes_copy[version] 226 else: 227 if version > 1: 228 str = 'Change log missing for v%d' % version 229 print(col.Color(col.RED, str)) 230 for version in changes_copy: 231 str = 'Change log for unknown version v%d' % version 232 print(col.Color(col.RED, str)) 233 elif self.changes: 234 str = 'Change log exists, but no version is set' 235 print(col.Color(col.RED, str)) 236 237 def MakeCcFile(self, process_tags, cover_fname, raise_on_error, 238 add_maintainers, limit): 239 """Make a cc file for us to use for per-commit Cc automation 240 241 Also stores in self._generated_cc to make ShowActions() faster. 242 243 Args: 244 process_tags: Process tags as if they were aliases 245 cover_fname: If non-None the name of the cover letter. 246 raise_on_error: True to raise an error when an alias fails to match, 247 False to just print a message. 248 add_maintainers: Either: 249 True/False to call the get_maintainers to CC maintainers 250 List of maintainers to include (for testing) 251 limit: Limit the length of the Cc list (None if no limit) 252 Return: 253 Filename of temp file created 254 """ 255 col = terminal.Color() 256 # Look for commit tags (of the form 'xxx:' at the start of the subject) 257 fname = '/tmp/patman.%d' % os.getpid() 258 fd = open(fname, 'w', encoding='utf-8') 259 all_ccs = [] 260 for commit in self.commits: 261 cc = [] 262 if process_tags: 263 cc += gitutil.BuildEmailList(commit.tags, 264 raise_on_error=raise_on_error) 265 cc += gitutil.BuildEmailList(commit.cc_list, 266 raise_on_error=raise_on_error) 267 if type(add_maintainers) == type(cc): 268 cc += add_maintainers 269 elif add_maintainers: 270 dir_list = [os.path.join(gitutil.GetTopLevel(), 'scripts')] 271 cc += get_maintainer.GetMaintainer(dir_list, commit.patch) 272 for x in set(cc) & set(settings.bounces): 273 print(col.Color(col.YELLOW, 'Skipping "%s"' % x)) 274 cc = list(set(cc) - set(settings.bounces)) 275 if limit is not None: 276 cc = cc[:limit] 277 all_ccs += cc 278 print(commit.patch, '\0'.join(sorted(set(cc))), file=fd) 279 self._generated_cc[commit.patch] = cc 280 281 if cover_fname: 282 cover_cc = gitutil.BuildEmailList(self.get('cover_cc', '')) 283 cover_cc = list(set(cover_cc + all_ccs)) 284 if limit is not None: 285 cover_cc = cover_cc[:limit] 286 cc_list = '\0'.join([x for x in sorted(cover_cc)]) 287 print(cover_fname, cc_list, file=fd) 288 289 fd.close() 290 return fname 291 292 def AddChange(self, version, commit, info): 293 """Add a new change line to a version. 294 295 This will later appear in the change log. 296 297 Args: 298 version: version number to add change list to 299 info: change line for this version 300 """ 301 if not self.changes.get(version): 302 self.changes[version] = [] 303 self.changes[version].append([commit, info]) 304 305 def GetPatchPrefix(self): 306 """Get the patch version string 307 308 Return: 309 Patch string, like 'RFC PATCH v5' or just 'PATCH' 310 """ 311 git_prefix = gitutil.GetDefaultSubjectPrefix() 312 if git_prefix: 313 git_prefix = '%s][' % git_prefix 314 else: 315 git_prefix = '' 316 317 version = '' 318 if self.get('version'): 319 version = ' v%s' % self['version'] 320 321 # Get patch name prefix 322 prefix = '' 323 if self.get('prefix'): 324 prefix = '%s ' % self['prefix'] 325 return '%s%sPATCH%s' % (git_prefix, prefix, version) 326