blob: 99fac795c20d5fd0b47a7fc96cbb72370b200cec [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 """
Albert ARIBAUD313d4482012-10-15 09:55:50 +000042 pipe = [['git', 'log', '--no-color', '--oneline', '@{upstream}..'],
Simon Glass26132882012-01-14 15:12:45 +000043 ['wc', '-l']]
Simon Glass34e59432012-12-15 10:42:04 +000044 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
Simon Glass26132882012-01-14 15:12:45 +000045 patch_count = int(stdout)
46 return patch_count
47
Simon Glass11aba512012-12-15 10:42:07 +000048def GetUpstream(git_dir, branch):
49 """Returns the name of the upstream for a branch
50
51 Args:
52 git_dir: Git directory containing repo
53 branch: Name of branch
54
55 Returns:
56 Name of upstream branch (e.g. 'upstream/master') or None if none
57 """
58 remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
59 'branch.%s.remote' % branch)
60 merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
61 'branch.%s.merge' % branch)
62 if remote == '.':
63 return merge
64 elif remote and merge:
65 leaf = merge.split('/')[-1]
66 return '%s/%s' % (remote, leaf)
67 else:
68 raise ValueError, ("Cannot determine upstream branch for branch "
69 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
70
71
72def GetRangeInBranch(git_dir, branch, include_upstream=False):
73 """Returns an expression for the commits in the given branch.
74
75 Args:
76 git_dir: Directory containing git repo
77 branch: Name of branch
78 Return:
79 Expression in the form 'upstream..branch' which can be used to
80 access the commits.
81 """
82 upstream = GetUpstream(git_dir, branch)
83 return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
84
85def CountCommitsInBranch(git_dir, branch, include_upstream=False):
86 """Returns the number of commits in the given branch.
87
88 Args:
89 git_dir: Directory containing git repo
90 branch: Name of branch
91 Return:
92 Number of patches that exist on top of the branch
93 """
94 range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
95 pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', range_expr],
96 ['wc', '-l']]
97 result = command.RunPipe(pipe, capture=True, oneline=True)
98 patch_count = int(result.stdout)
99 return patch_count
100
101def CountCommits(commit_range):
102 """Returns the number of commits in the given range.
103
104 Args:
105 commit_range: Range of commits to count (e.g. 'HEAD..base')
106 Return:
107 Number of patches that exist on top of the branch
108 """
109 pipe = [['git', 'log', '--oneline', commit_range],
110 ['wc', '-l']]
111 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
112 patch_count = int(stdout)
113 return patch_count
114
115def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
116 """Checkout the selected commit for this build
117
118 Args:
119 commit_hash: Commit hash to check out
120 """
121 pipe = ['git']
122 if git_dir:
123 pipe.extend(['--git-dir', git_dir])
124 if work_tree:
125 pipe.extend(['--work-tree', work_tree])
126 pipe.append('checkout')
127 if force:
128 pipe.append('-f')
129 pipe.append(commit_hash)
130 result = command.RunPipe([pipe], capture=True, raise_on_error=False)
131 if result.return_code != 0:
132 raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
133
134def Clone(git_dir, output_dir):
135 """Checkout the selected commit for this build
136
137 Args:
138 commit_hash: Commit hash to check out
139 """
140 pipe = ['git', 'clone', git_dir, '.']
141 result = command.RunPipe([pipe], capture=True, cwd=output_dir)
142 if result.return_code != 0:
143 raise OSError, 'git clone: %s' % result.stderr
144
145def Fetch(git_dir=None, work_tree=None):
146 """Fetch from the origin repo
147
148 Args:
149 commit_hash: Commit hash to check out
150 """
151 pipe = ['git']
152 if git_dir:
153 pipe.extend(['--git-dir', git_dir])
154 if work_tree:
155 pipe.extend(['--work-tree', work_tree])
156 pipe.append('fetch')
157 result = command.RunPipe([pipe], capture=True)
158 if result.return_code != 0:
159 raise OSError, 'git fetch: %s' % result.stderr
160
Simon Glass26132882012-01-14 15:12:45 +0000161def CreatePatches(start, count, series):
162 """Create a series of patches from the top of the current branch.
163
164 The patch files are written to the current directory using
165 git format-patch.
166
167 Args:
168 start: Commit to start from: 0=HEAD, 1=next one, etc.
169 count: number of commits to include
170 Return:
171 Filename of cover letter
172 List of filenames of patch files
173 """
174 if series.get('version'):
175 version = '%s ' % series['version']
176 cmd = ['git', 'format-patch', '-M', '--signoff']
177 if series.get('cover'):
178 cmd.append('--cover-letter')
179 prefix = series.GetPatchPrefix()
180 if prefix:
181 cmd += ['--subject-prefix=%s' % prefix]
182 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
183
184 stdout = command.RunList(cmd)
185 files = stdout.splitlines()
186
187 # We have an extra file if there is a cover letter
188 if series.get('cover'):
189 return files[0], files[1:]
190 else:
191 return None, files
192
193def ApplyPatch(verbose, fname):
194 """Apply a patch with git am to test it
195
196 TODO: Convert these to use command, with stderr option
197
198 Args:
199 fname: filename of patch file to apply
200 """
201 cmd = ['git', 'am', fname]
202 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
203 stderr=subprocess.PIPE)
204 stdout, stderr = pipe.communicate()
205 re_error = re.compile('^error: patch failed: (.+):(\d+)')
206 for line in stderr.splitlines():
207 if verbose:
208 print line
209 match = re_error.match(line)
210 if match:
211 print GetWarningMsg('warning', match.group(1), int(match.group(2)),
212 'Patch failed')
213 return pipe.returncode == 0, stdout
214
215def ApplyPatches(verbose, args, start_point):
216 """Apply the patches with git am to make sure all is well
217
218 Args:
219 verbose: Print out 'git am' output verbatim
220 args: List of patch files to apply
221 start_point: Number of commits back from HEAD to start applying.
222 Normally this is len(args), but it can be larger if a start
223 offset was given.
224 """
225 error_count = 0
226 col = terminal.Color()
227
228 # Figure out our current position
229 cmd = ['git', 'name-rev', 'HEAD', '--name-only']
230 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
231 stdout, stderr = pipe.communicate()
232 if pipe.returncode:
233 str = 'Could not find current commit name'
234 print col.Color(col.RED, str)
235 print stdout
236 return False
237 old_head = stdout.splitlines()[0]
238
239 # Checkout the required start point
240 cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
241 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
242 stderr=subprocess.PIPE)
243 stdout, stderr = pipe.communicate()
244 if pipe.returncode:
245 str = 'Could not move to commit before patch series'
246 print col.Color(col.RED, str)
247 print stdout, stderr
248 return False
249
250 # Apply all the patches
251 for fname in args:
252 ok, stdout = ApplyPatch(verbose, fname)
253 if not ok:
254 print col.Color(col.RED, 'git am returned errors for %s: will '
255 'skip this patch' % fname)
256 if verbose:
257 print stdout
258 error_count += 1
259 cmd = ['git', 'am', '--skip']
260 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
261 stdout, stderr = pipe.communicate()
262 if pipe.returncode != 0:
263 print col.Color(col.RED, 'Unable to skip patch! Aborting...')
264 print stdout
265 break
266
267 # Return to our previous position
268 cmd = ['git', 'checkout', old_head]
269 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
270 stdout, stderr = pipe.communicate()
271 if pipe.returncode:
272 print col.Color(col.RED, 'Could not move back to head commit')
273 print stdout, stderr
274 return error_count == 0
275
276def BuildEmailList(in_list, tag=None, alias=None):
277 """Build a list of email addresses based on an input list.
278
279 Takes a list of email addresses and aliases, and turns this into a list
280 of only email address, by resolving any aliases that are present.
281
282 If the tag is given, then each email address is prepended with this
283 tag and a space. If the tag starts with a minus sign (indicating a
284 command line parameter) then the email address is quoted.
285
286 Args:
287 in_list: List of aliases/email addresses
288 tag: Text to put before each address
289
290 Returns:
291 List of email addresses
292
293 >>> alias = {}
294 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
295 >>> alias['john'] = ['j.bloggs@napier.co.nz']
296 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
297 >>> alias['boys'] = ['fred', ' john']
298 >>> alias['all'] = ['fred ', 'john', ' mary ']
299 >>> BuildEmailList(['john', 'mary'], None, alias)
300 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
301 >>> BuildEmailList(['john', 'mary'], '--to', alias)
302 ['--to "j.bloggs@napier.co.nz"', \
303'--to "Mary Poppins <m.poppins@cloud.net>"']
304 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
305 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
306 """
307 quote = '"' if tag and tag[0] == '-' else ''
308 raw = []
309 for item in in_list:
310 raw += LookupEmail(item, alias)
311 result = []
312 for item in raw:
313 if not item in result:
314 result.append(item)
315 if tag:
316 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
317 return result
318
319def EmailPatches(series, cover_fname, args, dry_run, cc_fname,
320 self_only=False, alias=None):
321 """Email a patch series.
322
323 Args:
324 series: Series object containing destination info
325 cover_fname: filename of cover letter
326 args: list of filenames of patch files
327 dry_run: Just return the command that would be run
328 cc_fname: Filename of Cc file for per-commit Cc
329 self_only: True to just email to yourself as a test
330
331 Returns:
332 Git command that was/would be run
333
Doug Anderson51d73212012-11-26 15:21:40 +0000334 # For the duration of this doctest pretend that we ran patman with ./patman
335 >>> _old_argv0 = sys.argv[0]
336 >>> sys.argv[0] = './patman'
337
Simon Glass26132882012-01-14 15:12:45 +0000338 >>> alias = {}
339 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
340 >>> alias['john'] = ['j.bloggs@napier.co.nz']
341 >>> alias['mary'] = ['m.poppins@cloud.net']
342 >>> alias['boys'] = ['fred', ' john']
343 >>> alias['all'] = ['fred ', 'john', ' mary ']
344 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
345 >>> series = series.Series()
346 >>> series.to = ['fred']
347 >>> series.cc = ['mary']
348 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
349 alias)
350 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
351"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
352 >>> EmailPatches(series, None, ['p1'], True, 'cc-fname', False, alias)
353 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
354"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
355 >>> series.cc = ['all']
356 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', True, \
357 alias)
358 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
359--cc-cmd cc-fname" cover p1 p2'
360 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
361 alias)
362 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
363"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
364"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
Doug Anderson51d73212012-11-26 15:21:40 +0000365
366 # Restore argv[0] since we clobbered it.
367 >>> sys.argv[0] = _old_argv0
Simon Glass26132882012-01-14 15:12:45 +0000368 """
369 to = BuildEmailList(series.get('to'), '--to', alias)
370 if not to:
371 print ("No recipient, please add something like this to a commit\n"
372 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
373 return
374 cc = BuildEmailList(series.get('cc'), '--cc', alias)
375 if self_only:
376 to = BuildEmailList([os.getenv('USER')], '--to', alias)
377 cc = []
378 cmd = ['git', 'send-email', '--annotate']
379 cmd += to
380 cmd += cc
381 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
382 if cover_fname:
383 cmd.append(cover_fname)
384 cmd += args
385 str = ' '.join(cmd)
386 if not dry_run:
387 os.system(str)
388 return str
389
390
391def LookupEmail(lookup_name, alias=None, level=0):
392 """If an email address is an alias, look it up and return the full name
393
394 TODO: Why not just use git's own alias feature?
395
396 Args:
397 lookup_name: Alias or email address to look up
398
399 Returns:
400 tuple:
401 list containing a list of email addresses
402
403 Raises:
404 OSError if a recursive alias reference was found
405 ValueError if an alias was not found
406
407 >>> alias = {}
408 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
409 >>> alias['john'] = ['j.bloggs@napier.co.nz']
410 >>> alias['mary'] = ['m.poppins@cloud.net']
411 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
412 >>> alias['all'] = ['fred ', 'john', ' mary ']
413 >>> alias['loop'] = ['other', 'john', ' mary ']
414 >>> alias['other'] = ['loop', 'john', ' mary ']
415 >>> LookupEmail('mary', alias)
416 ['m.poppins@cloud.net']
417 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
418 ['arthur.wellesley@howe.ro.uk']
419 >>> LookupEmail('boys', alias)
420 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
421 >>> LookupEmail('all', alias)
422 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
423 >>> LookupEmail('odd', alias)
424 Traceback (most recent call last):
425 ...
426 ValueError: Alias 'odd' not found
427 >>> LookupEmail('loop', alias)
428 Traceback (most recent call last):
429 ...
430 OSError: Recursive email alias at 'other'
431 """
432 if not alias:
433 alias = settings.alias
434 lookup_name = lookup_name.strip()
435 if '@' in lookup_name: # Perhaps a real email address
436 return [lookup_name]
437
438 lookup_name = lookup_name.lower()
439
440 if level > 10:
441 raise OSError, "Recursive email alias at '%s'" % lookup_name
442
443 out_list = []
444 if lookup_name:
445 if not lookup_name in alias:
446 raise ValueError, "Alias '%s' not found" % lookup_name
447 for item in alias[lookup_name]:
448 todo = LookupEmail(item, alias, level + 1)
449 for new_item in todo:
450 if not new_item in out_list:
451 out_list.append(new_item)
452
453 #print "No match for alias '%s'" % lookup_name
454 return out_list
455
456def GetTopLevel():
457 """Return name of top-level directory for this git repo.
458
459 Returns:
460 Full path to git top-level directory
461
462 This test makes sure that we are running tests in the right subdir
463
Doug Anderson51d73212012-11-26 15:21:40 +0000464 >>> os.path.realpath(os.path.dirname(__file__)) == \
465 os.path.join(GetTopLevel(), 'tools', 'patman')
Simon Glass26132882012-01-14 15:12:45 +0000466 True
467 """
468 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
469
470def GetAliasFile():
471 """Gets the name of the git alias file.
472
473 Returns:
474 Filename of git alias file, or None if none
475 """
Simon Glass519fad22012-12-15 10:42:05 +0000476 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
477 raise_on_error=False)
Simon Glass26132882012-01-14 15:12:45 +0000478 if fname:
479 fname = os.path.join(GetTopLevel(), fname.strip())
480 return fname
481
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000482def GetDefaultUserName():
483 """Gets the user.name from .gitconfig file.
484
485 Returns:
486 User name found in .gitconfig file, or None if none
487 """
488 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
489 return uname
490
491def GetDefaultUserEmail():
492 """Gets the user.email from the global .gitconfig file.
493
494 Returns:
495 User's email found in .gitconfig file, or None if none
496 """
497 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
498 return uemail
499
Simon Glass26132882012-01-14 15:12:45 +0000500def Setup():
501 """Set up git utils, by reading the alias files."""
Simon Glass26132882012-01-14 15:12:45 +0000502 # Check for a git alias file also
503 alias_fname = GetAliasFile()
504 if alias_fname:
505 settings.ReadGitAliases(alias_fname)
506
Simon Glass11aba512012-12-15 10:42:07 +0000507def GetHead():
508 """Get the hash of the current HEAD
509
510 Returns:
511 Hash of HEAD
512 """
513 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
514
Simon Glass26132882012-01-14 15:12:45 +0000515if __name__ == "__main__":
516 import doctest
517
518 doctest.testmod()