1 # Copyright (c) 2011 The Chromium OS Authors.
3 # See file CREDITS for list of people who contributed to this
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.
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.
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,
33 def CountCommitsToBranch():
34 """Returns number of commits between HEAD and the tracking branch.
36 This looks back to the tracking branch and works out the number of commits
40 Number of patches that exist on top of the branch
42 pipe = [['git', 'log', '--no-color', '--oneline', '--no-decorate',
45 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
46 patch_count = int(stdout)
49 def GetUpstream(git_dir, branch):
50 """Returns the name of the upstream for a branch
53 git_dir: Git directory containing repo
54 branch: Name of branch
57 Name of upstream branch (e.g. 'upstream/master') or None if none
60 remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
61 'branch.%s.remote' % branch)
62 merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
63 'branch.%s.merge' % branch)
69 elif remote and merge:
70 leaf = merge.split('/')[-1]
71 return '%s/%s' % (remote, leaf)
73 raise ValueError, ("Cannot determine upstream branch for branch "
74 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
77 def GetRangeInBranch(git_dir, branch, include_upstream=False):
78 """Returns an expression for the commits in the given branch.
81 git_dir: Directory containing git repo
82 branch: Name of branch
84 Expression in the form 'upstream..branch' which can be used to
85 access the commits. If the branch does not exist, returns None.
87 upstream = GetUpstream(git_dir, branch)
90 return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
92 def CountCommitsInBranch(git_dir, branch, include_upstream=False):
93 """Returns the number of commits in the given branch.
96 git_dir: Directory containing git repo
97 branch: Name of branch
99 Number of patches that exist on top of the branch, or None if the
100 branch does not exist.
102 range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
105 pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', '--no-decorate',
108 result = command.RunPipe(pipe, capture=True, oneline=True)
109 patch_count = int(result.stdout)
112 def CountCommits(commit_range):
113 """Returns the number of commits in the given range.
116 commit_range: Range of commits to count (e.g. 'HEAD..base')
118 Number of patches that exist on top of the branch
120 pipe = [['git', 'log', '--oneline', '--no-decorate', commit_range],
122 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
123 patch_count = int(stdout)
126 def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
127 """Checkout the selected commit for this build
130 commit_hash: Commit hash to check out
134 pipe.extend(['--git-dir', git_dir])
136 pipe.extend(['--work-tree', work_tree])
137 pipe.append('checkout')
140 pipe.append(commit_hash)
141 result = command.RunPipe([pipe], capture=True, raise_on_error=False)
142 if result.return_code != 0:
143 raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
145 def Clone(git_dir, output_dir):
146 """Checkout the selected commit for this build
149 commit_hash: Commit hash to check out
151 pipe = ['git', 'clone', git_dir, '.']
152 result = command.RunPipe([pipe], capture=True, cwd=output_dir)
153 if result.return_code != 0:
154 raise OSError, 'git clone: %s' % result.stderr
156 def Fetch(git_dir=None, work_tree=None):
157 """Fetch from the origin repo
160 commit_hash: Commit hash to check out
164 pipe.extend(['--git-dir', git_dir])
166 pipe.extend(['--work-tree', work_tree])
168 result = command.RunPipe([pipe], capture=True)
169 if result.return_code != 0:
170 raise OSError, 'git fetch: %s' % result.stderr
172 def CreatePatches(start, count, series):
173 """Create a series of patches from the top of the current branch.
175 The patch files are written to the current directory using
179 start: Commit to start from: 0=HEAD, 1=next one, etc.
180 count: number of commits to include
182 Filename of cover letter
183 List of filenames of patch files
185 if series.get('version'):
186 version = '%s ' % series['version']
187 cmd = ['git', 'format-patch', '-M', '--signoff']
188 if series.get('cover'):
189 cmd.append('--cover-letter')
190 prefix = series.GetPatchPrefix()
192 cmd += ['--subject-prefix=%s' % prefix]
193 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
195 stdout = command.RunList(cmd)
196 files = stdout.splitlines()
198 # We have an extra file if there is a cover letter
199 if series.get('cover'):
200 return files[0], files[1:]
204 def ApplyPatch(verbose, fname):
205 """Apply a patch with git am to test it
207 TODO: Convert these to use command, with stderr option
210 fname: filename of patch file to apply
212 cmd = ['git', 'am', fname]
213 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
214 stderr=subprocess.PIPE)
215 stdout, stderr = pipe.communicate()
216 re_error = re.compile('^error: patch failed: (.+):(\d+)')
217 for line in stderr.splitlines():
220 match = re_error.match(line)
222 print GetWarningMsg('warning', match.group(1), int(match.group(2)),
224 return pipe.returncode == 0, stdout
226 def ApplyPatches(verbose, args, start_point):
227 """Apply the patches with git am to make sure all is well
230 verbose: Print out 'git am' output verbatim
231 args: List of patch files to apply
232 start_point: Number of commits back from HEAD to start applying.
233 Normally this is len(args), but it can be larger if a start
237 col = terminal.Color()
239 # Figure out our current position
240 cmd = ['git', 'name-rev', 'HEAD', '--name-only']
241 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
242 stdout, stderr = pipe.communicate()
244 str = 'Could not find current commit name'
245 print col.Color(col.RED, str)
248 old_head = stdout.splitlines()[0]
250 # Checkout the required start point
251 cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
252 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
253 stderr=subprocess.PIPE)
254 stdout, stderr = pipe.communicate()
256 str = 'Could not move to commit before patch series'
257 print col.Color(col.RED, str)
261 # Apply all the patches
263 ok, stdout = ApplyPatch(verbose, fname)
265 print col.Color(col.RED, 'git am returned errors for %s: will '
266 'skip this patch' % fname)
270 cmd = ['git', 'am', '--skip']
271 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
272 stdout, stderr = pipe.communicate()
273 if pipe.returncode != 0:
274 print col.Color(col.RED, 'Unable to skip patch! Aborting...')
278 # Return to our previous position
279 cmd = ['git', 'checkout', old_head]
280 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
281 stdout, stderr = pipe.communicate()
283 print col.Color(col.RED, 'Could not move back to head commit')
285 return error_count == 0
287 def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
288 """Build a list of email addresses based on an input list.
290 Takes a list of email addresses and aliases, and turns this into a list
291 of only email address, by resolving any aliases that are present.
293 If the tag is given, then each email address is prepended with this
294 tag and a space. If the tag starts with a minus sign (indicating a
295 command line parameter) then the email address is quoted.
298 in_list: List of aliases/email addresses
299 tag: Text to put before each address
300 alias: Alias dictionary
301 raise_on_error: True to raise an error when an alias fails to match,
302 False to just print a message.
305 List of email addresses
308 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
309 >>> alias['john'] = ['j.bloggs@napier.co.nz']
310 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
311 >>> alias['boys'] = ['fred', ' john']
312 >>> alias['all'] = ['fred ', 'john', ' mary ']
313 >>> BuildEmailList(['john', 'mary'], None, alias)
314 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
315 >>> BuildEmailList(['john', 'mary'], '--to', alias)
316 ['--to "j.bloggs@napier.co.nz"', \
317 '--to "Mary Poppins <m.poppins@cloud.net>"']
318 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
319 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
321 quote = '"' if tag and tag[0] == '-' else ''
324 raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
327 if not item in result:
330 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
333 def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
334 self_only=False, alias=None, in_reply_to=None):
335 """Email a patch series.
338 series: Series object containing destination info
339 cover_fname: filename of cover letter
340 args: list of filenames of patch files
341 dry_run: Just return the command that would be run
342 raise_on_error: True to raise an error when an alias fails to match,
343 False to just print a message.
344 cc_fname: Filename of Cc file for per-commit Cc
345 self_only: True to just email to yourself as a test
346 in_reply_to: If set we'll pass this to git as --in-reply-to.
347 Should be a message ID that this is in reply to.
350 Git command that was/would be run
352 # For the duration of this doctest pretend that we ran patman with ./patman
353 >>> _old_argv0 = sys.argv[0]
354 >>> sys.argv[0] = './patman'
357 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
358 >>> alias['john'] = ['j.bloggs@napier.co.nz']
359 >>> alias['mary'] = ['m.poppins@cloud.net']
360 >>> alias['boys'] = ['fred', ' john']
361 >>> alias['all'] = ['fred ', 'john', ' mary ']
362 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
363 >>> series = series.Series()
364 >>> series.to = ['fred']
365 >>> series.cc = ['mary']
366 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
368 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
369 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
370 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
372 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
373 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
374 >>> series.cc = ['all']
375 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
377 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
378 --cc-cmd cc-fname" cover p1 p2'
379 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
381 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
382 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
383 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
385 # Restore argv[0] since we clobbered it.
386 >>> sys.argv[0] = _old_argv0
388 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
390 print ("No recipient, please add something like this to a commit\n"
391 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
393 cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
395 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
397 cmd = ['git', 'send-email', '--annotate']
399 cmd.append('--in-reply-to="%s"' % in_reply_to)
403 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
405 cmd.append(cover_fname)
413 def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
414 """If an email address is an alias, look it up and return the full name
416 TODO: Why not just use git's own alias feature?
419 lookup_name: Alias or email address to look up
420 alias: Dictionary containing aliases (None to use settings default)
421 raise_on_error: True to raise an error when an alias fails to match,
422 False to just print a message.
426 list containing a list of email addresses
429 OSError if a recursive alias reference was found
430 ValueError if an alias was not found
433 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
434 >>> alias['john'] = ['j.bloggs@napier.co.nz']
435 >>> alias['mary'] = ['m.poppins@cloud.net']
436 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
437 >>> alias['all'] = ['fred ', 'john', ' mary ']
438 >>> alias['loop'] = ['other', 'john', ' mary ']
439 >>> alias['other'] = ['loop', 'john', ' mary ']
440 >>> LookupEmail('mary', alias)
441 ['m.poppins@cloud.net']
442 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
443 ['arthur.wellesley@howe.ro.uk']
444 >>> LookupEmail('boys', alias)
445 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
446 >>> LookupEmail('all', alias)
447 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
448 >>> LookupEmail('odd', alias)
449 Traceback (most recent call last):
451 ValueError: Alias 'odd' not found
452 >>> LookupEmail('loop', alias)
453 Traceback (most recent call last):
455 OSError: Recursive email alias at 'other'
456 >>> LookupEmail('odd', alias, raise_on_error=False)
457 \033[1;31mAlias 'odd' not found\033[0m
459 >>> # In this case the loop part will effectively be ignored.
460 >>> LookupEmail('loop', alias, raise_on_error=False)
461 \033[1;31mRecursive email alias at 'other'\033[0m
462 \033[1;31mRecursive email alias at 'john'\033[0m
463 \033[1;31mRecursive email alias at 'mary'\033[0m
464 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
467 alias = settings.alias
468 lookup_name = lookup_name.strip()
469 if '@' in lookup_name: # Perhaps a real email address
472 lookup_name = lookup_name.lower()
473 col = terminal.Color()
477 msg = "Recursive email alias at '%s'" % lookup_name
481 print col.Color(col.RED, msg)
485 if not lookup_name in alias:
486 msg = "Alias '%s' not found" % lookup_name
488 raise ValueError, msg
490 print col.Color(col.RED, msg)
492 for item in alias[lookup_name]:
493 todo = LookupEmail(item, alias, raise_on_error, level + 1)
494 for new_item in todo:
495 if not new_item in out_list:
496 out_list.append(new_item)
498 #print "No match for alias '%s'" % lookup_name
502 """Return name of top-level directory for this git repo.
505 Full path to git top-level directory
507 This test makes sure that we are running tests in the right subdir
509 >>> os.path.realpath(os.path.dirname(__file__)) == \
510 os.path.join(GetTopLevel(), 'tools', 'patman')
513 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
516 """Gets the name of the git alias file.
519 Filename of git alias file, or None if none
521 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
522 raise_on_error=False)
524 fname = os.path.join(GetTopLevel(), fname.strip())
527 def GetDefaultUserName():
528 """Gets the user.name from .gitconfig file.
531 User name found in .gitconfig file, or None if none
533 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
536 def GetDefaultUserEmail():
537 """Gets the user.email from the global .gitconfig file.
540 User's email found in .gitconfig file, or None if none
542 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
546 """Set up git utils, by reading the alias files."""
547 # Check for a git alias file also
548 alias_fname = GetAliasFile()
550 settings.ReadGitAliases(alias_fname)
553 """Get the hash of the current HEAD
558 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
560 if __name__ == "__main__":