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 command 23import re 24import os 25import series 26import settings 27import subprocess 28import sys 29import terminal 30 31 32def CountCommitsToBranch(): 33 """Returns number of commits between HEAD and the tracking branch. 34 35 This looks back to the tracking branch and works out the number of commits 36 since then. 37 38 Return: 39 Number of patches that exist on top of the branch 40 """ 41 pipe = [['git', 'log', '--oneline', '@{upstream}..'], 42 ['wc', '-l']] 43 stdout = command.RunPipe(pipe, capture=True, oneline=True) 44 patch_count = int(stdout) 45 return patch_count 46 47def CreatePatches(start, count, series): 48 """Create a series of patches from the top of the current branch. 49 50 The patch files are written to the current directory using 51 git format-patch. 52 53 Args: 54 start: Commit to start from: 0=HEAD, 1=next one, etc. 55 count: number of commits to include 56 Return: 57 Filename of cover letter 58 List of filenames of patch files 59 """ 60 if series.get('version'): 61 version = '%s ' % series['version'] 62 cmd = ['git', 'format-patch', '-M', '--signoff'] 63 if series.get('cover'): 64 cmd.append('--cover-letter') 65 prefix = series.GetPatchPrefix() 66 if prefix: 67 cmd += ['--subject-prefix=%s' % prefix] 68 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)] 69 70 stdout = command.RunList(cmd) 71 files = stdout.splitlines() 72 73 # We have an extra file if there is a cover letter 74 if series.get('cover'): 75 return files[0], files[1:] 76 else: 77 return None, files 78 79def ApplyPatch(verbose, fname): 80 """Apply a patch with git am to test it 81 82 TODO: Convert these to use command, with stderr option 83 84 Args: 85 fname: filename of patch file to apply 86 """ 87 cmd = ['git', 'am', fname] 88 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, 89 stderr=subprocess.PIPE) 90 stdout, stderr = pipe.communicate() 91 re_error = re.compile('^error: patch failed: (.+):(\d+)') 92 for line in stderr.splitlines(): 93 if verbose: 94 print line 95 match = re_error.match(line) 96 if match: 97 print GetWarningMsg('warning', match.group(1), int(match.group(2)), 98 'Patch failed') 99 return pipe.returncode == 0, stdout 100 101def ApplyPatches(verbose, args, start_point): 102 """Apply the patches with git am to make sure all is well 103 104 Args: 105 verbose: Print out 'git am' output verbatim 106 args: List of patch files to apply 107 start_point: Number of commits back from HEAD to start applying. 108 Normally this is len(args), but it can be larger if a start 109 offset was given. 110 """ 111 error_count = 0 112 col = terminal.Color() 113 114 # Figure out our current position 115 cmd = ['git', 'name-rev', 'HEAD', '--name-only'] 116 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE) 117 stdout, stderr = pipe.communicate() 118 if pipe.returncode: 119 str = 'Could not find current commit name' 120 print col.Color(col.RED, str) 121 print stdout 122 return False 123 old_head = stdout.splitlines()[0] 124 125 # Checkout the required start point 126 cmd = ['git', 'checkout', 'HEAD~%d' % start_point] 127 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, 128 stderr=subprocess.PIPE) 129 stdout, stderr = pipe.communicate() 130 if pipe.returncode: 131 str = 'Could not move to commit before patch series' 132 print col.Color(col.RED, str) 133 print stdout, stderr 134 return False 135 136 # Apply all the patches 137 for fname in args: 138 ok, stdout = ApplyPatch(verbose, fname) 139 if not ok: 140 print col.Color(col.RED, 'git am returned errors for %s: will ' 141 'skip this patch' % fname) 142 if verbose: 143 print stdout 144 error_count += 1 145 cmd = ['git', 'am', '--skip'] 146 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE) 147 stdout, stderr = pipe.communicate() 148 if pipe.returncode != 0: 149 print col.Color(col.RED, 'Unable to skip patch! Aborting...') 150 print stdout 151 break 152 153 # Return to our previous position 154 cmd = ['git', 'checkout', old_head] 155 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 156 stdout, stderr = pipe.communicate() 157 if pipe.returncode: 158 print col.Color(col.RED, 'Could not move back to head commit') 159 print stdout, stderr 160 return error_count == 0 161 162def BuildEmailList(in_list, tag=None, alias=None): 163 """Build a list of email addresses based on an input list. 164 165 Takes a list of email addresses and aliases, and turns this into a list 166 of only email address, by resolving any aliases that are present. 167 168 If the tag is given, then each email address is prepended with this 169 tag and a space. If the tag starts with a minus sign (indicating a 170 command line parameter) then the email address is quoted. 171 172 Args: 173 in_list: List of aliases/email addresses 174 tag: Text to put before each address 175 176 Returns: 177 List of email addresses 178 179 >>> alias = {} 180 >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 181 >>> alias['john'] = ['j.bloggs@napier.co.nz'] 182 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>'] 183 >>> alias['boys'] = ['fred', ' john'] 184 >>> alias['all'] = ['fred ', 'john', ' mary '] 185 >>> BuildEmailList(['john', 'mary'], None, alias) 186 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>'] 187 >>> BuildEmailList(['john', 'mary'], '--to', alias) 188 ['--to "j.bloggs@napier.co.nz"', \ 189'--to "Mary Poppins <m.poppins@cloud.net>"'] 190 >>> BuildEmailList(['john', 'mary'], 'Cc', alias) 191 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>'] 192 """ 193 quote = '"' if tag and tag[0] == '-' else '' 194 raw = [] 195 for item in in_list: 196 raw += LookupEmail(item, alias) 197 result = [] 198 for item in raw: 199 if not item in result: 200 result.append(item) 201 if tag: 202 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result] 203 return result 204 205def EmailPatches(series, cover_fname, args, dry_run, cc_fname, 206 self_only=False, alias=None): 207 """Email a patch series. 208 209 Args: 210 series: Series object containing destination info 211 cover_fname: filename of cover letter 212 args: list of filenames of patch files 213 dry_run: Just return the command that would be run 214 cc_fname: Filename of Cc file for per-commit Cc 215 self_only: True to just email to yourself as a test 216 217 Returns: 218 Git command that was/would be run 219 220 >>> alias = {} 221 >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 222 >>> alias['john'] = ['j.bloggs@napier.co.nz'] 223 >>> alias['mary'] = ['m.poppins@cloud.net'] 224 >>> alias['boys'] = ['fred', ' john'] 225 >>> alias['all'] = ['fred ', 'john', ' mary '] 226 >>> alias[os.getenv('USER')] = ['this-is-me@me.com'] 227 >>> series = series.Series() 228 >>> series.to = ['fred'] 229 >>> series.cc = ['mary'] 230 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \ 231 alias) 232 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 233"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 234 >>> EmailPatches(series, None, ['p1'], True, 'cc-fname', False, alias) 235 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 236"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1' 237 >>> series.cc = ['all'] 238 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', True, \ 239 alias) 240 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \ 241--cc-cmd cc-fname" cover p1 p2' 242 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \ 243 alias) 244 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 245"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \ 246"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 247 """ 248 to = BuildEmailList(series.get('to'), '--to', alias) 249 if not to: 250 print ("No recipient, please add something like this to a commit\n" 251 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>") 252 return 253 cc = BuildEmailList(series.get('cc'), '--cc', alias) 254 if self_only: 255 to = BuildEmailList([os.getenv('USER')], '--to', alias) 256 cc = [] 257 cmd = ['git', 'send-email', '--annotate'] 258 cmd += to 259 cmd += cc 260 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)] 261 if cover_fname: 262 cmd.append(cover_fname) 263 cmd += args 264 str = ' '.join(cmd) 265 if not dry_run: 266 os.system(str) 267 return str 268 269 270def LookupEmail(lookup_name, alias=None, level=0): 271 """If an email address is an alias, look it up and return the full name 272 273 TODO: Why not just use git's own alias feature? 274 275 Args: 276 lookup_name: Alias or email address to look up 277 278 Returns: 279 tuple: 280 list containing a list of email addresses 281 282 Raises: 283 OSError if a recursive alias reference was found 284 ValueError if an alias was not found 285 286 >>> alias = {} 287 >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 288 >>> alias['john'] = ['j.bloggs@napier.co.nz'] 289 >>> alias['mary'] = ['m.poppins@cloud.net'] 290 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz'] 291 >>> alias['all'] = ['fred ', 'john', ' mary '] 292 >>> alias['loop'] = ['other', 'john', ' mary '] 293 >>> alias['other'] = ['loop', 'john', ' mary '] 294 >>> LookupEmail('mary', alias) 295 ['m.poppins@cloud.net'] 296 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias) 297 ['arthur.wellesley@howe.ro.uk'] 298 >>> LookupEmail('boys', alias) 299 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz'] 300 >>> LookupEmail('all', alias) 301 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net'] 302 >>> LookupEmail('odd', alias) 303 Traceback (most recent call last): 304 ... 305 ValueError: Alias 'odd' not found 306 >>> LookupEmail('loop', alias) 307 Traceback (most recent call last): 308 ... 309 OSError: Recursive email alias at 'other' 310 """ 311 if not alias: 312 alias = settings.alias 313 lookup_name = lookup_name.strip() 314 if '@' in lookup_name: # Perhaps a real email address 315 return [lookup_name] 316 317 lookup_name = lookup_name.lower() 318 319 if level > 10: 320 raise OSError, "Recursive email alias at '%s'" % lookup_name 321 322 out_list = [] 323 if lookup_name: 324 if not lookup_name in alias: 325 raise ValueError, "Alias '%s' not found" % lookup_name 326 for item in alias[lookup_name]: 327 todo = LookupEmail(item, alias, level + 1) 328 for new_item in todo: 329 if not new_item in out_list: 330 out_list.append(new_item) 331 332 #print "No match for alias '%s'" % lookup_name 333 return out_list 334 335def GetTopLevel(): 336 """Return name of top-level directory for this git repo. 337 338 Returns: 339 Full path to git top-level directory 340 341 This test makes sure that we are running tests in the right subdir 342 343 >>> os.path.realpath(os.getcwd()) == \ 344 os.path.join(GetTopLevel(), 'tools', 'scripts', 'patman') 345 True 346 """ 347 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel') 348 349def GetAliasFile(): 350 """Gets the name of the git alias file. 351 352 Returns: 353 Filename of git alias file, or None if none 354 """ 355 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile') 356 if fname: 357 fname = os.path.join(GetTopLevel(), fname.strip()) 358 return fname 359 360def GetDefaultUserName(): 361 """Gets the user.name from .gitconfig file. 362 363 Returns: 364 User name found in .gitconfig file, or None if none 365 """ 366 uname = command.OutputOneLine('git', 'config', '--global', 'user.name') 367 return uname 368 369def GetDefaultUserEmail(): 370 """Gets the user.email from the global .gitconfig file. 371 372 Returns: 373 User's email found in .gitconfig file, or None if none 374 """ 375 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email') 376 return uemail 377 378def Setup(): 379 """Set up git utils, by reading the alias files.""" 380 settings.Setup('') 381 382 # Check for a git alias file also 383 alias_fname = GetAliasFile() 384 if alias_fname: 385 settings.ReadGitAliases(alias_fname) 386 387if __name__ == "__main__": 388 import doctest 389 390 doctest.testmod() 391