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
59 remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
60 'branch.%s.remote' % branch)
61 merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
62 'branch.%s.merge' % branch)
65 elif remote and merge:
66 leaf = merge.split('/')[-1]
67 return '%s/%s' % (remote, leaf)
69 raise ValueError, ("Cannot determine upstream branch for branch "
70 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
73 def GetRangeInBranch(git_dir, branch, include_upstream=False):
74 """Returns an expression for the commits in the given branch.
77 git_dir: Directory containing git repo
78 branch: Name of branch
80 Expression in the form 'upstream..branch' which can be used to
83 upstream = GetUpstream(git_dir, branch)
84 return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
86 def CountCommitsInBranch(git_dir, branch, include_upstream=False):
87 """Returns the number of commits in the given branch.
90 git_dir: Directory containing git repo
91 branch: Name of branch
93 Number of patches that exist on top of the branch
95 range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
96 pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', '--no-decorate',
99 result = command.RunPipe(pipe, capture=True, oneline=True)
100 patch_count = int(result.stdout)
103 def CountCommits(commit_range):
104 """Returns the number of commits in the given range.
107 commit_range: Range of commits to count (e.g. 'HEAD..base')
109 Number of patches that exist on top of the branch
111 pipe = [['git', 'log', '--oneline', '--no-decorate', commit_range],
113 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
114 patch_count = int(stdout)
117 def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
118 """Checkout the selected commit for this build
121 commit_hash: Commit hash to check out
125 pipe.extend(['--git-dir', git_dir])
127 pipe.extend(['--work-tree', work_tree])
128 pipe.append('checkout')
131 pipe.append(commit_hash)
132 result = command.RunPipe([pipe], capture=True, raise_on_error=False)
133 if result.return_code != 0:
134 raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
136 def Clone(git_dir, output_dir):
137 """Checkout the selected commit for this build
140 commit_hash: Commit hash to check out
142 pipe = ['git', 'clone', git_dir, '.']
143 result = command.RunPipe([pipe], capture=True, cwd=output_dir)
144 if result.return_code != 0:
145 raise OSError, 'git clone: %s' % result.stderr
147 def Fetch(git_dir=None, work_tree=None):
148 """Fetch from the origin repo
151 commit_hash: Commit hash to check out
155 pipe.extend(['--git-dir', git_dir])
157 pipe.extend(['--work-tree', work_tree])
159 result = command.RunPipe([pipe], capture=True)
160 if result.return_code != 0:
161 raise OSError, 'git fetch: %s' % result.stderr
163 def CreatePatches(start, count, series):
164 """Create a series of patches from the top of the current branch.
166 The patch files are written to the current directory using
170 start: Commit to start from: 0=HEAD, 1=next one, etc.
171 count: number of commits to include
173 Filename of cover letter
174 List of filenames of patch files
176 if series.get('version'):
177 version = '%s ' % series['version']
178 cmd = ['git', 'format-patch', '-M', '--signoff']
179 if series.get('cover'):
180 cmd.append('--cover-letter')
181 prefix = series.GetPatchPrefix()
183 cmd += ['--subject-prefix=%s' % prefix]
184 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
186 stdout = command.RunList(cmd)
187 files = stdout.splitlines()
189 # We have an extra file if there is a cover letter
190 if series.get('cover'):
191 return files[0], files[1:]
195 def ApplyPatch(verbose, fname):
196 """Apply a patch with git am to test it
198 TODO: Convert these to use command, with stderr option
201 fname: filename of patch file to apply
203 cmd = ['git', 'am', fname]
204 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
205 stderr=subprocess.PIPE)
206 stdout, stderr = pipe.communicate()
207 re_error = re.compile('^error: patch failed: (.+):(\d+)')
208 for line in stderr.splitlines():
211 match = re_error.match(line)
213 print GetWarningMsg('warning', match.group(1), int(match.group(2)),
215 return pipe.returncode == 0, stdout
217 def ApplyPatches(verbose, args, start_point):
218 """Apply the patches with git am to make sure all is well
221 verbose: Print out 'git am' output verbatim
222 args: List of patch files to apply
223 start_point: Number of commits back from HEAD to start applying.
224 Normally this is len(args), but it can be larger if a start
228 col = terminal.Color()
230 # Figure out our current position
231 cmd = ['git', 'name-rev', 'HEAD', '--name-only']
232 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
233 stdout, stderr = pipe.communicate()
235 str = 'Could not find current commit name'
236 print col.Color(col.RED, str)
239 old_head = stdout.splitlines()[0]
241 # Checkout the required start point
242 cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
243 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
244 stderr=subprocess.PIPE)
245 stdout, stderr = pipe.communicate()
247 str = 'Could not move to commit before patch series'
248 print col.Color(col.RED, str)
252 # Apply all the patches
254 ok, stdout = ApplyPatch(verbose, fname)
256 print col.Color(col.RED, 'git am returned errors for %s: will '
257 'skip this patch' % fname)
261 cmd = ['git', 'am', '--skip']
262 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
263 stdout, stderr = pipe.communicate()
264 if pipe.returncode != 0:
265 print col.Color(col.RED, 'Unable to skip patch! Aborting...')
269 # Return to our previous position
270 cmd = ['git', 'checkout', old_head]
271 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
272 stdout, stderr = pipe.communicate()
274 print col.Color(col.RED, 'Could not move back to head commit')
276 return error_count == 0
278 def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
279 """Build a list of email addresses based on an input list.
281 Takes a list of email addresses and aliases, and turns this into a list
282 of only email address, by resolving any aliases that are present.
284 If the tag is given, then each email address is prepended with this
285 tag and a space. If the tag starts with a minus sign (indicating a
286 command line parameter) then the email address is quoted.
289 in_list: List of aliases/email addresses
290 tag: Text to put before each address
291 alias: Alias dictionary
292 raise_on_error: True to raise an error when an alias fails to match,
293 False to just print a message.
296 List of email addresses
299 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
300 >>> alias['john'] = ['j.bloggs@napier.co.nz']
301 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
302 >>> alias['boys'] = ['fred', ' john']
303 >>> alias['all'] = ['fred ', 'john', ' mary ']
304 >>> BuildEmailList(['john', 'mary'], None, alias)
305 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
306 >>> BuildEmailList(['john', 'mary'], '--to', alias)
307 ['--to "j.bloggs@napier.co.nz"', \
308 '--to "Mary Poppins <m.poppins@cloud.net>"']
309 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
310 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
312 quote = '"' if tag and tag[0] == '-' else ''
315 raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
318 if not item in result:
321 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
324 def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
325 self_only=False, alias=None, in_reply_to=None):
326 """Email a patch series.
329 series: Series object containing destination info
330 cover_fname: filename of cover letter
331 args: list of filenames of patch files
332 dry_run: Just return the command that would be run
333 raise_on_error: True to raise an error when an alias fails to match,
334 False to just print a message.
335 cc_fname: Filename of Cc file for per-commit Cc
336 self_only: True to just email to yourself as a test
337 in_reply_to: If set we'll pass this to git as --in-reply-to.
338 Should be a message ID that this is in reply to.
341 Git command that was/would be run
343 # For the duration of this doctest pretend that we ran patman with ./patman
344 >>> _old_argv0 = sys.argv[0]
345 >>> sys.argv[0] = './patman'
348 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
349 >>> alias['john'] = ['j.bloggs@napier.co.nz']
350 >>> alias['mary'] = ['m.poppins@cloud.net']
351 >>> alias['boys'] = ['fred', ' john']
352 >>> alias['all'] = ['fred ', 'john', ' mary ']
353 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
354 >>> series = series.Series()
355 >>> series.to = ['fred']
356 >>> series.cc = ['mary']
357 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
359 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
360 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
361 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
363 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
364 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
365 >>> series.cc = ['all']
366 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
368 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
369 --cc-cmd cc-fname" cover p1 p2'
370 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
372 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
373 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
374 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
376 # Restore argv[0] since we clobbered it.
377 >>> sys.argv[0] = _old_argv0
379 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
381 print ("No recipient, please add something like this to a commit\n"
382 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
384 cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
386 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
388 cmd = ['git', 'send-email', '--annotate']
390 cmd.append('--in-reply-to="%s"' % in_reply_to)
394 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
396 cmd.append(cover_fname)
404 def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
405 """If an email address is an alias, look it up and return the full name
407 TODO: Why not just use git's own alias feature?
410 lookup_name: Alias or email address to look up
411 alias: Dictionary containing aliases (None to use settings default)
412 raise_on_error: True to raise an error when an alias fails to match,
413 False to just print a message.
417 list containing a list of email addresses
420 OSError if a recursive alias reference was found
421 ValueError if an alias was not found
424 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
425 >>> alias['john'] = ['j.bloggs@napier.co.nz']
426 >>> alias['mary'] = ['m.poppins@cloud.net']
427 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
428 >>> alias['all'] = ['fred ', 'john', ' mary ']
429 >>> alias['loop'] = ['other', 'john', ' mary ']
430 >>> alias['other'] = ['loop', 'john', ' mary ']
431 >>> LookupEmail('mary', alias)
432 ['m.poppins@cloud.net']
433 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
434 ['arthur.wellesley@howe.ro.uk']
435 >>> LookupEmail('boys', alias)
436 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
437 >>> LookupEmail('all', alias)
438 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
439 >>> LookupEmail('odd', alias)
440 Traceback (most recent call last):
442 ValueError: Alias 'odd' not found
443 >>> LookupEmail('loop', alias)
444 Traceback (most recent call last):
446 OSError: Recursive email alias at 'other'
447 >>> LookupEmail('odd', alias, raise_on_error=False)
448 \033[1;31mAlias 'odd' not found\033[0m
450 >>> # In this case the loop part will effectively be ignored.
451 >>> LookupEmail('loop', alias, raise_on_error=False)
452 \033[1;31mRecursive email alias at 'other'\033[0m
453 \033[1;31mRecursive email alias at 'john'\033[0m
454 \033[1;31mRecursive email alias at 'mary'\033[0m
455 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
458 alias = settings.alias
459 lookup_name = lookup_name.strip()
460 if '@' in lookup_name: # Perhaps a real email address
463 lookup_name = lookup_name.lower()
464 col = terminal.Color()
468 msg = "Recursive email alias at '%s'" % lookup_name
472 print col.Color(col.RED, msg)
476 if not lookup_name in alias:
477 msg = "Alias '%s' not found" % lookup_name
479 raise ValueError, msg
481 print col.Color(col.RED, msg)
483 for item in alias[lookup_name]:
484 todo = LookupEmail(item, alias, raise_on_error, level + 1)
485 for new_item in todo:
486 if not new_item in out_list:
487 out_list.append(new_item)
489 #print "No match for alias '%s'" % lookup_name
493 """Return name of top-level directory for this git repo.
496 Full path to git top-level directory
498 This test makes sure that we are running tests in the right subdir
500 >>> os.path.realpath(os.path.dirname(__file__)) == \
501 os.path.join(GetTopLevel(), 'tools', 'patman')
504 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
507 """Gets the name of the git alias file.
510 Filename of git alias file, or None if none
512 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
513 raise_on_error=False)
515 fname = os.path.join(GetTopLevel(), fname.strip())
518 def GetDefaultUserName():
519 """Gets the user.name from .gitconfig file.
522 User name found in .gitconfig file, or None if none
524 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
527 def GetDefaultUserEmail():
528 """Gets the user.email from the global .gitconfig file.
531 User's email found in .gitconfig file, or None if none
533 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
537 """Set up git utils, by reading the alias files."""
538 # Check for a git alias file also
539 alias_fname = GetAliasFile()
541 settings.ReadGitAliases(alias_fname)
544 """Get the hash of the current HEAD
549 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
551 if __name__ == "__main__":