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