1 # Copyright (c) 2011 The Chromium OS Authors.
3 # SPDX-License-Identifier: GPL-2.0+
17 def CountCommitsToBranch():
18 """Returns number of commits between HEAD and the tracking branch.
20 This looks back to the tracking branch and works out the number of commits
24 Number of patches that exist on top of the branch
26 pipe = [['git', 'log', '--no-color', '--oneline', '--no-decorate',
29 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
30 patch_count = int(stdout)
33 def GetUpstream(git_dir, branch):
34 """Returns the name of the upstream for a branch
37 git_dir: Git directory containing repo
38 branch: Name of branch
41 Name of upstream branch (e.g. 'upstream/master') or None if none
44 remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
45 'branch.%s.remote' % branch)
46 merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
47 'branch.%s.merge' % branch)
53 elif remote and merge:
54 leaf = merge.split('/')[-1]
55 return '%s/%s' % (remote, leaf)
57 raise ValueError, ("Cannot determine upstream branch for branch "
58 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
61 def GetRangeInBranch(git_dir, branch, include_upstream=False):
62 """Returns an expression for the commits in the given branch.
65 git_dir: Directory containing git repo
66 branch: Name of branch
68 Expression in the form 'upstream..branch' which can be used to
69 access the commits. If the branch does not exist, returns None.
71 upstream = GetUpstream(git_dir, branch)
74 return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
76 def CountCommitsInBranch(git_dir, branch, include_upstream=False):
77 """Returns the number of commits in the given branch.
80 git_dir: Directory containing git repo
81 branch: Name of branch
83 Number of patches that exist on top of the branch, or None if the
84 branch does not exist.
86 range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
89 pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', '--no-decorate',
92 result = command.RunPipe(pipe, capture=True, oneline=True)
93 patch_count = int(result.stdout)
96 def CountCommits(commit_range):
97 """Returns the number of commits in the given range.
100 commit_range: Range of commits to count (e.g. 'HEAD..base')
102 Number of patches that exist on top of the branch
104 pipe = [['git', 'log', '--oneline', '--no-decorate', commit_range],
106 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
107 patch_count = int(stdout)
110 def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
111 """Checkout the selected commit for this build
114 commit_hash: Commit hash to check out
118 pipe.extend(['--git-dir', git_dir])
120 pipe.extend(['--work-tree', work_tree])
121 pipe.append('checkout')
124 pipe.append(commit_hash)
125 result = command.RunPipe([pipe], capture=True, raise_on_error=False)
126 if result.return_code != 0:
127 raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
129 def Clone(git_dir, output_dir):
130 """Checkout the selected commit for this build
133 commit_hash: Commit hash to check out
135 pipe = ['git', 'clone', git_dir, '.']
136 result = command.RunPipe([pipe], capture=True, cwd=output_dir)
137 if result.return_code != 0:
138 raise OSError, 'git clone: %s' % result.stderr
140 def Fetch(git_dir=None, work_tree=None):
141 """Fetch from the origin repo
144 commit_hash: Commit hash to check out
148 pipe.extend(['--git-dir', git_dir])
150 pipe.extend(['--work-tree', work_tree])
152 result = command.RunPipe([pipe], capture=True)
153 if result.return_code != 0:
154 raise OSError, 'git fetch: %s' % result.stderr
156 def CreatePatches(start, count, series):
157 """Create a series of patches from the top of the current branch.
159 The patch files are written to the current directory using
163 start: Commit to start from: 0=HEAD, 1=next one, etc.
164 count: number of commits to include
166 Filename of cover letter
167 List of filenames of patch files
169 if series.get('version'):
170 version = '%s ' % series['version']
171 cmd = ['git', 'format-patch', '-M', '--signoff']
172 if series.get('cover'):
173 cmd.append('--cover-letter')
174 prefix = series.GetPatchPrefix()
176 cmd += ['--subject-prefix=%s' % prefix]
177 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
179 stdout = command.RunList(cmd)
180 files = stdout.splitlines()
182 # We have an extra file if there is a cover letter
183 if series.get('cover'):
184 return files[0], files[1:]
188 def ApplyPatch(verbose, fname):
189 """Apply a patch with git am to test it
191 TODO: Convert these to use command, with stderr option
194 fname: filename of patch file to apply
196 cmd = ['git', 'am', fname]
197 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
198 stderr=subprocess.PIPE)
199 stdout, stderr = pipe.communicate()
200 re_error = re.compile('^error: patch failed: (.+):(\d+)')
201 for line in stderr.splitlines():
204 match = re_error.match(line)
206 print GetWarningMsg('warning', match.group(1), int(match.group(2)),
208 return pipe.returncode == 0, stdout
210 def ApplyPatches(verbose, args, start_point):
211 """Apply the patches with git am to make sure all is well
214 verbose: Print out 'git am' output verbatim
215 args: List of patch files to apply
216 start_point: Number of commits back from HEAD to start applying.
217 Normally this is len(args), but it can be larger if a start
221 col = terminal.Color()
223 # Figure out our current position
224 cmd = ['git', 'name-rev', 'HEAD', '--name-only']
225 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
226 stdout, stderr = pipe.communicate()
228 str = 'Could not find current commit name'
229 print col.Color(col.RED, str)
232 old_head = stdout.splitlines()[0]
234 # Checkout the required start point
235 cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
236 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
237 stderr=subprocess.PIPE)
238 stdout, stderr = pipe.communicate()
240 str = 'Could not move to commit before patch series'
241 print col.Color(col.RED, str)
245 # Apply all the patches
247 ok, stdout = ApplyPatch(verbose, fname)
249 print col.Color(col.RED, 'git am returned errors for %s: will '
250 'skip this patch' % fname)
254 cmd = ['git', 'am', '--skip']
255 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
256 stdout, stderr = pipe.communicate()
257 if pipe.returncode != 0:
258 print col.Color(col.RED, 'Unable to skip patch! Aborting...')
262 # Return to our previous position
263 cmd = ['git', 'checkout', old_head]
264 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
265 stdout, stderr = pipe.communicate()
267 print col.Color(col.RED, 'Could not move back to head commit')
269 return error_count == 0
271 def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
272 """Build a list of email addresses based on an input list.
274 Takes a list of email addresses and aliases, and turns this into a list
275 of only email address, by resolving any aliases that are present.
277 If the tag is given, then each email address is prepended with this
278 tag and a space. If the tag starts with a minus sign (indicating a
279 command line parameter) then the email address is quoted.
282 in_list: List of aliases/email addresses
283 tag: Text to put before each address
284 alias: Alias dictionary
285 raise_on_error: True to raise an error when an alias fails to match,
286 False to just print a message.
289 List of email addresses
292 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
293 >>> alias['john'] = ['j.bloggs@napier.co.nz']
294 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
295 >>> alias['boys'] = ['fred', ' john']
296 >>> alias['all'] = ['fred ', 'john', ' mary ']
297 >>> BuildEmailList(['john', 'mary'], None, alias)
298 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
299 >>> BuildEmailList(['john', 'mary'], '--to', alias)
300 ['--to "j.bloggs@napier.co.nz"', \
301 '--to "Mary Poppins <m.poppins@cloud.net>"']
302 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
303 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
305 quote = '"' if tag and tag[0] == '-' else ''
308 raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
311 if not item in result:
314 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
317 def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
318 self_only=False, alias=None, in_reply_to=None):
319 """Email a patch series.
322 series: Series object containing destination info
323 cover_fname: filename of cover letter
324 args: list of filenames of patch files
325 dry_run: Just return the command that would be run
326 raise_on_error: True to raise an error when an alias fails to match,
327 False to just print a message.
328 cc_fname: Filename of Cc file for per-commit Cc
329 self_only: True to just email to yourself as a test
330 in_reply_to: If set we'll pass this to git as --in-reply-to.
331 Should be a message ID that this is in reply to.
334 Git command that was/would be run
336 # For the duration of this doctest pretend that we ran patman with ./patman
337 >>> _old_argv0 = sys.argv[0]
338 >>> sys.argv[0] = './patman'
341 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
342 >>> alias['john'] = ['j.bloggs@napier.co.nz']
343 >>> alias['mary'] = ['m.poppins@cloud.net']
344 >>> alias['boys'] = ['fred', ' john']
345 >>> alias['all'] = ['fred ', 'john', ' mary ']
346 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
347 >>> series = series.Series()
348 >>> series.to = ['fred']
349 >>> series.cc = ['mary']
350 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
352 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
353 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
354 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
356 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
357 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
358 >>> series.cc = ['all']
359 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
361 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
362 --cc-cmd cc-fname" cover p1 p2'
363 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
365 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
366 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
367 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
369 # Restore argv[0] since we clobbered it.
370 >>> sys.argv[0] = _old_argv0
372 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
374 print ("No recipient, please add something like this to a commit\n"
375 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
377 cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
379 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
381 cmd = ['git', 'send-email', '--annotate']
383 cmd.append('--in-reply-to="%s"' % in_reply_to)
387 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
389 cmd.append(cover_fname)
397 def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
398 """If an email address is an alias, look it up and return the full name
400 TODO: Why not just use git's own alias feature?
403 lookup_name: Alias or email address to look up
404 alias: Dictionary containing aliases (None to use settings default)
405 raise_on_error: True to raise an error when an alias fails to match,
406 False to just print a message.
410 list containing a list of email addresses
413 OSError if a recursive alias reference was found
414 ValueError if an alias was not found
417 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
418 >>> alias['john'] = ['j.bloggs@napier.co.nz']
419 >>> alias['mary'] = ['m.poppins@cloud.net']
420 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
421 >>> alias['all'] = ['fred ', 'john', ' mary ']
422 >>> alias['loop'] = ['other', 'john', ' mary ']
423 >>> alias['other'] = ['loop', 'john', ' mary ']
424 >>> LookupEmail('mary', alias)
425 ['m.poppins@cloud.net']
426 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
427 ['arthur.wellesley@howe.ro.uk']
428 >>> LookupEmail('boys', alias)
429 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
430 >>> LookupEmail('all', alias)
431 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
432 >>> LookupEmail('odd', alias)
433 Traceback (most recent call last):
435 ValueError: Alias 'odd' not found
436 >>> LookupEmail('loop', alias)
437 Traceback (most recent call last):
439 OSError: Recursive email alias at 'other'
440 >>> LookupEmail('odd', alias, raise_on_error=False)
441 \033[1;31mAlias 'odd' not found\033[0m
443 >>> # In this case the loop part will effectively be ignored.
444 >>> LookupEmail('loop', alias, raise_on_error=False)
445 \033[1;31mRecursive email alias at 'other'\033[0m
446 \033[1;31mRecursive email alias at 'john'\033[0m
447 \033[1;31mRecursive email alias at 'mary'\033[0m
448 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
451 alias = settings.alias
452 lookup_name = lookup_name.strip()
453 if '@' in lookup_name: # Perhaps a real email address
456 lookup_name = lookup_name.lower()
457 col = terminal.Color()
461 msg = "Recursive email alias at '%s'" % lookup_name
465 print col.Color(col.RED, msg)
469 if not lookup_name in alias:
470 msg = "Alias '%s' not found" % lookup_name
472 raise ValueError, msg
474 print col.Color(col.RED, msg)
476 for item in alias[lookup_name]:
477 todo = LookupEmail(item, alias, raise_on_error, level + 1)
478 for new_item in todo:
479 if not new_item in out_list:
480 out_list.append(new_item)
482 #print "No match for alias '%s'" % lookup_name
486 """Return name of top-level directory for this git repo.
489 Full path to git top-level directory
491 This test makes sure that we are running tests in the right subdir
493 >>> os.path.realpath(os.path.dirname(__file__)) == \
494 os.path.join(GetTopLevel(), 'tools', 'patman')
497 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
500 """Gets the name of the git alias file.
503 Filename of git alias file, or None if none
505 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
506 raise_on_error=False)
508 fname = os.path.join(GetTopLevel(), fname.strip())
511 def GetDefaultUserName():
512 """Gets the user.name from .gitconfig file.
515 User name found in .gitconfig file, or None if none
517 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
520 def GetDefaultUserEmail():
521 """Gets the user.email from the global .gitconfig file.
524 User's email found in .gitconfig file, or None if none
526 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
530 """Set up git utils, by reading the alias files."""
531 # Check for a git alias file also
532 alias_fname = GetAliasFile()
534 settings.ReadGitAliases(alias_fname)
537 """Get the hash of the current HEAD
542 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
544 if __name__ == "__main__":