]> git.kernelconcepts.de Git - karo-tx-uboot.git/blob - tools/patman/gitutil.py
Merge branch 'master' of git://git.denx.de/u-boot
[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 settings
27 import subprocess
28 import sys
29 import terminal
30
31
32 def CountCommitsToBranch():
33     """Returns number of commits between HEAD and the tracking branch.
34
35     This looks back to the tracking branch and works out the number of commits
36     since then.
37
38     Return:
39         Number of patches that exist on top of the branch
40     """
41     pipe = [['git', 'log', '--no-color', '--oneline', '@{upstream}..'],
42             ['wc', '-l']]
43     stdout = command.RunPipe(pipe, capture=True, oneline=True)
44     patch_count = int(stdout)
45     return patch_count
46
47 def CreatePatches(start, count, series):
48     """Create a series of patches from the top of the current branch.
49
50     The patch files are written to the current directory using
51     git format-patch.
52
53     Args:
54         start: Commit to start from: 0=HEAD, 1=next one, etc.
55         count: number of commits to include
56     Return:
57         Filename of cover letter
58         List of filenames of patch files
59     """
60     if series.get('version'):
61         version = '%s ' % series['version']
62     cmd = ['git', 'format-patch', '-M', '--signoff']
63     if series.get('cover'):
64         cmd.append('--cover-letter')
65     prefix = series.GetPatchPrefix()
66     if prefix:
67         cmd += ['--subject-prefix=%s' % prefix]
68     cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
69
70     stdout = command.RunList(cmd)
71     files = stdout.splitlines()
72
73     # We have an extra file if there is a cover letter
74     if series.get('cover'):
75        return files[0], files[1:]
76     else:
77        return None, files
78
79 def ApplyPatch(verbose, fname):
80     """Apply a patch with git am to test it
81
82     TODO: Convert these to use command, with stderr option
83
84     Args:
85         fname: filename of patch file to apply
86     """
87     cmd = ['git', 'am', fname]
88     pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
89             stderr=subprocess.PIPE)
90     stdout, stderr = pipe.communicate()
91     re_error = re.compile('^error: patch failed: (.+):(\d+)')
92     for line in stderr.splitlines():
93         if verbose:
94             print line
95         match = re_error.match(line)
96         if match:
97             print GetWarningMsg('warning', match.group(1), int(match.group(2)),
98                     'Patch failed')
99     return pipe.returncode == 0, stdout
100
101 def ApplyPatches(verbose, args, start_point):
102     """Apply the patches with git am to make sure all is well
103
104     Args:
105         verbose: Print out 'git am' output verbatim
106         args: List of patch files to apply
107         start_point: Number of commits back from HEAD to start applying.
108             Normally this is len(args), but it can be larger if a start
109             offset was given.
110     """
111     error_count = 0
112     col = terminal.Color()
113
114     # Figure out our current position
115     cmd = ['git', 'name-rev', 'HEAD', '--name-only']
116     pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
117     stdout, stderr = pipe.communicate()
118     if pipe.returncode:
119         str = 'Could not find current commit name'
120         print col.Color(col.RED, str)
121         print stdout
122         return False
123     old_head = stdout.splitlines()[0]
124
125     # Checkout the required start point
126     cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
127     pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
128             stderr=subprocess.PIPE)
129     stdout, stderr = pipe.communicate()
130     if pipe.returncode:
131         str = 'Could not move to commit before patch series'
132         print col.Color(col.RED, str)
133         print stdout, stderr
134         return False
135
136     # Apply all the patches
137     for fname in args:
138         ok, stdout = ApplyPatch(verbose, fname)
139         if not ok:
140             print col.Color(col.RED, 'git am returned errors for %s: will '
141                     'skip this patch' % fname)
142             if verbose:
143                 print stdout
144             error_count += 1
145             cmd = ['git', 'am', '--skip']
146             pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
147             stdout, stderr = pipe.communicate()
148             if pipe.returncode != 0:
149                 print col.Color(col.RED, 'Unable to skip patch! Aborting...')
150                 print stdout
151                 break
152
153     # Return to our previous position
154     cmd = ['git', 'checkout', old_head]
155     pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
156     stdout, stderr = pipe.communicate()
157     if pipe.returncode:
158         print col.Color(col.RED, 'Could not move back to head commit')
159         print stdout, stderr
160     return error_count == 0
161
162 def BuildEmailList(in_list, tag=None, alias=None):
163     """Build a list of email addresses based on an input list.
164
165     Takes a list of email addresses and aliases, and turns this into a list
166     of only email address, by resolving any aliases that are present.
167
168     If the tag is given, then each email address is prepended with this
169     tag and a space. If the tag starts with a minus sign (indicating a
170     command line parameter) then the email address is quoted.
171
172     Args:
173         in_list:        List of aliases/email addresses
174         tag:            Text to put before each address
175
176     Returns:
177         List of email addresses
178
179     >>> alias = {}
180     >>> alias['fred'] = ['f.bloggs@napier.co.nz']
181     >>> alias['john'] = ['j.bloggs@napier.co.nz']
182     >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
183     >>> alias['boys'] = ['fred', ' john']
184     >>> alias['all'] = ['fred ', 'john', '   mary   ']
185     >>> BuildEmailList(['john', 'mary'], None, alias)
186     ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
187     >>> BuildEmailList(['john', 'mary'], '--to', alias)
188     ['--to "j.bloggs@napier.co.nz"', \
189 '--to "Mary Poppins <m.poppins@cloud.net>"']
190     >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
191     ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
192     """
193     quote = '"' if tag and tag[0] == '-' else ''
194     raw = []
195     for item in in_list:
196         raw += LookupEmail(item, alias)
197     result = []
198     for item in raw:
199         if not item in result:
200             result.append(item)
201     if tag:
202         return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
203     return result
204
205 def EmailPatches(series, cover_fname, args, dry_run, cc_fname,
206         self_only=False, alias=None):
207     """Email a patch series.
208
209     Args:
210         series: Series object containing destination info
211         cover_fname: filename of cover letter
212         args: list of filenames of patch files
213         dry_run: Just return the command that would be run
214         cc_fname: Filename of Cc file for per-commit Cc
215         self_only: True to just email to yourself as a test
216
217     Returns:
218         Git command that was/would be run
219
220     # For the duration of this doctest pretend that we ran patman with ./patman
221     >>> _old_argv0 = sys.argv[0]
222     >>> sys.argv[0] = './patman'
223
224     >>> alias = {}
225     >>> alias['fred'] = ['f.bloggs@napier.co.nz']
226     >>> alias['john'] = ['j.bloggs@napier.co.nz']
227     >>> alias['mary'] = ['m.poppins@cloud.net']
228     >>> alias['boys'] = ['fred', ' john']
229     >>> alias['all'] = ['fred ', 'john', '   mary   ']
230     >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
231     >>> series = series.Series()
232     >>> series.to = ['fred']
233     >>> series.cc = ['mary']
234     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
235             alias)
236     'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
237 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
238     >>> EmailPatches(series, None, ['p1'], True, 'cc-fname', False, alias)
239     'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
240 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
241     >>> series.cc = ['all']
242     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', True, \
243             alias)
244     'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
245 --cc-cmd cc-fname" cover p1 p2'
246     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
247             alias)
248     'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
249 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
250 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
251
252     # Restore argv[0] since we clobbered it.
253     >>> sys.argv[0] = _old_argv0
254     """
255     to = BuildEmailList(series.get('to'), '--to', alias)
256     if not to:
257         print ("No recipient, please add something like this to a commit\n"
258             "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
259         return
260     cc = BuildEmailList(series.get('cc'), '--cc', alias)
261     if self_only:
262         to = BuildEmailList([os.getenv('USER')], '--to', alias)
263         cc = []
264     cmd = ['git', 'send-email', '--annotate']
265     cmd += to
266     cmd += cc
267     cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
268     if cover_fname:
269         cmd.append(cover_fname)
270     cmd += args
271     str = ' '.join(cmd)
272     if not dry_run:
273         os.system(str)
274     return str
275
276
277 def LookupEmail(lookup_name, alias=None, level=0):
278     """If an email address is an alias, look it up and return the full name
279
280     TODO: Why not just use git's own alias feature?
281
282     Args:
283         lookup_name: Alias or email address to look up
284
285     Returns:
286         tuple:
287             list containing a list of email addresses
288
289     Raises:
290         OSError if a recursive alias reference was found
291         ValueError if an alias was not found
292
293     >>> alias = {}
294     >>> alias['fred'] = ['f.bloggs@napier.co.nz']
295     >>> alias['john'] = ['j.bloggs@napier.co.nz']
296     >>> alias['mary'] = ['m.poppins@cloud.net']
297     >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
298     >>> alias['all'] = ['fred ', 'john', '   mary   ']
299     >>> alias['loop'] = ['other', 'john', '   mary   ']
300     >>> alias['other'] = ['loop', 'john', '   mary   ']
301     >>> LookupEmail('mary', alias)
302     ['m.poppins@cloud.net']
303     >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
304     ['arthur.wellesley@howe.ro.uk']
305     >>> LookupEmail('boys', alias)
306     ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
307     >>> LookupEmail('all', alias)
308     ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
309     >>> LookupEmail('odd', alias)
310     Traceback (most recent call last):
311     ...
312     ValueError: Alias 'odd' not found
313     >>> LookupEmail('loop', alias)
314     Traceback (most recent call last):
315     ...
316     OSError: Recursive email alias at 'other'
317     """
318     if not alias:
319         alias = settings.alias
320     lookup_name = lookup_name.strip()
321     if '@' in lookup_name: # Perhaps a real email address
322         return [lookup_name]
323
324     lookup_name = lookup_name.lower()
325
326     if level > 10:
327         raise OSError, "Recursive email alias at '%s'" % lookup_name
328
329     out_list = []
330     if lookup_name:
331         if not lookup_name in alias:
332             raise ValueError, "Alias '%s' not found" % lookup_name
333         for item in alias[lookup_name]:
334             todo = LookupEmail(item, alias, level + 1)
335             for new_item in todo:
336                 if not new_item in out_list:
337                     out_list.append(new_item)
338
339     #print "No match for alias '%s'" % lookup_name
340     return out_list
341
342 def GetTopLevel():
343     """Return name of top-level directory for this git repo.
344
345     Returns:
346         Full path to git top-level directory
347
348     This test makes sure that we are running tests in the right subdir
349
350     >>> os.path.realpath(os.path.dirname(__file__)) == \
351             os.path.join(GetTopLevel(), 'tools', 'patman')
352     True
353     """
354     return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
355
356 def GetAliasFile():
357     """Gets the name of the git alias file.
358
359     Returns:
360         Filename of git alias file, or None if none
361     """
362     fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile')
363     if fname:
364         fname = os.path.join(GetTopLevel(), fname.strip())
365     return fname
366
367 def GetDefaultUserName():
368     """Gets the user.name from .gitconfig file.
369
370     Returns:
371         User name found in .gitconfig file, or None if none
372     """
373     uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
374     return uname
375
376 def GetDefaultUserEmail():
377     """Gets the user.email from the global .gitconfig file.
378
379     Returns:
380         User's email found in .gitconfig file, or None if none
381     """
382     uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
383     return uemail
384
385 def Setup():
386     """Set up git utils, by reading the alias files."""
387     # Check for a git alias file also
388     alias_fname = GetAliasFile()
389     if alias_fname:
390         settings.ReadGitAliases(alias_fname)
391
392 if __name__ == "__main__":
393     import doctest
394
395     doctest.testmod()