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