xref: /rk3399_rockchip-uboot/tools/patman/gitutil.py (revision cce717a96c5840b0cf33e30c87fb1f2b8d633ee3)
10d24de9dSSimon Glass# Copyright (c) 2011 The Chromium OS Authors.
20d24de9dSSimon Glass#
30d24de9dSSimon Glass# See file CREDITS for list of people who contributed to this
40d24de9dSSimon Glass# project.
50d24de9dSSimon Glass#
60d24de9dSSimon Glass# This program is free software; you can redistribute it and/or
70d24de9dSSimon Glass# modify it under the terms of the GNU General Public License as
80d24de9dSSimon Glass# published by the Free Software Foundation; either version 2 of
90d24de9dSSimon Glass# the License, or (at your option) any later version.
100d24de9dSSimon Glass#
110d24de9dSSimon Glass# This program is distributed in the hope that it will be useful,
120d24de9dSSimon Glass# but WITHOUT ANY WARRANTY; without even the implied warranty of
130d24de9dSSimon Glass# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
140d24de9dSSimon Glass# GNU General Public License for more details.
150d24de9dSSimon Glass#
160d24de9dSSimon Glass# You should have received a copy of the GNU General Public License
170d24de9dSSimon Glass# along with this program; if not, write to the Free Software
180d24de9dSSimon Glass# Foundation, Inc., 59 Temple Place, Suite 330, Boston,
190d24de9dSSimon Glass# MA 02111-1307 USA
200d24de9dSSimon Glass#
210d24de9dSSimon Glass
220d24de9dSSimon Glassimport command
230d24de9dSSimon Glassimport re
240d24de9dSSimon Glassimport os
250d24de9dSSimon Glassimport series
260d24de9dSSimon Glassimport subprocess
270d24de9dSSimon Glassimport sys
280d24de9dSSimon Glassimport terminal
290d24de9dSSimon Glass
305f6a1c42SSimon Glassimport settings
315f6a1c42SSimon Glass
320d24de9dSSimon Glass
330d24de9dSSimon Glassdef CountCommitsToBranch():
340d24de9dSSimon Glass    """Returns number of commits between HEAD and the tracking branch.
350d24de9dSSimon Glass
360d24de9dSSimon Glass    This looks back to the tracking branch and works out the number of commits
370d24de9dSSimon Glass    since then.
380d24de9dSSimon Glass
390d24de9dSSimon Glass    Return:
400d24de9dSSimon Glass        Number of patches that exist on top of the branch
410d24de9dSSimon Glass    """
422386060cSAndreas Bießmann    pipe = [['git', 'log', '--no-color', '--oneline', '--no-decorate',
432386060cSAndreas Bießmann             '@{upstream}..'],
440d24de9dSSimon Glass            ['wc', '-l']]
45a10fd93cSSimon Glass    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
460d24de9dSSimon Glass    patch_count = int(stdout)
470d24de9dSSimon Glass    return patch_count
480d24de9dSSimon Glass
495f6a1c42SSimon Glassdef GetUpstream(git_dir, branch):
505f6a1c42SSimon Glass    """Returns the name of the upstream for a branch
515f6a1c42SSimon Glass
525f6a1c42SSimon Glass    Args:
535f6a1c42SSimon Glass        git_dir: Git directory containing repo
545f6a1c42SSimon Glass        branch: Name of branch
555f6a1c42SSimon Glass
565f6a1c42SSimon Glass    Returns:
575f6a1c42SSimon Glass        Name of upstream branch (e.g. 'upstream/master') or None if none
585f6a1c42SSimon Glass    """
59*cce717a9SSimon Glass    try:
605f6a1c42SSimon Glass        remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
615f6a1c42SSimon Glass                                       'branch.%s.remote' % branch)
625f6a1c42SSimon Glass        merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
635f6a1c42SSimon Glass                                      'branch.%s.merge' % branch)
64*cce717a9SSimon Glass    except:
65*cce717a9SSimon Glass        return None
66*cce717a9SSimon Glass
675f6a1c42SSimon Glass    if remote == '.':
685f6a1c42SSimon Glass        return merge
695f6a1c42SSimon Glass    elif remote and merge:
705f6a1c42SSimon Glass        leaf = merge.split('/')[-1]
715f6a1c42SSimon Glass        return '%s/%s' % (remote, leaf)
725f6a1c42SSimon Glass    else:
735f6a1c42SSimon Glass        raise ValueError, ("Cannot determine upstream branch for branch "
745f6a1c42SSimon Glass                "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
755f6a1c42SSimon Glass
765f6a1c42SSimon Glass
775f6a1c42SSimon Glassdef GetRangeInBranch(git_dir, branch, include_upstream=False):
785f6a1c42SSimon Glass    """Returns an expression for the commits in the given branch.
795f6a1c42SSimon Glass
805f6a1c42SSimon Glass    Args:
815f6a1c42SSimon Glass        git_dir: Directory containing git repo
825f6a1c42SSimon Glass        branch: Name of branch
835f6a1c42SSimon Glass    Return:
845f6a1c42SSimon Glass        Expression in the form 'upstream..branch' which can be used to
85*cce717a9SSimon Glass        access the commits. If the branch does not exist, returns None.
865f6a1c42SSimon Glass    """
875f6a1c42SSimon Glass    upstream = GetUpstream(git_dir, branch)
88*cce717a9SSimon Glass    if not upstream:
89*cce717a9SSimon Glass        return None
905f6a1c42SSimon Glass    return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
915f6a1c42SSimon Glass
925f6a1c42SSimon Glassdef CountCommitsInBranch(git_dir, branch, include_upstream=False):
935f6a1c42SSimon Glass    """Returns the number of commits in the given branch.
945f6a1c42SSimon Glass
955f6a1c42SSimon Glass    Args:
965f6a1c42SSimon Glass        git_dir: Directory containing git repo
975f6a1c42SSimon Glass        branch: Name of branch
985f6a1c42SSimon Glass    Return:
99*cce717a9SSimon Glass        Number of patches that exist on top of the branch, or None if the
100*cce717a9SSimon Glass        branch does not exist.
1015f6a1c42SSimon Glass    """
1025f6a1c42SSimon Glass    range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
103*cce717a9SSimon Glass    if not range_expr:
104*cce717a9SSimon Glass        return None
1052386060cSAndreas Bießmann    pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', '--no-decorate',
1062386060cSAndreas Bießmann             range_expr],
1075f6a1c42SSimon Glass            ['wc', '-l']]
1085f6a1c42SSimon Glass    result = command.RunPipe(pipe, capture=True, oneline=True)
1095f6a1c42SSimon Glass    patch_count = int(result.stdout)
1105f6a1c42SSimon Glass    return patch_count
1115f6a1c42SSimon Glass
1125f6a1c42SSimon Glassdef CountCommits(commit_range):
1135f6a1c42SSimon Glass    """Returns the number of commits in the given range.
1145f6a1c42SSimon Glass
1155f6a1c42SSimon Glass    Args:
1165f6a1c42SSimon Glass        commit_range: Range of commits to count (e.g. 'HEAD..base')
1175f6a1c42SSimon Glass    Return:
1185f6a1c42SSimon Glass        Number of patches that exist on top of the branch
1195f6a1c42SSimon Glass    """
1202386060cSAndreas Bießmann    pipe = [['git', 'log', '--oneline', '--no-decorate', commit_range],
1215f6a1c42SSimon Glass            ['wc', '-l']]
1225f6a1c42SSimon Glass    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
1235f6a1c42SSimon Glass    patch_count = int(stdout)
1245f6a1c42SSimon Glass    return patch_count
1255f6a1c42SSimon Glass
1265f6a1c42SSimon Glassdef Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
1275f6a1c42SSimon Glass    """Checkout the selected commit for this build
1285f6a1c42SSimon Glass
1295f6a1c42SSimon Glass    Args:
1305f6a1c42SSimon Glass        commit_hash: Commit hash to check out
1315f6a1c42SSimon Glass    """
1325f6a1c42SSimon Glass    pipe = ['git']
1335f6a1c42SSimon Glass    if git_dir:
1345f6a1c42SSimon Glass        pipe.extend(['--git-dir', git_dir])
1355f6a1c42SSimon Glass    if work_tree:
1365f6a1c42SSimon Glass        pipe.extend(['--work-tree', work_tree])
1375f6a1c42SSimon Glass    pipe.append('checkout')
1385f6a1c42SSimon Glass    if force:
1395f6a1c42SSimon Glass        pipe.append('-f')
1405f6a1c42SSimon Glass    pipe.append(commit_hash)
1415f6a1c42SSimon Glass    result = command.RunPipe([pipe], capture=True, raise_on_error=False)
1425f6a1c42SSimon Glass    if result.return_code != 0:
1435f6a1c42SSimon Glass        raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
1445f6a1c42SSimon Glass
1455f6a1c42SSimon Glassdef Clone(git_dir, output_dir):
1465f6a1c42SSimon Glass    """Checkout the selected commit for this build
1475f6a1c42SSimon Glass
1485f6a1c42SSimon Glass    Args:
1495f6a1c42SSimon Glass        commit_hash: Commit hash to check out
1505f6a1c42SSimon Glass    """
1515f6a1c42SSimon Glass    pipe = ['git', 'clone', git_dir, '.']
1525f6a1c42SSimon Glass    result = command.RunPipe([pipe], capture=True, cwd=output_dir)
1535f6a1c42SSimon Glass    if result.return_code != 0:
1545f6a1c42SSimon Glass        raise OSError, 'git clone: %s' % result.stderr
1555f6a1c42SSimon Glass
1565f6a1c42SSimon Glassdef Fetch(git_dir=None, work_tree=None):
1575f6a1c42SSimon Glass    """Fetch from the origin repo
1585f6a1c42SSimon Glass
1595f6a1c42SSimon Glass    Args:
1605f6a1c42SSimon Glass        commit_hash: Commit hash to check out
1615f6a1c42SSimon Glass    """
1625f6a1c42SSimon Glass    pipe = ['git']
1635f6a1c42SSimon Glass    if git_dir:
1645f6a1c42SSimon Glass        pipe.extend(['--git-dir', git_dir])
1655f6a1c42SSimon Glass    if work_tree:
1665f6a1c42SSimon Glass        pipe.extend(['--work-tree', work_tree])
1675f6a1c42SSimon Glass    pipe.append('fetch')
1685f6a1c42SSimon Glass    result = command.RunPipe([pipe], capture=True)
1695f6a1c42SSimon Glass    if result.return_code != 0:
1705f6a1c42SSimon Glass        raise OSError, 'git fetch: %s' % result.stderr
1715f6a1c42SSimon Glass
1720d24de9dSSimon Glassdef CreatePatches(start, count, series):
1730d24de9dSSimon Glass    """Create a series of patches from the top of the current branch.
1740d24de9dSSimon Glass
1750d24de9dSSimon Glass    The patch files are written to the current directory using
1760d24de9dSSimon Glass    git format-patch.
1770d24de9dSSimon Glass
1780d24de9dSSimon Glass    Args:
1790d24de9dSSimon Glass        start: Commit to start from: 0=HEAD, 1=next one, etc.
1800d24de9dSSimon Glass        count: number of commits to include
1810d24de9dSSimon Glass    Return:
1820d24de9dSSimon Glass        Filename of cover letter
1830d24de9dSSimon Glass        List of filenames of patch files
1840d24de9dSSimon Glass    """
1850d24de9dSSimon Glass    if series.get('version'):
1860d24de9dSSimon Glass        version = '%s ' % series['version']
1870d24de9dSSimon Glass    cmd = ['git', 'format-patch', '-M', '--signoff']
1880d24de9dSSimon Glass    if series.get('cover'):
1890d24de9dSSimon Glass        cmd.append('--cover-letter')
1900d24de9dSSimon Glass    prefix = series.GetPatchPrefix()
1910d24de9dSSimon Glass    if prefix:
1920d24de9dSSimon Glass        cmd += ['--subject-prefix=%s' % prefix]
1930d24de9dSSimon Glass    cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
1940d24de9dSSimon Glass
1950d24de9dSSimon Glass    stdout = command.RunList(cmd)
1960d24de9dSSimon Glass    files = stdout.splitlines()
1970d24de9dSSimon Glass
1980d24de9dSSimon Glass    # We have an extra file if there is a cover letter
1990d24de9dSSimon Glass    if series.get('cover'):
2000d24de9dSSimon Glass       return files[0], files[1:]
2010d24de9dSSimon Glass    else:
2020d24de9dSSimon Glass       return None, files
2030d24de9dSSimon Glass
2040d24de9dSSimon Glassdef ApplyPatch(verbose, fname):
2050d24de9dSSimon Glass    """Apply a patch with git am to test it
2060d24de9dSSimon Glass
2070d24de9dSSimon Glass    TODO: Convert these to use command, with stderr option
2080d24de9dSSimon Glass
2090d24de9dSSimon Glass    Args:
2100d24de9dSSimon Glass        fname: filename of patch file to apply
2110d24de9dSSimon Glass    """
2120d24de9dSSimon Glass    cmd = ['git', 'am', fname]
2130d24de9dSSimon Glass    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
2140d24de9dSSimon Glass            stderr=subprocess.PIPE)
2150d24de9dSSimon Glass    stdout, stderr = pipe.communicate()
2160d24de9dSSimon Glass    re_error = re.compile('^error: patch failed: (.+):(\d+)')
2170d24de9dSSimon Glass    for line in stderr.splitlines():
2180d24de9dSSimon Glass        if verbose:
2190d24de9dSSimon Glass            print line
2200d24de9dSSimon Glass        match = re_error.match(line)
2210d24de9dSSimon Glass        if match:
2220d24de9dSSimon Glass            print GetWarningMsg('warning', match.group(1), int(match.group(2)),
2230d24de9dSSimon Glass                    'Patch failed')
2240d24de9dSSimon Glass    return pipe.returncode == 0, stdout
2250d24de9dSSimon Glass
2260d24de9dSSimon Glassdef ApplyPatches(verbose, args, start_point):
2270d24de9dSSimon Glass    """Apply the patches with git am to make sure all is well
2280d24de9dSSimon Glass
2290d24de9dSSimon Glass    Args:
2300d24de9dSSimon Glass        verbose: Print out 'git am' output verbatim
2310d24de9dSSimon Glass        args: List of patch files to apply
2320d24de9dSSimon Glass        start_point: Number of commits back from HEAD to start applying.
2330d24de9dSSimon Glass            Normally this is len(args), but it can be larger if a start
2340d24de9dSSimon Glass            offset was given.
2350d24de9dSSimon Glass    """
2360d24de9dSSimon Glass    error_count = 0
2370d24de9dSSimon Glass    col = terminal.Color()
2380d24de9dSSimon Glass
2390d24de9dSSimon Glass    # Figure out our current position
2400d24de9dSSimon Glass    cmd = ['git', 'name-rev', 'HEAD', '--name-only']
2410d24de9dSSimon Glass    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
2420d24de9dSSimon Glass    stdout, stderr = pipe.communicate()
2430d24de9dSSimon Glass    if pipe.returncode:
2440d24de9dSSimon Glass        str = 'Could not find current commit name'
2450d24de9dSSimon Glass        print col.Color(col.RED, str)
2460d24de9dSSimon Glass        print stdout
2470d24de9dSSimon Glass        return False
2480d24de9dSSimon Glass    old_head = stdout.splitlines()[0]
2490d24de9dSSimon Glass
2500d24de9dSSimon Glass    # Checkout the required start point
2510d24de9dSSimon Glass    cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
2520d24de9dSSimon Glass    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
2530d24de9dSSimon Glass            stderr=subprocess.PIPE)
2540d24de9dSSimon Glass    stdout, stderr = pipe.communicate()
2550d24de9dSSimon Glass    if pipe.returncode:
2560d24de9dSSimon Glass        str = 'Could not move to commit before patch series'
2570d24de9dSSimon Glass        print col.Color(col.RED, str)
2580d24de9dSSimon Glass        print stdout, stderr
2590d24de9dSSimon Glass        return False
2600d24de9dSSimon Glass
2610d24de9dSSimon Glass    # Apply all the patches
2620d24de9dSSimon Glass    for fname in args:
2630d24de9dSSimon Glass        ok, stdout = ApplyPatch(verbose, fname)
2640d24de9dSSimon Glass        if not ok:
2650d24de9dSSimon Glass            print col.Color(col.RED, 'git am returned errors for %s: will '
2660d24de9dSSimon Glass                    'skip this patch' % fname)
2670d24de9dSSimon Glass            if verbose:
2680d24de9dSSimon Glass                print stdout
2690d24de9dSSimon Glass            error_count += 1
2700d24de9dSSimon Glass            cmd = ['git', 'am', '--skip']
2710d24de9dSSimon Glass            pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
2720d24de9dSSimon Glass            stdout, stderr = pipe.communicate()
2730d24de9dSSimon Glass            if pipe.returncode != 0:
2740d24de9dSSimon Glass                print col.Color(col.RED, 'Unable to skip patch! Aborting...')
2750d24de9dSSimon Glass                print stdout
2760d24de9dSSimon Glass                break
2770d24de9dSSimon Glass
2780d24de9dSSimon Glass    # Return to our previous position
2790d24de9dSSimon Glass    cmd = ['git', 'checkout', old_head]
2800d24de9dSSimon Glass    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2810d24de9dSSimon Glass    stdout, stderr = pipe.communicate()
2820d24de9dSSimon Glass    if pipe.returncode:
2830d24de9dSSimon Glass        print col.Color(col.RED, 'Could not move back to head commit')
2840d24de9dSSimon Glass        print stdout, stderr
2850d24de9dSSimon Glass    return error_count == 0
2860d24de9dSSimon Glass
287a1318f7cSSimon Glassdef BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
2880d24de9dSSimon Glass    """Build a list of email addresses based on an input list.
2890d24de9dSSimon Glass
2900d24de9dSSimon Glass    Takes a list of email addresses and aliases, and turns this into a list
2910d24de9dSSimon Glass    of only email address, by resolving any aliases that are present.
2920d24de9dSSimon Glass
2930d24de9dSSimon Glass    If the tag is given, then each email address is prepended with this
2940d24de9dSSimon Glass    tag and a space. If the tag starts with a minus sign (indicating a
2950d24de9dSSimon Glass    command line parameter) then the email address is quoted.
2960d24de9dSSimon Glass
2970d24de9dSSimon Glass    Args:
2980d24de9dSSimon Glass        in_list:        List of aliases/email addresses
2990d24de9dSSimon Glass        tag:            Text to put before each address
300a1318f7cSSimon Glass        alias:          Alias dictionary
301a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
302a1318f7cSSimon Glass                False to just print a message.
3030d24de9dSSimon Glass
3040d24de9dSSimon Glass    Returns:
3050d24de9dSSimon Glass        List of email addresses
3060d24de9dSSimon Glass
3070d24de9dSSimon Glass    >>> alias = {}
3080d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
3090d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
3100d24de9dSSimon Glass    >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
3110d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john']
3120d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
3130d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], None, alias)
3140d24de9dSSimon Glass    ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
3150d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], '--to', alias)
3160d24de9dSSimon Glass    ['--to "j.bloggs@napier.co.nz"', \
3170d24de9dSSimon Glass'--to "Mary Poppins <m.poppins@cloud.net>"']
3180d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
3190d24de9dSSimon Glass    ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
3200d24de9dSSimon Glass    """
3210d24de9dSSimon Glass    quote = '"' if tag and tag[0] == '-' else ''
3220d24de9dSSimon Glass    raw = []
3230d24de9dSSimon Glass    for item in in_list:
324a1318f7cSSimon Glass        raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
3250d24de9dSSimon Glass    result = []
3260d24de9dSSimon Glass    for item in raw:
3270d24de9dSSimon Glass        if not item in result:
3280d24de9dSSimon Glass            result.append(item)
3290d24de9dSSimon Glass    if tag:
3300d24de9dSSimon Glass        return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
3310d24de9dSSimon Glass    return result
3320d24de9dSSimon Glass
333a1318f7cSSimon Glassdef EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
3346d819925SDoug Anderson        self_only=False, alias=None, in_reply_to=None):
3350d24de9dSSimon Glass    """Email a patch series.
3360d24de9dSSimon Glass
3370d24de9dSSimon Glass    Args:
3380d24de9dSSimon Glass        series: Series object containing destination info
3390d24de9dSSimon Glass        cover_fname: filename of cover letter
3400d24de9dSSimon Glass        args: list of filenames of patch files
3410d24de9dSSimon Glass        dry_run: Just return the command that would be run
342a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
343a1318f7cSSimon Glass                False to just print a message.
3440d24de9dSSimon Glass        cc_fname: Filename of Cc file for per-commit Cc
3450d24de9dSSimon Glass        self_only: True to just email to yourself as a test
3466d819925SDoug Anderson        in_reply_to: If set we'll pass this to git as --in-reply-to.
3476d819925SDoug Anderson            Should be a message ID that this is in reply to.
3480d24de9dSSimon Glass
3490d24de9dSSimon Glass    Returns:
3500d24de9dSSimon Glass        Git command that was/would be run
3510d24de9dSSimon Glass
352a970048eSDoug Anderson    # For the duration of this doctest pretend that we ran patman with ./patman
353a970048eSDoug Anderson    >>> _old_argv0 = sys.argv[0]
354a970048eSDoug Anderson    >>> sys.argv[0] = './patman'
355a970048eSDoug Anderson
3560d24de9dSSimon Glass    >>> alias = {}
3570d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
3580d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
3590d24de9dSSimon Glass    >>> alias['mary'] = ['m.poppins@cloud.net']
3600d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john']
3610d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
3620d24de9dSSimon Glass    >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
3630d24de9dSSimon Glass    >>> series = series.Series()
3640d24de9dSSimon Glass    >>> series.to = ['fred']
3650d24de9dSSimon Glass    >>> series.cc = ['mary']
366a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
367a1318f7cSSimon Glass            False, alias)
3680d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3690d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
370a1318f7cSSimon Glass    >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
371a1318f7cSSimon Glass            alias)
3720d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3730d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
3740d24de9dSSimon Glass    >>> series.cc = ['all']
375a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
376a1318f7cSSimon Glass            True, alias)
3770d24de9dSSimon Glass    'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
3780d24de9dSSimon Glass--cc-cmd cc-fname" cover p1 p2'
379a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
380a1318f7cSSimon Glass            False, alias)
3810d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3820d24de9dSSimon Glass"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
3830d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
384a970048eSDoug Anderson
385a970048eSDoug Anderson    # Restore argv[0] since we clobbered it.
386a970048eSDoug Anderson    >>> sys.argv[0] = _old_argv0
3870d24de9dSSimon Glass    """
388a1318f7cSSimon Glass    to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
3890d24de9dSSimon Glass    if not to:
3900d24de9dSSimon Glass        print ("No recipient, please add something like this to a commit\n"
3910d24de9dSSimon Glass            "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
3920d24de9dSSimon Glass        return
393a1318f7cSSimon Glass    cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
3940d24de9dSSimon Glass    if self_only:
395a1318f7cSSimon Glass        to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
3960d24de9dSSimon Glass        cc = []
3970d24de9dSSimon Glass    cmd = ['git', 'send-email', '--annotate']
3986d819925SDoug Anderson    if in_reply_to:
3996d819925SDoug Anderson        cmd.append('--in-reply-to="%s"' % in_reply_to)
4006d819925SDoug Anderson
4010d24de9dSSimon Glass    cmd += to
4020d24de9dSSimon Glass    cmd += cc
4030d24de9dSSimon Glass    cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
4040d24de9dSSimon Glass    if cover_fname:
4050d24de9dSSimon Glass        cmd.append(cover_fname)
4060d24de9dSSimon Glass    cmd += args
4070d24de9dSSimon Glass    str = ' '.join(cmd)
4080d24de9dSSimon Glass    if not dry_run:
4090d24de9dSSimon Glass        os.system(str)
4100d24de9dSSimon Glass    return str
4110d24de9dSSimon Glass
4120d24de9dSSimon Glass
413a1318f7cSSimon Glassdef LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
4140d24de9dSSimon Glass    """If an email address is an alias, look it up and return the full name
4150d24de9dSSimon Glass
4160d24de9dSSimon Glass    TODO: Why not just use git's own alias feature?
4170d24de9dSSimon Glass
4180d24de9dSSimon Glass    Args:
4190d24de9dSSimon Glass        lookup_name: Alias or email address to look up
420a1318f7cSSimon Glass        alias: Dictionary containing aliases (None to use settings default)
421a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
422a1318f7cSSimon Glass                False to just print a message.
4230d24de9dSSimon Glass
4240d24de9dSSimon Glass    Returns:
4250d24de9dSSimon Glass        tuple:
4260d24de9dSSimon Glass            list containing a list of email addresses
4270d24de9dSSimon Glass
4280d24de9dSSimon Glass    Raises:
4290d24de9dSSimon Glass        OSError if a recursive alias reference was found
4300d24de9dSSimon Glass        ValueError if an alias was not found
4310d24de9dSSimon Glass
4320d24de9dSSimon Glass    >>> alias = {}
4330d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
4340d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
4350d24de9dSSimon Glass    >>> alias['mary'] = ['m.poppins@cloud.net']
4360d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
4370d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
4380d24de9dSSimon Glass    >>> alias['loop'] = ['other', 'john', '   mary   ']
4390d24de9dSSimon Glass    >>> alias['other'] = ['loop', 'john', '   mary   ']
4400d24de9dSSimon Glass    >>> LookupEmail('mary', alias)
4410d24de9dSSimon Glass    ['m.poppins@cloud.net']
4420d24de9dSSimon Glass    >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
4430d24de9dSSimon Glass    ['arthur.wellesley@howe.ro.uk']
4440d24de9dSSimon Glass    >>> LookupEmail('boys', alias)
4450d24de9dSSimon Glass    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
4460d24de9dSSimon Glass    >>> LookupEmail('all', alias)
4470d24de9dSSimon Glass    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
4480d24de9dSSimon Glass    >>> LookupEmail('odd', alias)
4490d24de9dSSimon Glass    Traceback (most recent call last):
4500d24de9dSSimon Glass    ...
4510d24de9dSSimon Glass    ValueError: Alias 'odd' not found
4520d24de9dSSimon Glass    >>> LookupEmail('loop', alias)
4530d24de9dSSimon Glass    Traceback (most recent call last):
4540d24de9dSSimon Glass    ...
4550d24de9dSSimon Glass    OSError: Recursive email alias at 'other'
456a1318f7cSSimon Glass    >>> LookupEmail('odd', alias, raise_on_error=False)
457a1318f7cSSimon Glass    \033[1;31mAlias 'odd' not found\033[0m
458a1318f7cSSimon Glass    []
459a1318f7cSSimon Glass    >>> # In this case the loop part will effectively be ignored.
460a1318f7cSSimon Glass    >>> LookupEmail('loop', alias, raise_on_error=False)
461a1318f7cSSimon Glass    \033[1;31mRecursive email alias at 'other'\033[0m
462a1318f7cSSimon Glass    \033[1;31mRecursive email alias at 'john'\033[0m
463a1318f7cSSimon Glass    \033[1;31mRecursive email alias at 'mary'\033[0m
464a1318f7cSSimon Glass    ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
4650d24de9dSSimon Glass    """
4660d24de9dSSimon Glass    if not alias:
4670d24de9dSSimon Glass        alias = settings.alias
4680d24de9dSSimon Glass    lookup_name = lookup_name.strip()
4690d24de9dSSimon Glass    if '@' in lookup_name: # Perhaps a real email address
4700d24de9dSSimon Glass        return [lookup_name]
4710d24de9dSSimon Glass
4720d24de9dSSimon Glass    lookup_name = lookup_name.lower()
473a1318f7cSSimon Glass    col = terminal.Color()
4740d24de9dSSimon Glass
4750d24de9dSSimon Glass    out_list = []
476a1318f7cSSimon Glass    if level > 10:
477a1318f7cSSimon Glass        msg = "Recursive email alias at '%s'" % lookup_name
478a1318f7cSSimon Glass        if raise_on_error:
479a1318f7cSSimon Glass            raise OSError, msg
480a1318f7cSSimon Glass        else:
481a1318f7cSSimon Glass            print col.Color(col.RED, msg)
482a1318f7cSSimon Glass            return out_list
483a1318f7cSSimon Glass
4840d24de9dSSimon Glass    if lookup_name:
4850d24de9dSSimon Glass        if not lookup_name in alias:
486a1318f7cSSimon Glass            msg = "Alias '%s' not found" % lookup_name
487a1318f7cSSimon Glass            if raise_on_error:
488a1318f7cSSimon Glass                raise ValueError, msg
489a1318f7cSSimon Glass            else:
490a1318f7cSSimon Glass                print col.Color(col.RED, msg)
491a1318f7cSSimon Glass                return out_list
4920d24de9dSSimon Glass        for item in alias[lookup_name]:
493a1318f7cSSimon Glass            todo = LookupEmail(item, alias, raise_on_error, level + 1)
4940d24de9dSSimon Glass            for new_item in todo:
4950d24de9dSSimon Glass                if not new_item in out_list:
4960d24de9dSSimon Glass                    out_list.append(new_item)
4970d24de9dSSimon Glass
4980d24de9dSSimon Glass    #print "No match for alias '%s'" % lookup_name
4990d24de9dSSimon Glass    return out_list
5000d24de9dSSimon Glass
5010d24de9dSSimon Glassdef GetTopLevel():
5020d24de9dSSimon Glass    """Return name of top-level directory for this git repo.
5030d24de9dSSimon Glass
5040d24de9dSSimon Glass    Returns:
5050d24de9dSSimon Glass        Full path to git top-level directory
5060d24de9dSSimon Glass
5070d24de9dSSimon Glass    This test makes sure that we are running tests in the right subdir
5080d24de9dSSimon Glass
509a970048eSDoug Anderson    >>> os.path.realpath(os.path.dirname(__file__)) == \
510a970048eSDoug Anderson            os.path.join(GetTopLevel(), 'tools', 'patman')
5110d24de9dSSimon Glass    True
5120d24de9dSSimon Glass    """
5130d24de9dSSimon Glass    return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
5140d24de9dSSimon Glass
5150d24de9dSSimon Glassdef GetAliasFile():
5160d24de9dSSimon Glass    """Gets the name of the git alias file.
5170d24de9dSSimon Glass
5180d24de9dSSimon Glass    Returns:
5190d24de9dSSimon Glass        Filename of git alias file, or None if none
5200d24de9dSSimon Glass    """
521dc191505SSimon Glass    fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
522dc191505SSimon Glass            raise_on_error=False)
5230d24de9dSSimon Glass    if fname:
5240d24de9dSSimon Glass        fname = os.path.join(GetTopLevel(), fname.strip())
5250d24de9dSSimon Glass    return fname
5260d24de9dSSimon Glass
52787d65558SVikram Narayanandef GetDefaultUserName():
52887d65558SVikram Narayanan    """Gets the user.name from .gitconfig file.
52987d65558SVikram Narayanan
53087d65558SVikram Narayanan    Returns:
53187d65558SVikram Narayanan        User name found in .gitconfig file, or None if none
53287d65558SVikram Narayanan    """
53387d65558SVikram Narayanan    uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
53487d65558SVikram Narayanan    return uname
53587d65558SVikram Narayanan
53687d65558SVikram Narayanandef GetDefaultUserEmail():
53787d65558SVikram Narayanan    """Gets the user.email from the global .gitconfig file.
53887d65558SVikram Narayanan
53987d65558SVikram Narayanan    Returns:
54087d65558SVikram Narayanan        User's email found in .gitconfig file, or None if none
54187d65558SVikram Narayanan    """
54287d65558SVikram Narayanan    uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
54387d65558SVikram Narayanan    return uemail
54487d65558SVikram Narayanan
5450d24de9dSSimon Glassdef Setup():
5460d24de9dSSimon Glass    """Set up git utils, by reading the alias files."""
5470d24de9dSSimon Glass    # Check for a git alias file also
5480d24de9dSSimon Glass    alias_fname = GetAliasFile()
5490d24de9dSSimon Glass    if alias_fname:
5500d24de9dSSimon Glass        settings.ReadGitAliases(alias_fname)
5510d24de9dSSimon Glass
5525f6a1c42SSimon Glassdef GetHead():
5535f6a1c42SSimon Glass    """Get the hash of the current HEAD
5545f6a1c42SSimon Glass
5555f6a1c42SSimon Glass    Returns:
5565f6a1c42SSimon Glass        Hash of HEAD
5575f6a1c42SSimon Glass    """
5585f6a1c42SSimon Glass    return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
5595f6a1c42SSimon Glass
5600d24de9dSSimon Glassif __name__ == "__main__":
5610d24de9dSSimon Glass    import doctest
5620d24de9dSSimon Glass
5630d24de9dSSimon Glass    doctest.testmod()
564