xref: /openbmc/u-boot/tools/patman/gitutil.py (revision 427ac8cc)
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
22import command
23import re
24import os
25import series
26import settings
27import subprocess
28import sys
29import terminal
30
31
32def 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
47def 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
79def 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
101def 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
162def 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
205def 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
277def 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
342def 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
356def 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
367def 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
376def 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
385def 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
392if __name__ == "__main__":
393    import doctest
394
395    doctest.testmod()
396