]> git.kernelconcepts.de Git - karo-tx-uboot.git/blob - tools/patman/gitutil.py
Merge remote-tracking branch 'u-boot-ti/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 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     >>> alias = {}
221     >>> alias['fred'] = ['f.bloggs@napier.co.nz']
222     >>> alias['john'] = ['j.bloggs@napier.co.nz']
223     >>> alias['mary'] = ['m.poppins@cloud.net']
224     >>> alias['boys'] = ['fred', ' john']
225     >>> alias['all'] = ['fred ', 'john', '   mary   ']
226     >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
227     >>> series = series.Series()
228     >>> series.to = ['fred']
229     >>> series.cc = ['mary']
230     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
231             alias)
232     'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
233 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
234     >>> EmailPatches(series, None, ['p1'], True, 'cc-fname', False, alias)
235     'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
236 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
237     >>> series.cc = ['all']
238     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', True, \
239             alias)
240     'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
241 --cc-cmd cc-fname" cover p1 p2'
242     >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
243             alias)
244     'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
245 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
246 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
247     """
248     to = BuildEmailList(series.get('to'), '--to', alias)
249     if not to:
250         print ("No recipient, please add something like this to a commit\n"
251             "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
252         return
253     cc = BuildEmailList(series.get('cc'), '--cc', alias)
254     if self_only:
255         to = BuildEmailList([os.getenv('USER')], '--to', alias)
256         cc = []
257     cmd = ['git', 'send-email', '--annotate']
258     cmd += to
259     cmd += cc
260     cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
261     if cover_fname:
262         cmd.append(cover_fname)
263     cmd += args
264     str = ' '.join(cmd)
265     if not dry_run:
266         os.system(str)
267     return str
268
269
270 def LookupEmail(lookup_name, alias=None, level=0):
271     """If an email address is an alias, look it up and return the full name
272
273     TODO: Why not just use git's own alias feature?
274
275     Args:
276         lookup_name: Alias or email address to look up
277
278     Returns:
279         tuple:
280             list containing a list of email addresses
281
282     Raises:
283         OSError if a recursive alias reference was found
284         ValueError if an alias was not found
285
286     >>> alias = {}
287     >>> alias['fred'] = ['f.bloggs@napier.co.nz']
288     >>> alias['john'] = ['j.bloggs@napier.co.nz']
289     >>> alias['mary'] = ['m.poppins@cloud.net']
290     >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
291     >>> alias['all'] = ['fred ', 'john', '   mary   ']
292     >>> alias['loop'] = ['other', 'john', '   mary   ']
293     >>> alias['other'] = ['loop', 'john', '   mary   ']
294     >>> LookupEmail('mary', alias)
295     ['m.poppins@cloud.net']
296     >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
297     ['arthur.wellesley@howe.ro.uk']
298     >>> LookupEmail('boys', alias)
299     ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
300     >>> LookupEmail('all', alias)
301     ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
302     >>> LookupEmail('odd', alias)
303     Traceback (most recent call last):
304     ...
305     ValueError: Alias 'odd' not found
306     >>> LookupEmail('loop', alias)
307     Traceback (most recent call last):
308     ...
309     OSError: Recursive email alias at 'other'
310     """
311     if not alias:
312         alias = settings.alias
313     lookup_name = lookup_name.strip()
314     if '@' in lookup_name: # Perhaps a real email address
315         return [lookup_name]
316
317     lookup_name = lookup_name.lower()
318
319     if level > 10:
320         raise OSError, "Recursive email alias at '%s'" % lookup_name
321
322     out_list = []
323     if lookup_name:
324         if not lookup_name in alias:
325             raise ValueError, "Alias '%s' not found" % lookup_name
326         for item in alias[lookup_name]:
327             todo = LookupEmail(item, alias, level + 1)
328             for new_item in todo:
329                 if not new_item in out_list:
330                     out_list.append(new_item)
331
332     #print "No match for alias '%s'" % lookup_name
333     return out_list
334
335 def GetTopLevel():
336     """Return name of top-level directory for this git repo.
337
338     Returns:
339         Full path to git top-level directory
340
341     This test makes sure that we are running tests in the right subdir
342
343     >>> os.path.realpath(os.getcwd()) == \
344             os.path.join(GetTopLevel(), 'tools', 'scripts', 'patman')
345     True
346     """
347     return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
348
349 def GetAliasFile():
350     """Gets the name of the git alias file.
351
352     Returns:
353         Filename of git alias file, or None if none
354     """
355     fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile')
356     if fname:
357         fname = os.path.join(GetTopLevel(), fname.strip())
358     return fname
359
360 def GetDefaultUserName():
361     """Gets the user.name from .gitconfig file.
362
363     Returns:
364         User name found in .gitconfig file, or None if none
365     """
366     uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
367     return uname
368
369 def GetDefaultUserEmail():
370     """Gets the user.email from the global .gitconfig file.
371
372     Returns:
373         User's email found in .gitconfig file, or None if none
374     """
375     uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
376     return uemail
377
378 def Setup():
379     """Set up git utils, by reading the alias files."""
380     settings.Setup('')
381
382     # Check for a git alias file also
383     alias_fname = GetAliasFile()
384     if alias_fname:
385         settings.ReadGitAliases(alias_fname)
386
387 if __name__ == "__main__":
388     import doctest
389
390     doctest.testmod()