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