]> 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', '@{upstream}..'],
43             ['wc', '-l']]
44     stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
45     patch_count = int(stdout)
46     return patch_count
47
48 def 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
72 def 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
85 def 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
101 def 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
115 def 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
134 def 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
145 def 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
161 def 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
193 def 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
215 def 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
276 def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
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         alias:          Alias dictionary
290         raise_on_error: True to raise an error when an alias fails to match,
291                 False to just print a message.
292
293     Returns:
294         List of email addresses
295
296     >>> alias = {}
297     >>> alias['fred'] = ['f.bloggs@napier.co.nz']
298     >>> alias['john'] = ['j.bloggs@napier.co.nz']
299     >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
300     >>> alias['boys'] = ['fred', ' john']
301     >>> alias['all'] = ['fred ', 'john', '   mary   ']
302     >>> BuildEmailList(['john', 'mary'], None, alias)
303     ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
304     >>> BuildEmailList(['john', 'mary'], '--to', alias)
305     ['--to "j.bloggs@napier.co.nz"', \
306 '--to "Mary Poppins <m.poppins@cloud.net>"']
307     >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
308     ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
309     """
310     quote = '"' if tag and tag[0] == '-' else ''
311     raw = []
312     for item in in_list:
313         raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
314     result = []
315     for item in raw:
316         if not item in result:
317             result.append(item)
318     if tag:
319         return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
320     return result
321
322 def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
323         self_only=False, alias=None, in_reply_to=None):
324     """Email a patch series.
325
326     Args:
327         series: Series object containing destination info
328         cover_fname: filename of cover letter
329         args: list of filenames of patch files
330         dry_run: Just return the command that would be run
331         raise_on_error: True to raise an error when an alias fails to match,
332                 False to just print a message.
333         cc_fname: Filename of Cc file for per-commit Cc
334         self_only: True to just email to yourself as a test
335         in_reply_to: If set we'll pass this to git as --in-reply-to.
336             Should be a message ID that this is in reply to.
337
338     Returns:
339         Git command that was/would be run
340
341     # For the duration of this doctest pretend that we ran patman with ./patman
342     >>> _old_argv0 = sys.argv[0]
343     >>> sys.argv[0] = './patman'
344
345     >>> alias = {}
346     >>> alias['fred'] = ['f.bloggs@napier.co.nz']
347     >>> alias['john'] = ['j.bloggs@napier.co.nz']
348     >>> alias['mary'] = ['m.poppins@cloud.net']
349     >>> alias['boys'] = ['fred', ' john']
350     >>> alias['all'] = ['fred ', 'john', '   mary   ']
351     >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
352     >>> series = series.Series()
353     >>> series.to = ['fred']
354     >>> series.cc = ['mary']
355     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
356             False, alias)
357     'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
358 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
359     >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
360             alias)
361     'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
362 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
363     >>> series.cc = ['all']
364     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
365             True, alias)
366     'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
367 --cc-cmd cc-fname" cover p1 p2'
368     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
369             False, alias)
370     'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
371 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
372 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
373
374     # Restore argv[0] since we clobbered it.
375     >>> sys.argv[0] = _old_argv0
376     """
377     to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
378     if not to:
379         print ("No recipient, please add something like this to a commit\n"
380             "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
381         return
382     cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
383     if self_only:
384         to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
385         cc = []
386     cmd = ['git', 'send-email', '--annotate']
387     if in_reply_to:
388         cmd.append('--in-reply-to="%s"' % in_reply_to)
389
390     cmd += to
391     cmd += cc
392     cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
393     if cover_fname:
394         cmd.append(cover_fname)
395     cmd += args
396     str = ' '.join(cmd)
397     if not dry_run:
398         os.system(str)
399     return str
400
401
402 def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
403     """If an email address is an alias, look it up and return the full name
404
405     TODO: Why not just use git's own alias feature?
406
407     Args:
408         lookup_name: Alias or email address to look up
409         alias: Dictionary containing aliases (None to use settings default)
410         raise_on_error: True to raise an error when an alias fails to match,
411                 False to just print a message.
412
413     Returns:
414         tuple:
415             list containing a list of email addresses
416
417     Raises:
418         OSError if a recursive alias reference was found
419         ValueError if an alias was not found
420
421     >>> alias = {}
422     >>> alias['fred'] = ['f.bloggs@napier.co.nz']
423     >>> alias['john'] = ['j.bloggs@napier.co.nz']
424     >>> alias['mary'] = ['m.poppins@cloud.net']
425     >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
426     >>> alias['all'] = ['fred ', 'john', '   mary   ']
427     >>> alias['loop'] = ['other', 'john', '   mary   ']
428     >>> alias['other'] = ['loop', 'john', '   mary   ']
429     >>> LookupEmail('mary', alias)
430     ['m.poppins@cloud.net']
431     >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
432     ['arthur.wellesley@howe.ro.uk']
433     >>> LookupEmail('boys', alias)
434     ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
435     >>> LookupEmail('all', alias)
436     ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
437     >>> LookupEmail('odd', alias)
438     Traceback (most recent call last):
439     ...
440     ValueError: Alias 'odd' not found
441     >>> LookupEmail('loop', alias)
442     Traceback (most recent call last):
443     ...
444     OSError: Recursive email alias at 'other'
445     >>> LookupEmail('odd', alias, raise_on_error=False)
446     \033[1;31mAlias 'odd' not found\033[0m
447     []
448     >>> # In this case the loop part will effectively be ignored.
449     >>> LookupEmail('loop', alias, raise_on_error=False)
450     \033[1;31mRecursive email alias at 'other'\033[0m
451     \033[1;31mRecursive email alias at 'john'\033[0m
452     \033[1;31mRecursive email alias at 'mary'\033[0m
453     ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
454     """
455     if not alias:
456         alias = settings.alias
457     lookup_name = lookup_name.strip()
458     if '@' in lookup_name: # Perhaps a real email address
459         return [lookup_name]
460
461     lookup_name = lookup_name.lower()
462     col = terminal.Color()
463
464     out_list = []
465     if level > 10:
466         msg = "Recursive email alias at '%s'" % lookup_name
467         if raise_on_error:
468             raise OSError, msg
469         else:
470             print col.Color(col.RED, msg)
471             return out_list
472
473     if lookup_name:
474         if not lookup_name in alias:
475             msg = "Alias '%s' not found" % lookup_name
476             if raise_on_error:
477                 raise ValueError, msg
478             else:
479                 print col.Color(col.RED, msg)
480                 return out_list
481         for item in alias[lookup_name]:
482             todo = LookupEmail(item, alias, raise_on_error, level + 1)
483             for new_item in todo:
484                 if not new_item in out_list:
485                     out_list.append(new_item)
486
487     #print "No match for alias '%s'" % lookup_name
488     return out_list
489
490 def GetTopLevel():
491     """Return name of top-level directory for this git repo.
492
493     Returns:
494         Full path to git top-level directory
495
496     This test makes sure that we are running tests in the right subdir
497
498     >>> os.path.realpath(os.path.dirname(__file__)) == \
499             os.path.join(GetTopLevel(), 'tools', 'patman')
500     True
501     """
502     return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
503
504 def GetAliasFile():
505     """Gets the name of the git alias file.
506
507     Returns:
508         Filename of git alias file, or None if none
509     """
510     fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
511             raise_on_error=False)
512     if fname:
513         fname = os.path.join(GetTopLevel(), fname.strip())
514     return fname
515
516 def GetDefaultUserName():
517     """Gets the user.name from .gitconfig file.
518
519     Returns:
520         User name found in .gitconfig file, or None if none
521     """
522     uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
523     return uname
524
525 def GetDefaultUserEmail():
526     """Gets the user.email from the global .gitconfig file.
527
528     Returns:
529         User's email found in .gitconfig file, or None if none
530     """
531     uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
532     return uemail
533
534 def Setup():
535     """Set up git utils, by reading the alias files."""
536     # Check for a git alias file also
537     alias_fname = GetAliasFile()
538     if alias_fname:
539         settings.ReadGitAliases(alias_fname)
540
541 def GetHead():
542     """Get the hash of the current HEAD
543
544     Returns:
545         Hash of HEAD
546     """
547     return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
548
549 if __name__ == "__main__":
550     import doctest
551
552     doctest.testmod()