]> git.kernelconcepts.de Git - karo-tx-uboot.git/blob - tools/patman/gitutil.py
Merge branch 'u-boot-imx/master' into 'u-boot-arm/master'
[karo-tx-uboot.git] / tools / patman / gitutil.py
1 # 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
22 import command
23 import re
24 import os
25 import series
26 import subprocess
27 import sys
28 import terminal
29
30 import settings
31
32
33 def 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     """
42     pipe = [['git', 'log', '--no-color', '--oneline', '--no-decorate',
43              '@{upstream}..'],
44             ['wc', '-l']]
45     stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
46     patch_count = int(stdout)
47     return patch_count
48
49 def 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
73 def 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
86 def 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)
96     pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', '--no-decorate',
97              range_expr],
98             ['wc', '-l']]
99     result = command.RunPipe(pipe, capture=True, oneline=True)
100     patch_count = int(result.stdout)
101     return patch_count
102
103 def 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     """
111     pipe = [['git', 'log', '--oneline', '--no-decorate', commit_range],
112             ['wc', '-l']]
113     stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
114     patch_count = int(stdout)
115     return patch_count
116
117 def 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
136 def 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
147 def 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
163 def 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
195 def 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
217 def 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
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.
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
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.
294
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:
315         raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
316     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
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.
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
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.
339
340     Returns:
341         Git command that was/would be run
342
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'
346
347     >>> 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']
357     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
358             False, alias)
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, \
362             alias)
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', \
367             True, alias)
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', \
371             False, alias)
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'
375
376     # Restore argv[0] since we clobbered it.
377     >>> sys.argv[0] = _old_argv0
378     """
379     to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
380     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
384     cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
385     if self_only:
386         to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
387         cc = []
388     cmd = ['git', 'send-email', '--annotate']
389     if in_reply_to:
390         cmd.append('--in-reply-to="%s"' % in_reply_to)
391
392     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
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
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
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.
414
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'
447     >>> 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']
456     """
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()
464     col = terminal.Color()
465
466     out_list = []
467     if level > 10:
468         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
474
475     if lookup_name:
476         if not lookup_name in alias:
477             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
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)
488
489     #print "No match for alias '%s'" % lookup_name
490     return out_list
491
492 def 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
500     >>> os.path.realpath(os.path.dirname(__file__)) == \
501             os.path.join(GetTopLevel(), 'tools', 'patman')
502     True
503     """
504     return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
505
506 def GetAliasFile():
507     """Gets the name of the git alias file.
508
509     Returns:
510         Filename of git alias file, or None if none
511     """
512     fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
513             raise_on_error=False)
514     if fname:
515         fname = os.path.join(GetTopLevel(), fname.strip())
516     return fname
517
518 def 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
527 def 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
536 def Setup():
537     """Set up git utils, by reading the alias files."""
538     # Check for a git alias file also
539     alias_fname = GetAliasFile()
540     if alias_fname:
541         settings.ReadGitAliases(alias_fname)
542
543 def 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
551 if __name__ == "__main__":
552     import doctest
553
554     doctest.testmod()