blob: e31da1548dcf87c8a822385d818b8afc5a8878d0 [file] [log] [blame]
Simon Glass26132882012-01-14 15:12:45 +00001# 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
Simon Glass26132882012-01-14 15:12:45 +000026import subprocess
27import sys
28import terminal
29
Simon Glass11aba512012-12-15 10:42:07 +000030import settings
31
Simon Glass26132882012-01-14 15:12:45 +000032
33def CountCommitsToBranch():
34 """Returns number of commits between HEAD and the tracking branch.
35
36 This looks back to the tracking branch and works out the number of commits
37 since then.
38
39 Return:
40 Number of patches that exist on top of the branch
41 """
Andreas Bießmann69310592013-04-15 23:52:18 +000042 pipe = [['git', 'log', '--no-color', '--oneline', '--no-decorate',
43 '@{upstream}..'],
Simon Glass26132882012-01-14 15:12:45 +000044 ['wc', '-l']]
Simon Glass34e59432012-12-15 10:42:04 +000045 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
Simon Glass26132882012-01-14 15:12:45 +000046 patch_count = int(stdout)
47 return patch_count
48
Simon Glass11aba512012-12-15 10:42:07 +000049def GetUpstream(git_dir, branch):
50 """Returns the name of the upstream for a branch
51
52 Args:
53 git_dir: Git directory containing repo
54 branch: Name of branch
55
56 Returns:
57 Name of upstream branch (e.g. 'upstream/master') or None if none
58 """
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)
63 if remote == '.':
64 return merge
65 elif remote and merge:
66 leaf = merge.split('/')[-1]
67 return '%s/%s' % (remote, leaf)
68 else:
69 raise ValueError, ("Cannot determine upstream branch for branch "
70 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
71
72
73def GetRangeInBranch(git_dir, branch, include_upstream=False):
74 """Returns an expression for the commits in the given branch.
75
76 Args:
77 git_dir: Directory containing git repo
78 branch: Name of branch
79 Return:
80 Expression in the form 'upstream..branch' which can be used to
81 access the commits.
82 """
83 upstream = GetUpstream(git_dir, branch)
84 return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
85
86def CountCommitsInBranch(git_dir, branch, include_upstream=False):
87 """Returns the number of commits in the given branch.
88
89 Args:
90 git_dir: Directory containing git repo
91 branch: Name of branch
92 Return:
93 Number of patches that exist on top of the branch
94 """
95 range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
Andreas Bießmann69310592013-04-15 23:52:18 +000096 pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', '--no-decorate',
97 range_expr],
Simon Glass11aba512012-12-15 10:42:07 +000098 ['wc', '-l']]
99 result = command.RunPipe(pipe, capture=True, oneline=True)
100 patch_count = int(result.stdout)
101 return patch_count
102
103def CountCommits(commit_range):
104 """Returns the number of commits in the given range.
105
106 Args:
107 commit_range: Range of commits to count (e.g. 'HEAD..base')
108 Return:
109 Number of patches that exist on top of the branch
110 """
Andreas Bießmann69310592013-04-15 23:52:18 +0000111 pipe = [['git', 'log', '--oneline', '--no-decorate', commit_range],
Simon Glass11aba512012-12-15 10:42:07 +0000112 ['wc', '-l']]
113 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
114 patch_count = int(stdout)
115 return patch_count
116
117def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
118 """Checkout the selected commit for this build
119
120 Args:
121 commit_hash: Commit hash to check out
122 """
123 pipe = ['git']
124 if git_dir:
125 pipe.extend(['--git-dir', git_dir])
126 if work_tree:
127 pipe.extend(['--work-tree', work_tree])
128 pipe.append('checkout')
129 if force:
130 pipe.append('-f')
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)
135
136def Clone(git_dir, output_dir):
137 """Checkout the selected commit for this build
138
139 Args:
140 commit_hash: Commit hash to check out
141 """
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
146
147def Fetch(git_dir=None, work_tree=None):
148 """Fetch from the origin repo
149
150 Args:
151 commit_hash: Commit hash to check out
152 """
153 pipe = ['git']
154 if git_dir:
155 pipe.extend(['--git-dir', git_dir])
156 if work_tree:
157 pipe.extend(['--work-tree', work_tree])
158 pipe.append('fetch')
159 result = command.RunPipe([pipe], capture=True)
160 if result.return_code != 0:
161 raise OSError, 'git fetch: %s' % result.stderr
162
Simon Glass26132882012-01-14 15:12:45 +0000163def CreatePatches(start, count, series):
164 """Create a series of patches from the top of the current branch.
165
166 The patch files are written to the current directory using
167 git format-patch.
168
169 Args:
170 start: Commit to start from: 0=HEAD, 1=next one, etc.
171 count: number of commits to include
172 Return:
173 Filename of cover letter
174 List of filenames of patch files
175 """
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()
182 if prefix:
183 cmd += ['--subject-prefix=%s' % prefix]
184 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
185
186 stdout = command.RunList(cmd)
187 files = stdout.splitlines()
188
189 # We have an extra file if there is a cover letter
190 if series.get('cover'):
191 return files[0], files[1:]
192 else:
193 return None, files
194
195def ApplyPatch(verbose, fname):
196 """Apply a patch with git am to test it
197
198 TODO: Convert these to use command, with stderr option
199
200 Args:
201 fname: filename of patch file to apply
202 """
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():
209 if verbose:
210 print line
211 match = re_error.match(line)
212 if match:
213 print GetWarningMsg('warning', match.group(1), int(match.group(2)),
214 'Patch failed')
215 return pipe.returncode == 0, stdout
216
217def ApplyPatches(verbose, args, start_point):
218 """Apply the patches with git am to make sure all is well
219
220 Args:
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
225 offset was given.
226 """
227 error_count = 0
228 col = terminal.Color()
229
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()
234 if pipe.returncode:
235 str = 'Could not find current commit name'
236 print col.Color(col.RED, str)
237 print stdout
238 return False
239 old_head = stdout.splitlines()[0]
240
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()
246 if pipe.returncode:
247 str = 'Could not move to commit before patch series'
248 print col.Color(col.RED, str)
249 print stdout, stderr
250 return False
251
252 # Apply all the patches
253 for fname in args:
254 ok, stdout = ApplyPatch(verbose, fname)
255 if not ok:
256 print col.Color(col.RED, 'git am returned errors for %s: will '
257 'skip this patch' % fname)
258 if verbose:
259 print stdout
260 error_count += 1
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...')
266 print stdout
267 break
268
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()
273 if pipe.returncode:
274 print col.Color(col.RED, 'Could not move back to head commit')
275 print stdout, stderr
276 return error_count == 0
277
Simon Glass12ea5f42013-03-26 13:09:42 +0000278def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
Simon Glass26132882012-01-14 15:12:45 +0000279 """Build a list of email addresses based on an input list.
280
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.
283
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.
287
288 Args:
289 in_list: List of aliases/email addresses
290 tag: Text to put before each address
Simon Glass12ea5f42013-03-26 13:09:42 +0000291 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.
Simon Glass26132882012-01-14 15:12:45 +0000294
295 Returns:
296 List of email addresses
297
298 >>> alias = {}
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>']
311 """
312 quote = '"' if tag and tag[0] == '-' else ''
313 raw = []
314 for item in in_list:
Simon Glass12ea5f42013-03-26 13:09:42 +0000315 raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000316 result = []
317 for item in raw:
318 if not item in result:
319 result.append(item)
320 if tag:
321 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
322 return result
323
Simon Glass12ea5f42013-03-26 13:09:42 +0000324def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
Doug Anderson06f27ac2013-03-17 10:31:04 +0000325 self_only=False, alias=None, in_reply_to=None):
Simon Glass26132882012-01-14 15:12:45 +0000326 """Email a patch series.
327
328 Args:
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
Simon Glass12ea5f42013-03-26 13:09:42 +0000333 raise_on_error: True to raise an error when an alias fails to match,
334 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000335 cc_fname: Filename of Cc file for per-commit Cc
336 self_only: True to just email to yourself as a test
Doug Anderson06f27ac2013-03-17 10:31:04 +0000337 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.
Simon Glass26132882012-01-14 15:12:45 +0000339
340 Returns:
341 Git command that was/would be run
342
Doug Anderson51d73212012-11-26 15:21:40 +0000343 # For the duration of this doctest pretend that we ran patman with ./patman
344 >>> _old_argv0 = sys.argv[0]
345 >>> sys.argv[0] = './patman'
346
Simon Glass26132882012-01-14 15:12:45 +0000347 >>> alias = {}
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']
Simon Glass12ea5f42013-03-26 13:09:42 +0000357 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
358 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000359 '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'
Simon Glass12ea5f42013-03-26 13:09:42 +0000361 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
362 alias)
Simon Glass26132882012-01-14 15:12:45 +0000363 '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']
Simon Glass12ea5f42013-03-26 13:09:42 +0000366 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
367 True, alias)
Simon Glass26132882012-01-14 15:12:45 +0000368 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
369--cc-cmd cc-fname" cover p1 p2'
Simon Glass12ea5f42013-03-26 13:09:42 +0000370 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
371 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000372 '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'
Doug Anderson51d73212012-11-26 15:21:40 +0000375
376 # Restore argv[0] since we clobbered it.
377 >>> sys.argv[0] = _old_argv0
Simon Glass26132882012-01-14 15:12:45 +0000378 """
Simon Glass12ea5f42013-03-26 13:09:42 +0000379 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000380 if not to:
381 print ("No recipient, please add something like this to a commit\n"
382 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
383 return
Simon Glass12ea5f42013-03-26 13:09:42 +0000384 cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000385 if self_only:
Simon Glass12ea5f42013-03-26 13:09:42 +0000386 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000387 cc = []
388 cmd = ['git', 'send-email', '--annotate']
Doug Anderson06f27ac2013-03-17 10:31:04 +0000389 if in_reply_to:
390 cmd.append('--in-reply-to="%s"' % in_reply_to)
391
Simon Glass26132882012-01-14 15:12:45 +0000392 cmd += to
393 cmd += cc
394 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
395 if cover_fname:
396 cmd.append(cover_fname)
397 cmd += args
398 str = ' '.join(cmd)
399 if not dry_run:
400 os.system(str)
401 return str
402
403
Simon Glass12ea5f42013-03-26 13:09:42 +0000404def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
Simon Glass26132882012-01-14 15:12:45 +0000405 """If an email address is an alias, look it up and return the full name
406
407 TODO: Why not just use git's own alias feature?
408
409 Args:
410 lookup_name: Alias or email address to look up
Simon Glass12ea5f42013-03-26 13:09:42 +0000411 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.
Simon Glass26132882012-01-14 15:12:45 +0000414
415 Returns:
416 tuple:
417 list containing a list of email addresses
418
419 Raises:
420 OSError if a recursive alias reference was found
421 ValueError if an alias was not found
422
423 >>> alias = {}
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):
441 ...
442 ValueError: Alias 'odd' not found
443 >>> LookupEmail('loop', alias)
444 Traceback (most recent call last):
445 ...
446 OSError: Recursive email alias at 'other'
Simon Glass12ea5f42013-03-26 13:09:42 +0000447 >>> LookupEmail('odd', alias, raise_on_error=False)
448 \033[1;31mAlias 'odd' not found\033[0m
449 []
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']
Simon Glass26132882012-01-14 15:12:45 +0000456 """
457 if not alias:
458 alias = settings.alias
459 lookup_name = lookup_name.strip()
460 if '@' in lookup_name: # Perhaps a real email address
461 return [lookup_name]
462
463 lookup_name = lookup_name.lower()
Simon Glass12ea5f42013-03-26 13:09:42 +0000464 col = terminal.Color()
Simon Glass26132882012-01-14 15:12:45 +0000465
Simon Glass12ea5f42013-03-26 13:09:42 +0000466 out_list = []
Simon Glass26132882012-01-14 15:12:45 +0000467 if level > 10:
Simon Glass12ea5f42013-03-26 13:09:42 +0000468 msg = "Recursive email alias at '%s'" % lookup_name
469 if raise_on_error:
470 raise OSError, msg
471 else:
472 print col.Color(col.RED, msg)
473 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000474
Simon Glass26132882012-01-14 15:12:45 +0000475 if lookup_name:
476 if not lookup_name in alias:
Simon Glass12ea5f42013-03-26 13:09:42 +0000477 msg = "Alias '%s' not found" % lookup_name
478 if raise_on_error:
479 raise ValueError, msg
480 else:
481 print col.Color(col.RED, msg)
482 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000483 for item in alias[lookup_name]:
Simon Glass12ea5f42013-03-26 13:09:42 +0000484 todo = LookupEmail(item, alias, raise_on_error, level + 1)
Simon Glass26132882012-01-14 15:12:45 +0000485 for new_item in todo:
486 if not new_item in out_list:
487 out_list.append(new_item)
488
489 #print "No match for alias '%s'" % lookup_name
490 return out_list
491
492def GetTopLevel():
493 """Return name of top-level directory for this git repo.
494
495 Returns:
496 Full path to git top-level directory
497
498 This test makes sure that we are running tests in the right subdir
499
Doug Anderson51d73212012-11-26 15:21:40 +0000500 >>> os.path.realpath(os.path.dirname(__file__)) == \
501 os.path.join(GetTopLevel(), 'tools', 'patman')
Simon Glass26132882012-01-14 15:12:45 +0000502 True
503 """
504 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
505
506def GetAliasFile():
507 """Gets the name of the git alias file.
508
509 Returns:
510 Filename of git alias file, or None if none
511 """
Simon Glass519fad22012-12-15 10:42:05 +0000512 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
513 raise_on_error=False)
Simon Glass26132882012-01-14 15:12:45 +0000514 if fname:
515 fname = os.path.join(GetTopLevel(), fname.strip())
516 return fname
517
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000518def GetDefaultUserName():
519 """Gets the user.name from .gitconfig file.
520
521 Returns:
522 User name found in .gitconfig file, or None if none
523 """
524 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
525 return uname
526
527def GetDefaultUserEmail():
528 """Gets the user.email from the global .gitconfig file.
529
530 Returns:
531 User's email found in .gitconfig file, or None if none
532 """
533 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
534 return uemail
535
Simon Glass26132882012-01-14 15:12:45 +0000536def Setup():
537 """Set up git utils, by reading the alias files."""
Simon Glass26132882012-01-14 15:12:45 +0000538 # Check for a git alias file also
539 alias_fname = GetAliasFile()
540 if alias_fname:
541 settings.ReadGitAliases(alias_fname)
542
Simon Glass11aba512012-12-15 10:42:07 +0000543def GetHead():
544 """Get the hash of the current HEAD
545
546 Returns:
547 Hash of HEAD
548 """
549 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
550
Simon Glass26132882012-01-14 15:12:45 +0000551if __name__ == "__main__":
552 import doctest
553
554 doctest.testmod()