xref: /rk3399_rockchip-uboot/tools/patman/gitutil.py (revision e49f14af1349eef94e41b636320bbfcace7403b5)
10d24de9dSSimon Glass# Copyright (c) 2011 The Chromium OS Authors.
20d24de9dSSimon Glass#
31a459660SWolfgang Denk# SPDX-License-Identifier:	GPL-2.0+
40d24de9dSSimon Glass#
50d24de9dSSimon Glass
60d24de9dSSimon Glassimport command
70d24de9dSSimon Glassimport re
80d24de9dSSimon Glassimport os
90d24de9dSSimon Glassimport series
100d24de9dSSimon Glassimport subprocess
110d24de9dSSimon Glassimport sys
120d24de9dSSimon Glassimport terminal
130d24de9dSSimon Glass
14757f64a8SSimon Glassimport checkpatch
155f6a1c42SSimon Glassimport settings
165f6a1c42SSimon Glass
17*e49f14afSSimon Glass# True to use --no-decorate - we check this in Setup()
18*e49f14afSSimon Glassuse_no_decorate = True
19*e49f14afSSimon Glass
20cda2a611SSimon Glassdef LogCmd(commit_range, git_dir=None, oneline=False, reverse=False,
21cda2a611SSimon Glass           count=None):
22cda2a611SSimon Glass    """Create a command to perform a 'git log'
23cda2a611SSimon Glass
24cda2a611SSimon Glass    Args:
25cda2a611SSimon Glass        commit_range: Range expression to use for log, None for none
26cda2a611SSimon Glass        git_dir: Path to git repositiory (None to use default)
27cda2a611SSimon Glass        oneline: True to use --oneline, else False
28cda2a611SSimon Glass        reverse: True to reverse the log (--reverse)
29cda2a611SSimon Glass        count: Number of commits to list, or None for no limit
30cda2a611SSimon Glass    Return:
31cda2a611SSimon Glass        List containing command and arguments to run
32cda2a611SSimon Glass    """
33cda2a611SSimon Glass    cmd = ['git']
34cda2a611SSimon Glass    if git_dir:
35cda2a611SSimon Glass        cmd += ['--git-dir', git_dir]
36cda2a611SSimon Glass    cmd += ['log', '--no-color']
37cda2a611SSimon Glass    if oneline:
38cda2a611SSimon Glass        cmd.append('--oneline')
39*e49f14afSSimon Glass    if use_no_decorate:
40cda2a611SSimon Glass        cmd.append('--no-decorate')
41cda2a611SSimon Glass    if count is not None:
42cda2a611SSimon Glass        cmd.append('-n%d' % count)
43cda2a611SSimon Glass    if commit_range:
44cda2a611SSimon Glass        cmd.append(commit_range)
45cda2a611SSimon Glass    return cmd
460d24de9dSSimon Glass
470d24de9dSSimon Glassdef CountCommitsToBranch():
480d24de9dSSimon Glass    """Returns number of commits between HEAD and the tracking branch.
490d24de9dSSimon Glass
500d24de9dSSimon Glass    This looks back to the tracking branch and works out the number of commits
510d24de9dSSimon Glass    since then.
520d24de9dSSimon Glass
530d24de9dSSimon Glass    Return:
540d24de9dSSimon Glass        Number of patches that exist on top of the branch
550d24de9dSSimon Glass    """
56cda2a611SSimon Glass    pipe = [LogCmd('@{upstream}..', oneline=True),
570d24de9dSSimon Glass            ['wc', '-l']]
58a10fd93cSSimon Glass    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
590d24de9dSSimon Glass    patch_count = int(stdout)
600d24de9dSSimon Glass    return patch_count
610d24de9dSSimon Glass
625f6a1c42SSimon Glassdef GetUpstream(git_dir, branch):
635f6a1c42SSimon Glass    """Returns the name of the upstream for a branch
645f6a1c42SSimon Glass
655f6a1c42SSimon Glass    Args:
665f6a1c42SSimon Glass        git_dir: Git directory containing repo
675f6a1c42SSimon Glass        branch: Name of branch
685f6a1c42SSimon Glass
695f6a1c42SSimon Glass    Returns:
705f6a1c42SSimon Glass        Name of upstream branch (e.g. 'upstream/master') or None if none
715f6a1c42SSimon Glass    """
72cce717a9SSimon Glass    try:
735f6a1c42SSimon Glass        remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
745f6a1c42SSimon Glass                                       'branch.%s.remote' % branch)
755f6a1c42SSimon Glass        merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
765f6a1c42SSimon Glass                                      'branch.%s.merge' % branch)
77cce717a9SSimon Glass    except:
78cce717a9SSimon Glass        return None
79cce717a9SSimon Glass
805f6a1c42SSimon Glass    if remote == '.':
815f6a1c42SSimon Glass        return merge
825f6a1c42SSimon Glass    elif remote and merge:
835f6a1c42SSimon Glass        leaf = merge.split('/')[-1]
845f6a1c42SSimon Glass        return '%s/%s' % (remote, leaf)
855f6a1c42SSimon Glass    else:
865f6a1c42SSimon Glass        raise ValueError, ("Cannot determine upstream branch for branch "
875f6a1c42SSimon Glass                "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
885f6a1c42SSimon Glass
895f6a1c42SSimon Glass
905f6a1c42SSimon Glassdef GetRangeInBranch(git_dir, branch, include_upstream=False):
915f6a1c42SSimon Glass    """Returns an expression for the commits in the given branch.
925f6a1c42SSimon Glass
935f6a1c42SSimon Glass    Args:
945f6a1c42SSimon Glass        git_dir: Directory containing git repo
955f6a1c42SSimon Glass        branch: Name of branch
965f6a1c42SSimon Glass    Return:
975f6a1c42SSimon Glass        Expression in the form 'upstream..branch' which can be used to
98cce717a9SSimon Glass        access the commits. If the branch does not exist, returns None.
995f6a1c42SSimon Glass    """
1005f6a1c42SSimon Glass    upstream = GetUpstream(git_dir, branch)
101cce717a9SSimon Glass    if not upstream:
102cce717a9SSimon Glass        return None
1035f6a1c42SSimon Glass    return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
1045f6a1c42SSimon Glass
1055f6a1c42SSimon Glassdef CountCommitsInBranch(git_dir, branch, include_upstream=False):
1065f6a1c42SSimon Glass    """Returns the number of commits in the given branch.
1075f6a1c42SSimon Glass
1085f6a1c42SSimon Glass    Args:
1095f6a1c42SSimon Glass        git_dir: Directory containing git repo
1105f6a1c42SSimon Glass        branch: Name of branch
1115f6a1c42SSimon Glass    Return:
112cce717a9SSimon Glass        Number of patches that exist on top of the branch, or None if the
113cce717a9SSimon Glass        branch does not exist.
1145f6a1c42SSimon Glass    """
1155f6a1c42SSimon Glass    range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
116cce717a9SSimon Glass    if not range_expr:
117cce717a9SSimon Glass        return None
118cda2a611SSimon Glass    pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True),
1195f6a1c42SSimon Glass            ['wc', '-l']]
1205f6a1c42SSimon Glass    result = command.RunPipe(pipe, capture=True, oneline=True)
1215f6a1c42SSimon Glass    patch_count = int(result.stdout)
1225f6a1c42SSimon Glass    return patch_count
1235f6a1c42SSimon Glass
1245f6a1c42SSimon Glassdef CountCommits(commit_range):
1255f6a1c42SSimon Glass    """Returns the number of commits in the given range.
1265f6a1c42SSimon Glass
1275f6a1c42SSimon Glass    Args:
1285f6a1c42SSimon Glass        commit_range: Range of commits to count (e.g. 'HEAD..base')
1295f6a1c42SSimon Glass    Return:
1305f6a1c42SSimon Glass        Number of patches that exist on top of the branch
1315f6a1c42SSimon Glass    """
132cda2a611SSimon Glass    pipe = [LogCmd(commit_range, oneline=True),
1335f6a1c42SSimon Glass            ['wc', '-l']]
1345f6a1c42SSimon Glass    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
1355f6a1c42SSimon Glass    patch_count = int(stdout)
1365f6a1c42SSimon Glass    return patch_count
1375f6a1c42SSimon Glass
1385f6a1c42SSimon Glassdef Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
1395f6a1c42SSimon Glass    """Checkout the selected commit for this build
1405f6a1c42SSimon Glass
1415f6a1c42SSimon Glass    Args:
1425f6a1c42SSimon Glass        commit_hash: Commit hash to check out
1435f6a1c42SSimon Glass    """
1445f6a1c42SSimon Glass    pipe = ['git']
1455f6a1c42SSimon Glass    if git_dir:
1465f6a1c42SSimon Glass        pipe.extend(['--git-dir', git_dir])
1475f6a1c42SSimon Glass    if work_tree:
1485f6a1c42SSimon Glass        pipe.extend(['--work-tree', work_tree])
1495f6a1c42SSimon Glass    pipe.append('checkout')
1505f6a1c42SSimon Glass    if force:
1515f6a1c42SSimon Glass        pipe.append('-f')
1525f6a1c42SSimon Glass    pipe.append(commit_hash)
1535f6a1c42SSimon Glass    result = command.RunPipe([pipe], capture=True, raise_on_error=False)
1545f6a1c42SSimon Glass    if result.return_code != 0:
1555f6a1c42SSimon Glass        raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
1565f6a1c42SSimon Glass
1575f6a1c42SSimon Glassdef Clone(git_dir, output_dir):
1585f6a1c42SSimon Glass    """Checkout the selected commit for this build
1595f6a1c42SSimon Glass
1605f6a1c42SSimon Glass    Args:
1615f6a1c42SSimon Glass        commit_hash: Commit hash to check out
1625f6a1c42SSimon Glass    """
1635f6a1c42SSimon Glass    pipe = ['git', 'clone', git_dir, '.']
1645f6a1c42SSimon Glass    result = command.RunPipe([pipe], capture=True, cwd=output_dir)
1655f6a1c42SSimon Glass    if result.return_code != 0:
1665f6a1c42SSimon Glass        raise OSError, 'git clone: %s' % result.stderr
1675f6a1c42SSimon Glass
1685f6a1c42SSimon Glassdef Fetch(git_dir=None, work_tree=None):
1695f6a1c42SSimon Glass    """Fetch from the origin repo
1705f6a1c42SSimon Glass
1715f6a1c42SSimon Glass    Args:
1725f6a1c42SSimon Glass        commit_hash: Commit hash to check out
1735f6a1c42SSimon Glass    """
1745f6a1c42SSimon Glass    pipe = ['git']
1755f6a1c42SSimon Glass    if git_dir:
1765f6a1c42SSimon Glass        pipe.extend(['--git-dir', git_dir])
1775f6a1c42SSimon Glass    if work_tree:
1785f6a1c42SSimon Glass        pipe.extend(['--work-tree', work_tree])
1795f6a1c42SSimon Glass    pipe.append('fetch')
1805f6a1c42SSimon Glass    result = command.RunPipe([pipe], capture=True)
1815f6a1c42SSimon Glass    if result.return_code != 0:
1825f6a1c42SSimon Glass        raise OSError, 'git fetch: %s' % result.stderr
1835f6a1c42SSimon Glass
1840d24de9dSSimon Glassdef CreatePatches(start, count, series):
1850d24de9dSSimon Glass    """Create a series of patches from the top of the current branch.
1860d24de9dSSimon Glass
1870d24de9dSSimon Glass    The patch files are written to the current directory using
1880d24de9dSSimon Glass    git format-patch.
1890d24de9dSSimon Glass
1900d24de9dSSimon Glass    Args:
1910d24de9dSSimon Glass        start: Commit to start from: 0=HEAD, 1=next one, etc.
1920d24de9dSSimon Glass        count: number of commits to include
1930d24de9dSSimon Glass    Return:
1940d24de9dSSimon Glass        Filename of cover letter
1950d24de9dSSimon Glass        List of filenames of patch files
1960d24de9dSSimon Glass    """
1970d24de9dSSimon Glass    if series.get('version'):
1980d24de9dSSimon Glass        version = '%s ' % series['version']
1990d24de9dSSimon Glass    cmd = ['git', 'format-patch', '-M', '--signoff']
2000d24de9dSSimon Glass    if series.get('cover'):
2010d24de9dSSimon Glass        cmd.append('--cover-letter')
2020d24de9dSSimon Glass    prefix = series.GetPatchPrefix()
2030d24de9dSSimon Glass    if prefix:
2040d24de9dSSimon Glass        cmd += ['--subject-prefix=%s' % prefix]
2050d24de9dSSimon Glass    cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
2060d24de9dSSimon Glass
2070d24de9dSSimon Glass    stdout = command.RunList(cmd)
2080d24de9dSSimon Glass    files = stdout.splitlines()
2090d24de9dSSimon Glass
2100d24de9dSSimon Glass    # We have an extra file if there is a cover letter
2110d24de9dSSimon Glass    if series.get('cover'):
2120d24de9dSSimon Glass       return files[0], files[1:]
2130d24de9dSSimon Glass    else:
2140d24de9dSSimon Glass       return None, files
2150d24de9dSSimon Glass
2160d24de9dSSimon Glassdef ApplyPatch(verbose, fname):
2170d24de9dSSimon Glass    """Apply a patch with git am to test it
2180d24de9dSSimon Glass
2190d24de9dSSimon Glass    TODO: Convert these to use command, with stderr option
2200d24de9dSSimon Glass
2210d24de9dSSimon Glass    Args:
2220d24de9dSSimon Glass        fname: filename of patch file to apply
2230d24de9dSSimon Glass    """
224757f64a8SSimon Glass    col = terminal.Color()
2250d24de9dSSimon Glass    cmd = ['git', 'am', fname]
2260d24de9dSSimon Glass    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
2270d24de9dSSimon Glass            stderr=subprocess.PIPE)
2280d24de9dSSimon Glass    stdout, stderr = pipe.communicate()
2290d24de9dSSimon Glass    re_error = re.compile('^error: patch failed: (.+):(\d+)')
2300d24de9dSSimon Glass    for line in stderr.splitlines():
2310d24de9dSSimon Glass        if verbose:
2320d24de9dSSimon Glass            print line
2330d24de9dSSimon Glass        match = re_error.match(line)
2340d24de9dSSimon Glass        if match:
235757f64a8SSimon Glass            print checkpatch.GetWarningMsg(col, 'warning', match.group(1),
236757f64a8SSimon Glass                                           int(match.group(2)), 'Patch failed')
2370d24de9dSSimon Glass    return pipe.returncode == 0, stdout
2380d24de9dSSimon Glass
2390d24de9dSSimon Glassdef ApplyPatches(verbose, args, start_point):
2400d24de9dSSimon Glass    """Apply the patches with git am to make sure all is well
2410d24de9dSSimon Glass
2420d24de9dSSimon Glass    Args:
2430d24de9dSSimon Glass        verbose: Print out 'git am' output verbatim
2440d24de9dSSimon Glass        args: List of patch files to apply
2450d24de9dSSimon Glass        start_point: Number of commits back from HEAD to start applying.
2460d24de9dSSimon Glass            Normally this is len(args), but it can be larger if a start
2470d24de9dSSimon Glass            offset was given.
2480d24de9dSSimon Glass    """
2490d24de9dSSimon Glass    error_count = 0
2500d24de9dSSimon Glass    col = terminal.Color()
2510d24de9dSSimon Glass
2520d24de9dSSimon Glass    # Figure out our current position
2530d24de9dSSimon Glass    cmd = ['git', 'name-rev', 'HEAD', '--name-only']
2540d24de9dSSimon Glass    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
2550d24de9dSSimon Glass    stdout, stderr = pipe.communicate()
2560d24de9dSSimon Glass    if pipe.returncode:
2570d24de9dSSimon Glass        str = 'Could not find current commit name'
2580d24de9dSSimon Glass        print col.Color(col.RED, str)
2590d24de9dSSimon Glass        print stdout
2600d24de9dSSimon Glass        return False
2610d24de9dSSimon Glass    old_head = stdout.splitlines()[0]
2624251978aSSimon Glass    if old_head == 'undefined':
2634251978aSSimon Glass        str = "Invalid HEAD '%s'" % stdout.strip()
2644251978aSSimon Glass        print col.Color(col.RED, str)
2654251978aSSimon Glass        return False
2660d24de9dSSimon Glass
2670d24de9dSSimon Glass    # Checkout the required start point
2680d24de9dSSimon Glass    cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
2690d24de9dSSimon Glass    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
2700d24de9dSSimon Glass            stderr=subprocess.PIPE)
2710d24de9dSSimon Glass    stdout, stderr = pipe.communicate()
2720d24de9dSSimon Glass    if pipe.returncode:
2730d24de9dSSimon Glass        str = 'Could not move to commit before patch series'
2740d24de9dSSimon Glass        print col.Color(col.RED, str)
2750d24de9dSSimon Glass        print stdout, stderr
2760d24de9dSSimon Glass        return False
2770d24de9dSSimon Glass
2780d24de9dSSimon Glass    # Apply all the patches
2790d24de9dSSimon Glass    for fname in args:
2800d24de9dSSimon Glass        ok, stdout = ApplyPatch(verbose, fname)
2810d24de9dSSimon Glass        if not ok:
2820d24de9dSSimon Glass            print col.Color(col.RED, 'git am returned errors for %s: will '
2830d24de9dSSimon Glass                    'skip this patch' % fname)
2840d24de9dSSimon Glass            if verbose:
2850d24de9dSSimon Glass                print stdout
2860d24de9dSSimon Glass            error_count += 1
2870d24de9dSSimon Glass            cmd = ['git', 'am', '--skip']
2880d24de9dSSimon Glass            pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
2890d24de9dSSimon Glass            stdout, stderr = pipe.communicate()
2900d24de9dSSimon Glass            if pipe.returncode != 0:
2910d24de9dSSimon Glass                print col.Color(col.RED, 'Unable to skip patch! Aborting...')
2920d24de9dSSimon Glass                print stdout
2930d24de9dSSimon Glass                break
2940d24de9dSSimon Glass
2950d24de9dSSimon Glass    # Return to our previous position
2960d24de9dSSimon Glass    cmd = ['git', 'checkout', old_head]
2970d24de9dSSimon Glass    pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2980d24de9dSSimon Glass    stdout, stderr = pipe.communicate()
2990d24de9dSSimon Glass    if pipe.returncode:
3000d24de9dSSimon Glass        print col.Color(col.RED, 'Could not move back to head commit')
3010d24de9dSSimon Glass        print stdout, stderr
3020d24de9dSSimon Glass    return error_count == 0
3030d24de9dSSimon Glass
304a1318f7cSSimon Glassdef BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
3050d24de9dSSimon Glass    """Build a list of email addresses based on an input list.
3060d24de9dSSimon Glass
3070d24de9dSSimon Glass    Takes a list of email addresses and aliases, and turns this into a list
3080d24de9dSSimon Glass    of only email address, by resolving any aliases that are present.
3090d24de9dSSimon Glass
3100d24de9dSSimon Glass    If the tag is given, then each email address is prepended with this
3110d24de9dSSimon Glass    tag and a space. If the tag starts with a minus sign (indicating a
3120d24de9dSSimon Glass    command line parameter) then the email address is quoted.
3130d24de9dSSimon Glass
3140d24de9dSSimon Glass    Args:
3150d24de9dSSimon Glass        in_list:        List of aliases/email addresses
3160d24de9dSSimon Glass        tag:            Text to put before each address
317a1318f7cSSimon Glass        alias:          Alias dictionary
318a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
319a1318f7cSSimon Glass                False to just print a message.
3200d24de9dSSimon Glass
3210d24de9dSSimon Glass    Returns:
3220d24de9dSSimon Glass        List of email addresses
3230d24de9dSSimon Glass
3240d24de9dSSimon Glass    >>> alias = {}
3250d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
3260d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
3270d24de9dSSimon Glass    >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
3280d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john']
3290d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
3300d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], None, alias)
3310d24de9dSSimon Glass    ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
3320d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], '--to', alias)
3330d24de9dSSimon Glass    ['--to "j.bloggs@napier.co.nz"', \
3340d24de9dSSimon Glass'--to "Mary Poppins <m.poppins@cloud.net>"']
3350d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
3360d24de9dSSimon Glass    ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
3370d24de9dSSimon Glass    """
3380d24de9dSSimon Glass    quote = '"' if tag and tag[0] == '-' else ''
3390d24de9dSSimon Glass    raw = []
3400d24de9dSSimon Glass    for item in in_list:
341a1318f7cSSimon Glass        raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
3420d24de9dSSimon Glass    result = []
3430d24de9dSSimon Glass    for item in raw:
3440d24de9dSSimon Glass        if not item in result:
3450d24de9dSSimon Glass            result.append(item)
3460d24de9dSSimon Glass    if tag:
3470d24de9dSSimon Glass        return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
3480d24de9dSSimon Glass    return result
3490d24de9dSSimon Glass
350a1318f7cSSimon Glassdef EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
3516d819925SDoug Anderson        self_only=False, alias=None, in_reply_to=None):
3520d24de9dSSimon Glass    """Email a patch series.
3530d24de9dSSimon Glass
3540d24de9dSSimon Glass    Args:
3550d24de9dSSimon Glass        series: Series object containing destination info
3560d24de9dSSimon Glass        cover_fname: filename of cover letter
3570d24de9dSSimon Glass        args: list of filenames of patch files
3580d24de9dSSimon Glass        dry_run: Just return the command that would be run
359a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
360a1318f7cSSimon Glass                False to just print a message.
3610d24de9dSSimon Glass        cc_fname: Filename of Cc file for per-commit Cc
3620d24de9dSSimon Glass        self_only: True to just email to yourself as a test
3636d819925SDoug Anderson        in_reply_to: If set we'll pass this to git as --in-reply-to.
3646d819925SDoug Anderson            Should be a message ID that this is in reply to.
3650d24de9dSSimon Glass
3660d24de9dSSimon Glass    Returns:
3670d24de9dSSimon Glass        Git command that was/would be run
3680d24de9dSSimon Glass
369a970048eSDoug Anderson    # For the duration of this doctest pretend that we ran patman with ./patman
370a970048eSDoug Anderson    >>> _old_argv0 = sys.argv[0]
371a970048eSDoug Anderson    >>> sys.argv[0] = './patman'
372a970048eSDoug Anderson
3730d24de9dSSimon Glass    >>> alias = {}
3740d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
3750d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
3760d24de9dSSimon Glass    >>> alias['mary'] = ['m.poppins@cloud.net']
3770d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john']
3780d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
3790d24de9dSSimon Glass    >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
3800d24de9dSSimon Glass    >>> series = series.Series()
3810d24de9dSSimon Glass    >>> series.to = ['fred']
3820d24de9dSSimon Glass    >>> series.cc = ['mary']
383a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
384a1318f7cSSimon Glass            False, alias)
3850d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3860d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
387a1318f7cSSimon Glass    >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
388a1318f7cSSimon Glass            alias)
3890d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3900d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
3910d24de9dSSimon Glass    >>> series.cc = ['all']
392a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
393a1318f7cSSimon Glass            True, alias)
3940d24de9dSSimon Glass    'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
3950d24de9dSSimon Glass--cc-cmd cc-fname" cover p1 p2'
396a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
397a1318f7cSSimon Glass            False, alias)
3980d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3990d24de9dSSimon Glass"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
4000d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
401a970048eSDoug Anderson
402a970048eSDoug Anderson    # Restore argv[0] since we clobbered it.
403a970048eSDoug Anderson    >>> sys.argv[0] = _old_argv0
4040d24de9dSSimon Glass    """
405a1318f7cSSimon Glass    to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
4060d24de9dSSimon Glass    if not to:
407ee860c60SMasahiro Yamada        git_config_to = command.Output('git', 'config', 'sendemail.to')
408ee860c60SMasahiro Yamada        if not git_config_to:
409ee860c60SMasahiro Yamada            print ("No recipient.\n"
410ee860c60SMasahiro Yamada                   "Please add something like this to a commit\n"
411ee860c60SMasahiro Yamada                   "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
412ee860c60SMasahiro Yamada                   "Or do something like this\n"
413ee860c60SMasahiro Yamada                   "git config sendemail.to u-boot@lists.denx.de")
4140d24de9dSSimon Glass            return
415a1318f7cSSimon Glass    cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
4160d24de9dSSimon Glass    if self_only:
417a1318f7cSSimon Glass        to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
4180d24de9dSSimon Glass        cc = []
4190d24de9dSSimon Glass    cmd = ['git', 'send-email', '--annotate']
4206d819925SDoug Anderson    if in_reply_to:
4216d819925SDoug Anderson        cmd.append('--in-reply-to="%s"' % in_reply_to)
4226d819925SDoug Anderson
4230d24de9dSSimon Glass    cmd += to
4240d24de9dSSimon Glass    cmd += cc
4250d24de9dSSimon Glass    cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
4260d24de9dSSimon Glass    if cover_fname:
4270d24de9dSSimon Glass        cmd.append(cover_fname)
4280d24de9dSSimon Glass    cmd += args
4290d24de9dSSimon Glass    str = ' '.join(cmd)
4300d24de9dSSimon Glass    if not dry_run:
4310d24de9dSSimon Glass        os.system(str)
4320d24de9dSSimon Glass    return str
4330d24de9dSSimon Glass
4340d24de9dSSimon Glass
435a1318f7cSSimon Glassdef LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
4360d24de9dSSimon Glass    """If an email address is an alias, look it up and return the full name
4370d24de9dSSimon Glass
4380d24de9dSSimon Glass    TODO: Why not just use git's own alias feature?
4390d24de9dSSimon Glass
4400d24de9dSSimon Glass    Args:
4410d24de9dSSimon Glass        lookup_name: Alias or email address to look up
442a1318f7cSSimon Glass        alias: Dictionary containing aliases (None to use settings default)
443a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
444a1318f7cSSimon Glass                False to just print a message.
4450d24de9dSSimon Glass
4460d24de9dSSimon Glass    Returns:
4470d24de9dSSimon Glass        tuple:
4480d24de9dSSimon Glass            list containing a list of email addresses
4490d24de9dSSimon Glass
4500d24de9dSSimon Glass    Raises:
4510d24de9dSSimon Glass        OSError if a recursive alias reference was found
4520d24de9dSSimon Glass        ValueError if an alias was not found
4530d24de9dSSimon Glass
4540d24de9dSSimon Glass    >>> alias = {}
4550d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
4560d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
4570d24de9dSSimon Glass    >>> alias['mary'] = ['m.poppins@cloud.net']
4580d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
4590d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
4600d24de9dSSimon Glass    >>> alias['loop'] = ['other', 'john', '   mary   ']
4610d24de9dSSimon Glass    >>> alias['other'] = ['loop', 'john', '   mary   ']
4620d24de9dSSimon Glass    >>> LookupEmail('mary', alias)
4630d24de9dSSimon Glass    ['m.poppins@cloud.net']
4640d24de9dSSimon Glass    >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
4650d24de9dSSimon Glass    ['arthur.wellesley@howe.ro.uk']
4660d24de9dSSimon Glass    >>> LookupEmail('boys', alias)
4670d24de9dSSimon Glass    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
4680d24de9dSSimon Glass    >>> LookupEmail('all', alias)
4690d24de9dSSimon Glass    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
4700d24de9dSSimon Glass    >>> LookupEmail('odd', alias)
4710d24de9dSSimon Glass    Traceback (most recent call last):
4720d24de9dSSimon Glass    ...
4730d24de9dSSimon Glass    ValueError: Alias 'odd' not found
4740d24de9dSSimon Glass    >>> LookupEmail('loop', alias)
4750d24de9dSSimon Glass    Traceback (most recent call last):
4760d24de9dSSimon Glass    ...
4770d24de9dSSimon Glass    OSError: Recursive email alias at 'other'
478a1318f7cSSimon Glass    >>> LookupEmail('odd', alias, raise_on_error=False)
479a1318f7cSSimon Glass    \033[1;31mAlias 'odd' not found\033[0m
480a1318f7cSSimon Glass    []
481a1318f7cSSimon Glass    >>> # In this case the loop part will effectively be ignored.
482a1318f7cSSimon Glass    >>> LookupEmail('loop', alias, raise_on_error=False)
483a1318f7cSSimon Glass    \033[1;31mRecursive email alias at 'other'\033[0m
484a1318f7cSSimon Glass    \033[1;31mRecursive email alias at 'john'\033[0m
485a1318f7cSSimon Glass    \033[1;31mRecursive email alias at 'mary'\033[0m
486a1318f7cSSimon Glass    ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
4870d24de9dSSimon Glass    """
4880d24de9dSSimon Glass    if not alias:
4890d24de9dSSimon Glass        alias = settings.alias
4900d24de9dSSimon Glass    lookup_name = lookup_name.strip()
4910d24de9dSSimon Glass    if '@' in lookup_name: # Perhaps a real email address
4920d24de9dSSimon Glass        return [lookup_name]
4930d24de9dSSimon Glass
4940d24de9dSSimon Glass    lookup_name = lookup_name.lower()
495a1318f7cSSimon Glass    col = terminal.Color()
4960d24de9dSSimon Glass
4970d24de9dSSimon Glass    out_list = []
498a1318f7cSSimon Glass    if level > 10:
499a1318f7cSSimon Glass        msg = "Recursive email alias at '%s'" % lookup_name
500a1318f7cSSimon Glass        if raise_on_error:
501a1318f7cSSimon Glass            raise OSError, msg
502a1318f7cSSimon Glass        else:
503a1318f7cSSimon Glass            print col.Color(col.RED, msg)
504a1318f7cSSimon Glass            return out_list
505a1318f7cSSimon Glass
5060d24de9dSSimon Glass    if lookup_name:
5070d24de9dSSimon Glass        if not lookup_name in alias:
508a1318f7cSSimon Glass            msg = "Alias '%s' not found" % lookup_name
509a1318f7cSSimon Glass            if raise_on_error:
510a1318f7cSSimon Glass                raise ValueError, msg
511a1318f7cSSimon Glass            else:
512a1318f7cSSimon Glass                print col.Color(col.RED, msg)
513a1318f7cSSimon Glass                return out_list
5140d24de9dSSimon Glass        for item in alias[lookup_name]:
515a1318f7cSSimon Glass            todo = LookupEmail(item, alias, raise_on_error, level + 1)
5160d24de9dSSimon Glass            for new_item in todo:
5170d24de9dSSimon Glass                if not new_item in out_list:
5180d24de9dSSimon Glass                    out_list.append(new_item)
5190d24de9dSSimon Glass
5200d24de9dSSimon Glass    #print "No match for alias '%s'" % lookup_name
5210d24de9dSSimon Glass    return out_list
5220d24de9dSSimon Glass
5230d24de9dSSimon Glassdef GetTopLevel():
5240d24de9dSSimon Glass    """Return name of top-level directory for this git repo.
5250d24de9dSSimon Glass
5260d24de9dSSimon Glass    Returns:
5270d24de9dSSimon Glass        Full path to git top-level directory
5280d24de9dSSimon Glass
5290d24de9dSSimon Glass    This test makes sure that we are running tests in the right subdir
5300d24de9dSSimon Glass
531a970048eSDoug Anderson    >>> os.path.realpath(os.path.dirname(__file__)) == \
532a970048eSDoug Anderson            os.path.join(GetTopLevel(), 'tools', 'patman')
5330d24de9dSSimon Glass    True
5340d24de9dSSimon Glass    """
5350d24de9dSSimon Glass    return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
5360d24de9dSSimon Glass
5370d24de9dSSimon Glassdef GetAliasFile():
5380d24de9dSSimon Glass    """Gets the name of the git alias file.
5390d24de9dSSimon Glass
5400d24de9dSSimon Glass    Returns:
5410d24de9dSSimon Glass        Filename of git alias file, or None if none
5420d24de9dSSimon Glass    """
543dc191505SSimon Glass    fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
544dc191505SSimon Glass            raise_on_error=False)
5450d24de9dSSimon Glass    if fname:
5460d24de9dSSimon Glass        fname = os.path.join(GetTopLevel(), fname.strip())
5470d24de9dSSimon Glass    return fname
5480d24de9dSSimon Glass
54987d65558SVikram Narayanandef GetDefaultUserName():
55087d65558SVikram Narayanan    """Gets the user.name from .gitconfig file.
55187d65558SVikram Narayanan
55287d65558SVikram Narayanan    Returns:
55387d65558SVikram Narayanan        User name found in .gitconfig file, or None if none
55487d65558SVikram Narayanan    """
55587d65558SVikram Narayanan    uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
55687d65558SVikram Narayanan    return uname
55787d65558SVikram Narayanan
55887d65558SVikram Narayanandef GetDefaultUserEmail():
55987d65558SVikram Narayanan    """Gets the user.email from the global .gitconfig file.
56087d65558SVikram Narayanan
56187d65558SVikram Narayanan    Returns:
56287d65558SVikram Narayanan        User's email found in .gitconfig file, or None if none
56387d65558SVikram Narayanan    """
56487d65558SVikram Narayanan    uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
56587d65558SVikram Narayanan    return uemail
56687d65558SVikram Narayanan
5670d24de9dSSimon Glassdef Setup():
5680d24de9dSSimon Glass    """Set up git utils, by reading the alias files."""
5690d24de9dSSimon Glass    # Check for a git alias file also
5700d24de9dSSimon Glass    alias_fname = GetAliasFile()
5710d24de9dSSimon Glass    if alias_fname:
5720d24de9dSSimon Glass        settings.ReadGitAliases(alias_fname)
573*e49f14afSSimon Glass    cmd = LogCmd(None, count=0)
574*e49f14afSSimon Glass    use_no_decorate = (command.RunPipe([cmd], raise_on_error=False)
575*e49f14afSSimon Glass                       .return_code == 0)
5760d24de9dSSimon Glass
5775f6a1c42SSimon Glassdef GetHead():
5785f6a1c42SSimon Glass    """Get the hash of the current HEAD
5795f6a1c42SSimon Glass
5805f6a1c42SSimon Glass    Returns:
5815f6a1c42SSimon Glass        Hash of HEAD
5825f6a1c42SSimon Glass    """
5835f6a1c42SSimon Glass    return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
5845f6a1c42SSimon Glass
5850d24de9dSSimon Glassif __name__ == "__main__":
5860d24de9dSSimon Glass    import doctest
5870d24de9dSSimon Glass
5880d24de9dSSimon Glass    doctest.testmod()
589