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