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