xref: /rk3399_rockchip-uboot/tools/patman/gitutil.py (revision 27067a46c56cdaa0f8a2ef433f0aed4376834c44)
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
17e49f14afSSimon Glass# True to use --no-decorate - we check this in Setup()
18e49f14afSSimon Glassuse_no_decorate = True
19e49f14afSSimon 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]
369447a6b2SSimon Glass    cmd += ['--no-pager', 'log', '--no-color']
37cda2a611SSimon Glass    if oneline:
38cda2a611SSimon Glass        cmd.append('--oneline')
39e49f14afSSimon Glass    if use_no_decorate:
40cda2a611SSimon Glass        cmd.append('--no-decorate')
41042a732cSSimon Glass    if reverse:
42042a732cSSimon Glass        cmd.append('--reverse')
43cda2a611SSimon Glass    if count is not None:
44cda2a611SSimon Glass        cmd.append('-n%d' % count)
45cda2a611SSimon Glass    if commit_range:
46cda2a611SSimon Glass        cmd.append(commit_range)
47cda2a611SSimon Glass    return cmd
480d24de9dSSimon Glass
490d24de9dSSimon Glassdef CountCommitsToBranch():
500d24de9dSSimon Glass    """Returns number of commits between HEAD and the tracking branch.
510d24de9dSSimon Glass
520d24de9dSSimon Glass    This looks back to the tracking branch and works out the number of commits
530d24de9dSSimon Glass    since then.
540d24de9dSSimon Glass
550d24de9dSSimon Glass    Return:
560d24de9dSSimon Glass        Number of patches that exist on top of the branch
570d24de9dSSimon Glass    """
58cda2a611SSimon Glass    pipe = [LogCmd('@{upstream}..', oneline=True),
590d24de9dSSimon Glass            ['wc', '-l']]
60a10fd93cSSimon Glass    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
610d24de9dSSimon Glass    patch_count = int(stdout)
620d24de9dSSimon Glass    return patch_count
630d24de9dSSimon Glass
642a9e2c6aSSimon Glassdef NameRevision(commit_hash):
652a9e2c6aSSimon Glass    """Gets the revision name for a commit
662a9e2c6aSSimon Glass
672a9e2c6aSSimon Glass    Args:
682a9e2c6aSSimon Glass        commit_hash: Commit hash to look up
692a9e2c6aSSimon Glass
702a9e2c6aSSimon Glass    Return:
712a9e2c6aSSimon Glass        Name of revision, if any, else None
722a9e2c6aSSimon Glass    """
732a9e2c6aSSimon Glass    pipe = ['git', 'name-rev', commit_hash]
742a9e2c6aSSimon Glass    stdout = command.RunPipe([pipe], capture=True, oneline=True).stdout
752a9e2c6aSSimon Glass
762a9e2c6aSSimon Glass    # We expect a commit, a space, then a revision name
772a9e2c6aSSimon Glass    name = stdout.split(' ')[1].strip()
782a9e2c6aSSimon Glass    return name
792a9e2c6aSSimon Glass
802a9e2c6aSSimon Glassdef GuessUpstream(git_dir, branch):
812a9e2c6aSSimon Glass    """Tries to guess the upstream for a branch
822a9e2c6aSSimon Glass
832a9e2c6aSSimon Glass    This lists out top commits on a branch and tries to find a suitable
842a9e2c6aSSimon Glass    upstream. It does this by looking for the first commit where
852a9e2c6aSSimon Glass    'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
862a9e2c6aSSimon Glass
872a9e2c6aSSimon Glass    Args:
882a9e2c6aSSimon Glass        git_dir: Git directory containing repo
892a9e2c6aSSimon Glass        branch: Name of branch
902a9e2c6aSSimon Glass
912a9e2c6aSSimon Glass    Returns:
922a9e2c6aSSimon Glass        Tuple:
932a9e2c6aSSimon Glass            Name of upstream branch (e.g. 'upstream/master') or None if none
942a9e2c6aSSimon Glass            Warning/error message, or None if none
952a9e2c6aSSimon Glass    """
962a9e2c6aSSimon Glass    pipe = [LogCmd(branch, git_dir=git_dir, oneline=True, count=100)]
972a9e2c6aSSimon Glass    result = command.RunPipe(pipe, capture=True, capture_stderr=True,
982a9e2c6aSSimon Glass                             raise_on_error=False)
992a9e2c6aSSimon Glass    if result.return_code:
1002a9e2c6aSSimon Glass        return None, "Branch '%s' not found" % branch
1012a9e2c6aSSimon Glass    for line in result.stdout.splitlines()[1:]:
1022a9e2c6aSSimon Glass        commit_hash = line.split(' ')[0]
1032a9e2c6aSSimon Glass        name = NameRevision(commit_hash)
1042a9e2c6aSSimon Glass        if '~' not in name and '^' not in name:
1052a9e2c6aSSimon Glass            if name.startswith('remotes/'):
1062a9e2c6aSSimon Glass                name = name[8:]
1072a9e2c6aSSimon Glass            return name, "Guessing upstream as '%s'" % name
1082a9e2c6aSSimon Glass    return None, "Cannot find a suitable upstream for branch '%s'" % branch
1092a9e2c6aSSimon Glass
1105f6a1c42SSimon Glassdef GetUpstream(git_dir, branch):
1115f6a1c42SSimon Glass    """Returns the name of the upstream for a branch
1125f6a1c42SSimon Glass
1135f6a1c42SSimon Glass    Args:
1145f6a1c42SSimon Glass        git_dir: Git directory containing repo
1155f6a1c42SSimon Glass        branch: Name of branch
1165f6a1c42SSimon Glass
1175f6a1c42SSimon Glass    Returns:
1182a9e2c6aSSimon Glass        Tuple:
1195f6a1c42SSimon Glass            Name of upstream branch (e.g. 'upstream/master') or None if none
1202a9e2c6aSSimon Glass            Warning/error message, or None if none
1215f6a1c42SSimon Glass    """
122cce717a9SSimon Glass    try:
1235f6a1c42SSimon Glass        remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
1245f6a1c42SSimon Glass                                       'branch.%s.remote' % branch)
1255f6a1c42SSimon Glass        merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
1265f6a1c42SSimon Glass                                      'branch.%s.merge' % branch)
127cce717a9SSimon Glass    except:
1282a9e2c6aSSimon Glass        upstream, msg = GuessUpstream(git_dir, branch)
1292a9e2c6aSSimon Glass        return upstream, msg
130cce717a9SSimon Glass
1315f6a1c42SSimon Glass    if remote == '.':
13271edbe5cSSimon Glass        return merge, None
1335f6a1c42SSimon Glass    elif remote and merge:
1345f6a1c42SSimon Glass        leaf = merge.split('/')[-1]
1352a9e2c6aSSimon Glass        return '%s/%s' % (remote, leaf), None
1365f6a1c42SSimon Glass    else:
1375f6a1c42SSimon Glass        raise ValueError, ("Cannot determine upstream branch for branch "
1385f6a1c42SSimon Glass                "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
1395f6a1c42SSimon Glass
1405f6a1c42SSimon Glass
1415f6a1c42SSimon Glassdef GetRangeInBranch(git_dir, branch, include_upstream=False):
1425f6a1c42SSimon Glass    """Returns an expression for the commits in the given branch.
1435f6a1c42SSimon Glass
1445f6a1c42SSimon Glass    Args:
1455f6a1c42SSimon Glass        git_dir: Directory containing git repo
1465f6a1c42SSimon Glass        branch: Name of branch
1475f6a1c42SSimon Glass    Return:
1485f6a1c42SSimon Glass        Expression in the form 'upstream..branch' which can be used to
149cce717a9SSimon Glass        access the commits. If the branch does not exist, returns None.
1505f6a1c42SSimon Glass    """
1512a9e2c6aSSimon Glass    upstream, msg = GetUpstream(git_dir, branch)
152cce717a9SSimon Glass    if not upstream:
1532a9e2c6aSSimon Glass        return None, msg
1542a9e2c6aSSimon Glass    rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
1552a9e2c6aSSimon Glass    return rstr, msg
1565f6a1c42SSimon Glass
1575abab20dSSimon Glassdef CountCommitsInRange(git_dir, range_expr):
1585abab20dSSimon Glass    """Returns the number of commits in the given range.
1595abab20dSSimon Glass
1605abab20dSSimon Glass    Args:
1615abab20dSSimon Glass        git_dir: Directory containing git repo
1625abab20dSSimon Glass        range_expr: Range to check
1635abab20dSSimon Glass    Return:
1645abab20dSSimon Glass        Number of patches that exist in the supplied rangem or None if none
1655abab20dSSimon Glass        were found
1665abab20dSSimon Glass    """
1675abab20dSSimon Glass    pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True)]
1685abab20dSSimon Glass    result = command.RunPipe(pipe, capture=True, capture_stderr=True,
1695abab20dSSimon Glass                             raise_on_error=False)
1705abab20dSSimon Glass    if result.return_code:
1715abab20dSSimon Glass        return None, "Range '%s' not found or is invalid" % range_expr
1725abab20dSSimon Glass    patch_count = len(result.stdout.splitlines())
1735abab20dSSimon Glass    return patch_count, None
1745abab20dSSimon Glass
1755f6a1c42SSimon Glassdef CountCommitsInBranch(git_dir, branch, include_upstream=False):
1765f6a1c42SSimon Glass    """Returns the number of commits in the given branch.
1775f6a1c42SSimon Glass
1785f6a1c42SSimon Glass    Args:
1795f6a1c42SSimon Glass        git_dir: Directory containing git repo
1805f6a1c42SSimon Glass        branch: Name of branch
1815f6a1c42SSimon Glass    Return:
182cce717a9SSimon Glass        Number of patches that exist on top of the branch, or None if the
183cce717a9SSimon Glass        branch does not exist.
1845f6a1c42SSimon Glass    """
1852a9e2c6aSSimon Glass    range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream)
186cce717a9SSimon Glass    if not range_expr:
1872a9e2c6aSSimon Glass        return None, msg
1885abab20dSSimon Glass    return CountCommitsInRange(git_dir, range_expr)
1895f6a1c42SSimon Glass
1905f6a1c42SSimon Glassdef CountCommits(commit_range):
1915f6a1c42SSimon Glass    """Returns the number of commits in the given range.
1925f6a1c42SSimon Glass
1935f6a1c42SSimon Glass    Args:
1945f6a1c42SSimon Glass        commit_range: Range of commits to count (e.g. 'HEAD..base')
1955f6a1c42SSimon Glass    Return:
1965f6a1c42SSimon Glass        Number of patches that exist on top of the branch
1975f6a1c42SSimon Glass    """
198cda2a611SSimon Glass    pipe = [LogCmd(commit_range, oneline=True),
1995f6a1c42SSimon Glass            ['wc', '-l']]
2005f6a1c42SSimon Glass    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
2015f6a1c42SSimon Glass    patch_count = int(stdout)
2025f6a1c42SSimon Glass    return patch_count
2035f6a1c42SSimon Glass
2045f6a1c42SSimon Glassdef Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
2055f6a1c42SSimon Glass    """Checkout the selected commit for this build
2065f6a1c42SSimon Glass
2075f6a1c42SSimon Glass    Args:
2085f6a1c42SSimon Glass        commit_hash: Commit hash to check out
2095f6a1c42SSimon Glass    """
2105f6a1c42SSimon Glass    pipe = ['git']
2115f6a1c42SSimon Glass    if git_dir:
2125f6a1c42SSimon Glass        pipe.extend(['--git-dir', git_dir])
2135f6a1c42SSimon Glass    if work_tree:
2145f6a1c42SSimon Glass        pipe.extend(['--work-tree', work_tree])
2155f6a1c42SSimon Glass    pipe.append('checkout')
2165f6a1c42SSimon Glass    if force:
2175f6a1c42SSimon Glass        pipe.append('-f')
2185f6a1c42SSimon Glass    pipe.append(commit_hash)
219ddaf5c8fSSimon Glass    result = command.RunPipe([pipe], capture=True, raise_on_error=False,
220ddaf5c8fSSimon Glass                             capture_stderr=True)
2215f6a1c42SSimon Glass    if result.return_code != 0:
2225f6a1c42SSimon Glass        raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
2235f6a1c42SSimon Glass
2245f6a1c42SSimon Glassdef Clone(git_dir, output_dir):
2255f6a1c42SSimon Glass    """Checkout the selected commit for this build
2265f6a1c42SSimon Glass
2275f6a1c42SSimon Glass    Args:
2285f6a1c42SSimon Glass        commit_hash: Commit hash to check out
2295f6a1c42SSimon Glass    """
2305f6a1c42SSimon Glass    pipe = ['git', 'clone', git_dir, '.']
231ddaf5c8fSSimon Glass    result = command.RunPipe([pipe], capture=True, cwd=output_dir,
232ddaf5c8fSSimon Glass                             capture_stderr=True)
2335f6a1c42SSimon Glass    if result.return_code != 0:
2345f6a1c42SSimon Glass        raise OSError, 'git clone: %s' % result.stderr
2355f6a1c42SSimon Glass
2365f6a1c42SSimon Glassdef Fetch(git_dir=None, work_tree=None):
2375f6a1c42SSimon Glass    """Fetch from the origin repo
2385f6a1c42SSimon Glass
2395f6a1c42SSimon Glass    Args:
2405f6a1c42SSimon Glass        commit_hash: Commit hash to check out
2415f6a1c42SSimon Glass    """
2425f6a1c42SSimon Glass    pipe = ['git']
2435f6a1c42SSimon Glass    if git_dir:
2445f6a1c42SSimon Glass        pipe.extend(['--git-dir', git_dir])
2455f6a1c42SSimon Glass    if work_tree:
2465f6a1c42SSimon Glass        pipe.extend(['--work-tree', work_tree])
2475f6a1c42SSimon Glass    pipe.append('fetch')
248ddaf5c8fSSimon Glass    result = command.RunPipe([pipe], capture=True, capture_stderr=True)
2495f6a1c42SSimon Glass    if result.return_code != 0:
2505f6a1c42SSimon Glass        raise OSError, 'git fetch: %s' % result.stderr
2515f6a1c42SSimon Glass
2520d24de9dSSimon Glassdef CreatePatches(start, count, series):
2530d24de9dSSimon Glass    """Create a series of patches from the top of the current branch.
2540d24de9dSSimon Glass
2550d24de9dSSimon Glass    The patch files are written to the current directory using
2560d24de9dSSimon Glass    git format-patch.
2570d24de9dSSimon Glass
2580d24de9dSSimon Glass    Args:
2590d24de9dSSimon Glass        start: Commit to start from: 0=HEAD, 1=next one, etc.
2600d24de9dSSimon Glass        count: number of commits to include
2610d24de9dSSimon Glass    Return:
2620d24de9dSSimon Glass        Filename of cover letter
2630d24de9dSSimon Glass        List of filenames of patch files
2640d24de9dSSimon Glass    """
2650d24de9dSSimon Glass    if series.get('version'):
2660d24de9dSSimon Glass        version = '%s ' % series['version']
2678d3595a4SMasahiro Yamada    cmd = ['git', 'format-patch', '-M', '--signoff']
2680d24de9dSSimon Glass    if series.get('cover'):
2690d24de9dSSimon Glass        cmd.append('--cover-letter')
2700d24de9dSSimon Glass    prefix = series.GetPatchPrefix()
2710d24de9dSSimon Glass    if prefix:
2720d24de9dSSimon Glass        cmd += ['--subject-prefix=%s' % prefix]
2730d24de9dSSimon Glass    cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
2740d24de9dSSimon Glass
2750d24de9dSSimon Glass    stdout = command.RunList(cmd)
2760d24de9dSSimon Glass    files = stdout.splitlines()
2770d24de9dSSimon Glass
2780d24de9dSSimon Glass    # We have an extra file if there is a cover letter
2790d24de9dSSimon Glass    if series.get('cover'):
2800d24de9dSSimon Glass       return files[0], files[1:]
2810d24de9dSSimon Glass    else:
2820d24de9dSSimon Glass       return None, files
2830d24de9dSSimon Glass
284a1318f7cSSimon Glassdef BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
2850d24de9dSSimon Glass    """Build a list of email addresses based on an input list.
2860d24de9dSSimon Glass
2870d24de9dSSimon Glass    Takes a list of email addresses and aliases, and turns this into a list
2880d24de9dSSimon Glass    of only email address, by resolving any aliases that are present.
2890d24de9dSSimon Glass
2900d24de9dSSimon Glass    If the tag is given, then each email address is prepended with this
2910d24de9dSSimon Glass    tag and a space. If the tag starts with a minus sign (indicating a
2920d24de9dSSimon Glass    command line parameter) then the email address is quoted.
2930d24de9dSSimon Glass
2940d24de9dSSimon Glass    Args:
2950d24de9dSSimon Glass        in_list:        List of aliases/email addresses
2960d24de9dSSimon Glass        tag:            Text to put before each address
297a1318f7cSSimon Glass        alias:          Alias dictionary
298a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
299a1318f7cSSimon Glass                False to just print a message.
3000d24de9dSSimon Glass
3010d24de9dSSimon Glass    Returns:
3020d24de9dSSimon Glass        List of email addresses
3030d24de9dSSimon Glass
3040d24de9dSSimon Glass    >>> alias = {}
3050d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
3060d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
3070d24de9dSSimon Glass    >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
3080d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john']
3090d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
3100d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], None, alias)
3110d24de9dSSimon Glass    ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
3120d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], '--to', alias)
3130d24de9dSSimon Glass    ['--to "j.bloggs@napier.co.nz"', \
3140d24de9dSSimon Glass'--to "Mary Poppins <m.poppins@cloud.net>"']
3150d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
3160d24de9dSSimon Glass    ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
3170d24de9dSSimon Glass    """
3180d24de9dSSimon Glass    quote = '"' if tag and tag[0] == '-' else ''
3190d24de9dSSimon Glass    raw = []
3200d24de9dSSimon Glass    for item in in_list:
321a1318f7cSSimon Glass        raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
3220d24de9dSSimon Glass    result = []
3230d24de9dSSimon Glass    for item in raw:
3240d24de9dSSimon Glass        if not item in result:
3250d24de9dSSimon Glass            result.append(item)
3260d24de9dSSimon Glass    if tag:
3270d24de9dSSimon Glass        return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
3280d24de9dSSimon Glass    return result
3290d24de9dSSimon Glass
330a1318f7cSSimon Glassdef EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
331*27067a46SMateusz Kulikowski        self_only=False, alias=None, in_reply_to=None, thread=False):
3320d24de9dSSimon Glass    """Email a patch series.
3330d24de9dSSimon Glass
3340d24de9dSSimon Glass    Args:
3350d24de9dSSimon Glass        series: Series object containing destination info
3360d24de9dSSimon Glass        cover_fname: filename of cover letter
3370d24de9dSSimon Glass        args: list of filenames of patch files
3380d24de9dSSimon Glass        dry_run: Just return the command that would be run
339a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
340a1318f7cSSimon Glass                False to just print a message.
3410d24de9dSSimon Glass        cc_fname: Filename of Cc file for per-commit Cc
3420d24de9dSSimon Glass        self_only: True to just email to yourself as a test
3436d819925SDoug Anderson        in_reply_to: If set we'll pass this to git as --in-reply-to.
3446d819925SDoug Anderson            Should be a message ID that this is in reply to.
345*27067a46SMateusz Kulikowski        thread: True to add --thread to git send-email (make
346*27067a46SMateusz Kulikowski            all patches reply to cover-letter or first patch in series)
3470d24de9dSSimon Glass
3480d24de9dSSimon Glass    Returns:
3490d24de9dSSimon Glass        Git command that was/would be run
3500d24de9dSSimon Glass
351a970048eSDoug Anderson    # For the duration of this doctest pretend that we ran patman with ./patman
352a970048eSDoug Anderson    >>> _old_argv0 = sys.argv[0]
353a970048eSDoug Anderson    >>> sys.argv[0] = './patman'
354a970048eSDoug Anderson
3550d24de9dSSimon Glass    >>> alias = {}
3560d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
3570d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
3580d24de9dSSimon Glass    >>> alias['mary'] = ['m.poppins@cloud.net']
3590d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john']
3600d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
3610d24de9dSSimon Glass    >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
3620d24de9dSSimon Glass    >>> series = series.Series()
3630d24de9dSSimon Glass    >>> series.to = ['fred']
3640d24de9dSSimon Glass    >>> series.cc = ['mary']
365a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
366a1318f7cSSimon Glass            False, alias)
3670d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3680d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
369a1318f7cSSimon Glass    >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
370a1318f7cSSimon Glass            alias)
3710d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3720d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
3730d24de9dSSimon Glass    >>> series.cc = ['all']
374a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
375a1318f7cSSimon Glass            True, alias)
3760d24de9dSSimon Glass    'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
3770d24de9dSSimon Glass--cc-cmd cc-fname" cover p1 p2'
378a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
379a1318f7cSSimon Glass            False, alias)
3800d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3810d24de9dSSimon Glass"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
3820d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
383a970048eSDoug Anderson
384a970048eSDoug Anderson    # Restore argv[0] since we clobbered it.
385a970048eSDoug Anderson    >>> sys.argv[0] = _old_argv0
3860d24de9dSSimon Glass    """
387a1318f7cSSimon Glass    to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
3880d24de9dSSimon Glass    if not to:
389ee860c60SMasahiro Yamada        git_config_to = command.Output('git', 'config', 'sendemail.to')
390ee860c60SMasahiro Yamada        if not git_config_to:
391ee860c60SMasahiro Yamada            print ("No recipient.\n"
392ee860c60SMasahiro Yamada                   "Please add something like this to a commit\n"
393ee860c60SMasahiro Yamada                   "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
394ee860c60SMasahiro Yamada                   "Or do something like this\n"
395ee860c60SMasahiro Yamada                   "git config sendemail.to u-boot@lists.denx.de")
3960d24de9dSSimon Glass            return
3972181830fSPeter Tyser    cc = BuildEmailList(list(set(series.get('cc')) - set(series.get('to'))),
3982181830fSPeter Tyser                        '--cc', alias, raise_on_error)
3990d24de9dSSimon Glass    if self_only:
400a1318f7cSSimon Glass        to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
4010d24de9dSSimon Glass        cc = []
4020d24de9dSSimon Glass    cmd = ['git', 'send-email', '--annotate']
4036d819925SDoug Anderson    if in_reply_to:
4046d819925SDoug Anderson        cmd.append('--in-reply-to="%s"' % in_reply_to)
405*27067a46SMateusz Kulikowski    if thread:
406*27067a46SMateusz Kulikowski        cmd.append('--thread')
4076d819925SDoug Anderson
4080d24de9dSSimon Glass    cmd += to
4090d24de9dSSimon Glass    cmd += cc
4100d24de9dSSimon Glass    cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
4110d24de9dSSimon Glass    if cover_fname:
4120d24de9dSSimon Glass        cmd.append(cover_fname)
4130d24de9dSSimon Glass    cmd += args
4140d24de9dSSimon Glass    str = ' '.join(cmd)
4150d24de9dSSimon Glass    if not dry_run:
4160d24de9dSSimon Glass        os.system(str)
4170d24de9dSSimon Glass    return str
4180d24de9dSSimon Glass
4190d24de9dSSimon Glass
420a1318f7cSSimon Glassdef LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
4210d24de9dSSimon Glass    """If an email address is an alias, look it up and return the full name
4220d24de9dSSimon Glass
4230d24de9dSSimon Glass    TODO: Why not just use git's own alias feature?
4240d24de9dSSimon Glass
4250d24de9dSSimon Glass    Args:
4260d24de9dSSimon Glass        lookup_name: Alias or email address to look up
427a1318f7cSSimon Glass        alias: Dictionary containing aliases (None to use settings default)
428a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
429a1318f7cSSimon Glass                False to just print a message.
4300d24de9dSSimon Glass
4310d24de9dSSimon Glass    Returns:
4320d24de9dSSimon Glass        tuple:
4330d24de9dSSimon Glass            list containing a list of email addresses
4340d24de9dSSimon Glass
4350d24de9dSSimon Glass    Raises:
4360d24de9dSSimon Glass        OSError if a recursive alias reference was found
4370d24de9dSSimon Glass        ValueError if an alias was not found
4380d24de9dSSimon Glass
4390d24de9dSSimon Glass    >>> alias = {}
4400d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
4410d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
4420d24de9dSSimon Glass    >>> alias['mary'] = ['m.poppins@cloud.net']
4430d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
4440d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
4450d24de9dSSimon Glass    >>> alias['loop'] = ['other', 'john', '   mary   ']
4460d24de9dSSimon Glass    >>> alias['other'] = ['loop', 'john', '   mary   ']
4470d24de9dSSimon Glass    >>> LookupEmail('mary', alias)
4480d24de9dSSimon Glass    ['m.poppins@cloud.net']
4490d24de9dSSimon Glass    >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
4500d24de9dSSimon Glass    ['arthur.wellesley@howe.ro.uk']
4510d24de9dSSimon Glass    >>> LookupEmail('boys', alias)
4520d24de9dSSimon Glass    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
4530d24de9dSSimon Glass    >>> LookupEmail('all', alias)
4540d24de9dSSimon Glass    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
4550d24de9dSSimon Glass    >>> LookupEmail('odd', alias)
4560d24de9dSSimon Glass    Traceback (most recent call last):
4570d24de9dSSimon Glass    ...
4580d24de9dSSimon Glass    ValueError: Alias 'odd' not found
4590d24de9dSSimon Glass    >>> LookupEmail('loop', alias)
4600d24de9dSSimon Glass    Traceback (most recent call last):
4610d24de9dSSimon Glass    ...
4620d24de9dSSimon Glass    OSError: Recursive email alias at 'other'
463a1318f7cSSimon Glass    >>> LookupEmail('odd', alias, raise_on_error=False)
464e752edcbSSimon Glass    Alias 'odd' not found
465a1318f7cSSimon Glass    []
466a1318f7cSSimon Glass    >>> # In this case the loop part will effectively be ignored.
467a1318f7cSSimon Glass    >>> LookupEmail('loop', alias, raise_on_error=False)
468e752edcbSSimon Glass    Recursive email alias at 'other'
469e752edcbSSimon Glass    Recursive email alias at 'john'
470e752edcbSSimon Glass    Recursive email alias at 'mary'
471a1318f7cSSimon Glass    ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
4720d24de9dSSimon Glass    """
4730d24de9dSSimon Glass    if not alias:
4740d24de9dSSimon Glass        alias = settings.alias
4750d24de9dSSimon Glass    lookup_name = lookup_name.strip()
4760d24de9dSSimon Glass    if '@' in lookup_name: # Perhaps a real email address
4770d24de9dSSimon Glass        return [lookup_name]
4780d24de9dSSimon Glass
4790d24de9dSSimon Glass    lookup_name = lookup_name.lower()
480a1318f7cSSimon Glass    col = terminal.Color()
4810d24de9dSSimon Glass
4820d24de9dSSimon Glass    out_list = []
483a1318f7cSSimon Glass    if level > 10:
484a1318f7cSSimon Glass        msg = "Recursive email alias at '%s'" % lookup_name
485a1318f7cSSimon Glass        if raise_on_error:
486a1318f7cSSimon Glass            raise OSError, msg
487a1318f7cSSimon Glass        else:
488a1318f7cSSimon Glass            print col.Color(col.RED, msg)
489a1318f7cSSimon Glass            return out_list
490a1318f7cSSimon Glass
4910d24de9dSSimon Glass    if lookup_name:
4920d24de9dSSimon Glass        if not lookup_name in alias:
493a1318f7cSSimon Glass            msg = "Alias '%s' not found" % lookup_name
494a1318f7cSSimon Glass            if raise_on_error:
495a1318f7cSSimon Glass                raise ValueError, msg
496a1318f7cSSimon Glass            else:
497a1318f7cSSimon Glass                print col.Color(col.RED, msg)
498a1318f7cSSimon Glass                return out_list
4990d24de9dSSimon Glass        for item in alias[lookup_name]:
500a1318f7cSSimon Glass            todo = LookupEmail(item, alias, raise_on_error, level + 1)
5010d24de9dSSimon Glass            for new_item in todo:
5020d24de9dSSimon Glass                if not new_item in out_list:
5030d24de9dSSimon Glass                    out_list.append(new_item)
5040d24de9dSSimon Glass
5050d24de9dSSimon Glass    #print "No match for alias '%s'" % lookup_name
5060d24de9dSSimon Glass    return out_list
5070d24de9dSSimon Glass
5080d24de9dSSimon Glassdef GetTopLevel():
5090d24de9dSSimon Glass    """Return name of top-level directory for this git repo.
5100d24de9dSSimon Glass
5110d24de9dSSimon Glass    Returns:
5120d24de9dSSimon Glass        Full path to git top-level directory
5130d24de9dSSimon Glass
5140d24de9dSSimon Glass    This test makes sure that we are running tests in the right subdir
5150d24de9dSSimon Glass
516a970048eSDoug Anderson    >>> os.path.realpath(os.path.dirname(__file__)) == \
517a970048eSDoug Anderson            os.path.join(GetTopLevel(), 'tools', 'patman')
5180d24de9dSSimon Glass    True
5190d24de9dSSimon Glass    """
5200d24de9dSSimon Glass    return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
5210d24de9dSSimon Glass
5220d24de9dSSimon Glassdef GetAliasFile():
5230d24de9dSSimon Glass    """Gets the name of the git alias file.
5240d24de9dSSimon Glass
5250d24de9dSSimon Glass    Returns:
5260d24de9dSSimon Glass        Filename of git alias file, or None if none
5270d24de9dSSimon Glass    """
528dc191505SSimon Glass    fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
529dc191505SSimon Glass            raise_on_error=False)
5300d24de9dSSimon Glass    if fname:
5310d24de9dSSimon Glass        fname = os.path.join(GetTopLevel(), fname.strip())
5320d24de9dSSimon Glass    return fname
5330d24de9dSSimon Glass
53487d65558SVikram Narayanandef GetDefaultUserName():
53587d65558SVikram Narayanan    """Gets the user.name from .gitconfig file.
53687d65558SVikram Narayanan
53787d65558SVikram Narayanan    Returns:
53887d65558SVikram Narayanan        User name found in .gitconfig file, or None if none
53987d65558SVikram Narayanan    """
54087d65558SVikram Narayanan    uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
54187d65558SVikram Narayanan    return uname
54287d65558SVikram Narayanan
54387d65558SVikram Narayanandef GetDefaultUserEmail():
54487d65558SVikram Narayanan    """Gets the user.email from the global .gitconfig file.
54587d65558SVikram Narayanan
54687d65558SVikram Narayanan    Returns:
54787d65558SVikram Narayanan        User's email found in .gitconfig file, or None if none
54887d65558SVikram Narayanan    """
54987d65558SVikram Narayanan    uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
55087d65558SVikram Narayanan    return uemail
55187d65558SVikram Narayanan
5523871cd85SWu, Joshdef GetDefaultSubjectPrefix():
5533871cd85SWu, Josh    """Gets the format.subjectprefix from local .git/config file.
5543871cd85SWu, Josh
5553871cd85SWu, Josh    Returns:
5563871cd85SWu, Josh        Subject prefix found in local .git/config file, or None if none
5573871cd85SWu, Josh    """
5583871cd85SWu, Josh    sub_prefix = command.OutputOneLine('git', 'config', 'format.subjectprefix',
5593871cd85SWu, Josh                 raise_on_error=False)
5603871cd85SWu, Josh
5613871cd85SWu, Josh    return sub_prefix
5623871cd85SWu, Josh
5630d24de9dSSimon Glassdef Setup():
5640d24de9dSSimon Glass    """Set up git utils, by reading the alias files."""
5650d24de9dSSimon Glass    # Check for a git alias file also
5660b703dbcSSimon Glass    global use_no_decorate
5670b703dbcSSimon Glass
5680d24de9dSSimon Glass    alias_fname = GetAliasFile()
5690d24de9dSSimon Glass    if alias_fname:
5700d24de9dSSimon Glass        settings.ReadGitAliases(alias_fname)
571e49f14afSSimon Glass    cmd = LogCmd(None, count=0)
572e49f14afSSimon Glass    use_no_decorate = (command.RunPipe([cmd], raise_on_error=False)
573e49f14afSSimon Glass                       .return_code == 0)
5740d24de9dSSimon Glass
5755f6a1c42SSimon Glassdef GetHead():
5765f6a1c42SSimon Glass    """Get the hash of the current HEAD
5775f6a1c42SSimon Glass
5785f6a1c42SSimon Glass    Returns:
5795f6a1c42SSimon Glass        Hash of HEAD
5805f6a1c42SSimon Glass    """
5815f6a1c42SSimon Glass    return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
5825f6a1c42SSimon Glass
5830d24de9dSSimon Glassif __name__ == "__main__":
5840d24de9dSSimon Glass    import doctest
5850d24de9dSSimon Glass
5860d24de9dSSimon Glass    doctest.testmod()
587