1 # Copyright (c) 2011 The Chromium OS Authors.
3 # SPDX-License-Identifier: GPL-2.0+
17 # True to use --no-decorate - we check this in Setup()
18 use_no_decorate = True
20 def LogCmd(commit_range, git_dir=None, oneline=False, reverse=False,
22 """Create a command to perform a 'git log'
25 commit_range: Range expression to use for log, None for none
26 git_dir: Path to git repositiory (None to use default)
27 oneline: True to use --oneline, else False
28 reverse: True to reverse the log (--reverse)
29 count: Number of commits to list, or None for no limit
31 List containing command and arguments to run
35 cmd += ['--git-dir', git_dir]
36 cmd += ['log', '--no-color']
38 cmd.append('--oneline')
40 cmd.append('--no-decorate')
42 cmd.append('--reverse')
44 cmd.append('-n%d' % count)
46 cmd.append(commit_range)
49 def CountCommitsToBranch():
50 """Returns number of commits between HEAD and the tracking branch.
52 This looks back to the tracking branch and works out the number of commits
56 Number of patches that exist on top of the branch
58 pipe = [LogCmd('@{upstream}..', oneline=True),
60 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
61 patch_count = int(stdout)
64 def GetUpstream(git_dir, branch):
65 """Returns the name of the upstream for a branch
68 git_dir: Git directory containing repo
69 branch: Name of branch
72 Name of upstream branch (e.g. 'upstream/master') or None if none
75 remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
76 'branch.%s.remote' % branch)
77 merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
78 'branch.%s.merge' % branch)
84 elif remote and merge:
85 leaf = merge.split('/')[-1]
86 return '%s/%s' % (remote, leaf)
88 raise ValueError, ("Cannot determine upstream branch for branch "
89 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
92 def GetRangeInBranch(git_dir, branch, include_upstream=False):
93 """Returns an expression for the commits in the given branch.
96 git_dir: Directory containing git repo
97 branch: Name of branch
99 Expression in the form 'upstream..branch' which can be used to
100 access the commits. If the branch does not exist, returns None.
102 upstream = GetUpstream(git_dir, branch)
105 return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
107 def CountCommitsInBranch(git_dir, branch, include_upstream=False):
108 """Returns the number of commits in the given branch.
111 git_dir: Directory containing git repo
112 branch: Name of branch
114 Number of patches that exist on top of the branch, or None if the
115 branch does not exist.
117 range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
120 pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True),
122 result = command.RunPipe(pipe, capture=True, oneline=True)
123 patch_count = int(result.stdout)
126 def CountCommits(commit_range):
127 """Returns the number of commits in the given range.
130 commit_range: Range of commits to count (e.g. 'HEAD..base')
132 Number of patches that exist on top of the branch
134 pipe = [LogCmd(commit_range, oneline=True),
136 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
137 patch_count = int(stdout)
140 def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
141 """Checkout the selected commit for this build
144 commit_hash: Commit hash to check out
148 pipe.extend(['--git-dir', git_dir])
150 pipe.extend(['--work-tree', work_tree])
151 pipe.append('checkout')
154 pipe.append(commit_hash)
155 result = command.RunPipe([pipe], capture=True, raise_on_error=False)
156 if result.return_code != 0:
157 raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
159 def Clone(git_dir, output_dir):
160 """Checkout the selected commit for this build
163 commit_hash: Commit hash to check out
165 pipe = ['git', 'clone', git_dir, '.']
166 result = command.RunPipe([pipe], capture=True, cwd=output_dir)
167 if result.return_code != 0:
168 raise OSError, 'git clone: %s' % result.stderr
170 def Fetch(git_dir=None, work_tree=None):
171 """Fetch from the origin repo
174 commit_hash: Commit hash to check out
178 pipe.extend(['--git-dir', git_dir])
180 pipe.extend(['--work-tree', work_tree])
182 result = command.RunPipe([pipe], capture=True)
183 if result.return_code != 0:
184 raise OSError, 'git fetch: %s' % result.stderr
186 def CreatePatches(start, count, series):
187 """Create a series of patches from the top of the current branch.
189 The patch files are written to the current directory using
193 start: Commit to start from: 0=HEAD, 1=next one, etc.
194 count: number of commits to include
196 Filename of cover letter
197 List of filenames of patch files
199 if series.get('version'):
200 version = '%s ' % series['version']
201 cmd = ['git', 'format-patch', '-M', '--signoff']
202 if series.get('cover'):
203 cmd.append('--cover-letter')
204 prefix = series.GetPatchPrefix()
206 cmd += ['--subject-prefix=%s' % prefix]
207 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
209 stdout = command.RunList(cmd)
210 files = stdout.splitlines()
212 # We have an extra file if there is a cover letter
213 if series.get('cover'):
214 return files[0], files[1:]
218 def ApplyPatch(verbose, fname):
219 """Apply a patch with git am to test it
221 TODO: Convert these to use command, with stderr option
224 fname: filename of patch file to apply
226 col = terminal.Color()
227 cmd = ['git', 'am', fname]
228 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
229 stderr=subprocess.PIPE)
230 stdout, stderr = pipe.communicate()
231 re_error = re.compile('^error: patch failed: (.+):(\d+)')
232 for line in stderr.splitlines():
235 match = re_error.match(line)
237 print checkpatch.GetWarningMsg(col, 'warning', match.group(1),
238 int(match.group(2)), 'Patch failed')
239 return pipe.returncode == 0, stdout
241 def ApplyPatches(verbose, args, start_point):
242 """Apply the patches with git am to make sure all is well
245 verbose: Print out 'git am' output verbatim
246 args: List of patch files to apply
247 start_point: Number of commits back from HEAD to start applying.
248 Normally this is len(args), but it can be larger if a start
252 col = terminal.Color()
254 # Figure out our current position
255 cmd = ['git', 'name-rev', 'HEAD', '--name-only']
256 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
257 stdout, stderr = pipe.communicate()
259 str = 'Could not find current commit name'
260 print col.Color(col.RED, str)
263 old_head = stdout.splitlines()[0]
264 if old_head == 'undefined':
265 str = "Invalid HEAD '%s'" % stdout.strip()
266 print col.Color(col.RED, str)
269 # Checkout the required start point
270 cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
271 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
272 stderr=subprocess.PIPE)
273 stdout, stderr = pipe.communicate()
275 str = 'Could not move to commit before patch series'
276 print col.Color(col.RED, str)
280 # Apply all the patches
282 ok, stdout = ApplyPatch(verbose, fname)
284 print col.Color(col.RED, 'git am returned errors for %s: will '
285 'skip this patch' % fname)
289 cmd = ['git', 'am', '--skip']
290 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
291 stdout, stderr = pipe.communicate()
292 if pipe.returncode != 0:
293 print col.Color(col.RED, 'Unable to skip patch! Aborting...')
297 # Return to our previous position
298 cmd = ['git', 'checkout', old_head]
299 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
300 stdout, stderr = pipe.communicate()
302 print col.Color(col.RED, 'Could not move back to head commit')
304 return error_count == 0
306 def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
307 """Build a list of email addresses based on an input list.
309 Takes a list of email addresses and aliases, and turns this into a list
310 of only email address, by resolving any aliases that are present.
312 If the tag is given, then each email address is prepended with this
313 tag and a space. If the tag starts with a minus sign (indicating a
314 command line parameter) then the email address is quoted.
317 in_list: List of aliases/email addresses
318 tag: Text to put before each address
319 alias: Alias dictionary
320 raise_on_error: True to raise an error when an alias fails to match,
321 False to just print a message.
324 List of email addresses
327 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
328 >>> alias['john'] = ['j.bloggs@napier.co.nz']
329 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
330 >>> alias['boys'] = ['fred', ' john']
331 >>> alias['all'] = ['fred ', 'john', ' mary ']
332 >>> BuildEmailList(['john', 'mary'], None, alias)
333 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
334 >>> BuildEmailList(['john', 'mary'], '--to', alias)
335 ['--to "j.bloggs@napier.co.nz"', \
336 '--to "Mary Poppins <m.poppins@cloud.net>"']
337 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
338 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
340 quote = '"' if tag and tag[0] == '-' else ''
343 raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
346 if not item in result:
349 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
352 def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
353 self_only=False, alias=None, in_reply_to=None):
354 """Email a patch series.
357 series: Series object containing destination info
358 cover_fname: filename of cover letter
359 args: list of filenames of patch files
360 dry_run: Just return the command that would be run
361 raise_on_error: True to raise an error when an alias fails to match,
362 False to just print a message.
363 cc_fname: Filename of Cc file for per-commit Cc
364 self_only: True to just email to yourself as a test
365 in_reply_to: If set we'll pass this to git as --in-reply-to.
366 Should be a message ID that this is in reply to.
369 Git command that was/would be run
371 # For the duration of this doctest pretend that we ran patman with ./patman
372 >>> _old_argv0 = sys.argv[0]
373 >>> sys.argv[0] = './patman'
376 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
377 >>> alias['john'] = ['j.bloggs@napier.co.nz']
378 >>> alias['mary'] = ['m.poppins@cloud.net']
379 >>> alias['boys'] = ['fred', ' john']
380 >>> alias['all'] = ['fred ', 'john', ' mary ']
381 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
382 >>> series = series.Series()
383 >>> series.to = ['fred']
384 >>> series.cc = ['mary']
385 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
387 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
388 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
389 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
391 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
392 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
393 >>> series.cc = ['all']
394 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
396 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
397 --cc-cmd cc-fname" cover p1 p2'
398 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
400 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
401 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
402 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
404 # Restore argv[0] since we clobbered it.
405 >>> sys.argv[0] = _old_argv0
407 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
409 git_config_to = command.Output('git', 'config', 'sendemail.to')
410 if not git_config_to:
411 print ("No recipient.\n"
412 "Please add something like this to a commit\n"
413 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
414 "Or do something like this\n"
415 "git config sendemail.to u-boot@lists.denx.de")
417 cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
419 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
421 cmd = ['git', 'send-email', '--annotate']
423 cmd.append('--in-reply-to="%s"' % in_reply_to)
427 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
429 cmd.append(cover_fname)
437 def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
438 """If an email address is an alias, look it up and return the full name
440 TODO: Why not just use git's own alias feature?
443 lookup_name: Alias or email address to look up
444 alias: Dictionary containing aliases (None to use settings default)
445 raise_on_error: True to raise an error when an alias fails to match,
446 False to just print a message.
450 list containing a list of email addresses
453 OSError if a recursive alias reference was found
454 ValueError if an alias was not found
457 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
458 >>> alias['john'] = ['j.bloggs@napier.co.nz']
459 >>> alias['mary'] = ['m.poppins@cloud.net']
460 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
461 >>> alias['all'] = ['fred ', 'john', ' mary ']
462 >>> alias['loop'] = ['other', 'john', ' mary ']
463 >>> alias['other'] = ['loop', 'john', ' mary ']
464 >>> LookupEmail('mary', alias)
465 ['m.poppins@cloud.net']
466 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
467 ['arthur.wellesley@howe.ro.uk']
468 >>> LookupEmail('boys', alias)
469 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
470 >>> LookupEmail('all', alias)
471 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
472 >>> LookupEmail('odd', alias)
473 Traceback (most recent call last):
475 ValueError: Alias 'odd' not found
476 >>> LookupEmail('loop', alias)
477 Traceback (most recent call last):
479 OSError: Recursive email alias at 'other'
480 >>> LookupEmail('odd', alias, raise_on_error=False)
481 Alias 'odd' not found
483 >>> # In this case the loop part will effectively be ignored.
484 >>> LookupEmail('loop', alias, raise_on_error=False)
485 Recursive email alias at 'other'
486 Recursive email alias at 'john'
487 Recursive email alias at 'mary'
488 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
491 alias = settings.alias
492 lookup_name = lookup_name.strip()
493 if '@' in lookup_name: # Perhaps a real email address
496 lookup_name = lookup_name.lower()
497 col = terminal.Color()
501 msg = "Recursive email alias at '%s'" % lookup_name
505 print col.Color(col.RED, msg)
509 if not lookup_name in alias:
510 msg = "Alias '%s' not found" % lookup_name
512 raise ValueError, msg
514 print col.Color(col.RED, msg)
516 for item in alias[lookup_name]:
517 todo = LookupEmail(item, alias, raise_on_error, level + 1)
518 for new_item in todo:
519 if not new_item in out_list:
520 out_list.append(new_item)
522 #print "No match for alias '%s'" % lookup_name
526 """Return name of top-level directory for this git repo.
529 Full path to git top-level directory
531 This test makes sure that we are running tests in the right subdir
533 >>> os.path.realpath(os.path.dirname(__file__)) == \
534 os.path.join(GetTopLevel(), 'tools', 'patman')
537 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
540 """Gets the name of the git alias file.
543 Filename of git alias file, or None if none
545 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
546 raise_on_error=False)
548 fname = os.path.join(GetTopLevel(), fname.strip())
551 def GetDefaultUserName():
552 """Gets the user.name from .gitconfig file.
555 User name found in .gitconfig file, or None if none
557 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
560 def GetDefaultUserEmail():
561 """Gets the user.email from the global .gitconfig file.
564 User's email found in .gitconfig file, or None if none
566 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
570 """Set up git utils, by reading the alias files."""
571 # Check for a git alias file also
572 alias_fname = GetAliasFile()
574 settings.ReadGitAliases(alias_fname)
575 cmd = LogCmd(None, count=0)
576 use_no_decorate = (command.RunPipe([cmd], raise_on_error=False)
580 """Get the hash of the current HEAD
585 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
587 if __name__ == "__main__":