xref: /openbmc/u-boot/tools/patman/gitutil.py (revision 94905e1db8d8d42c4f39f14dbee2f9788390db5e)
11a459660SWolfgang Denk# SPDX-License-Identifier: GPL-2.0+
283d290c5STom Rini# Copyright (c) 2011 The Chromium OS Authors.
30d24de9dSSimon Glass#
40d24de9dSSimon Glass
50d24de9dSSimon Glassimport command
60d24de9dSSimon Glassimport re
70d24de9dSSimon Glassimport os
80d24de9dSSimon Glassimport series
90d24de9dSSimon Glassimport subprocess
100d24de9dSSimon Glassimport sys
110d24de9dSSimon Glassimport terminal
120d24de9dSSimon Glass
13757f64a8SSimon Glassimport checkpatch
145f6a1c42SSimon Glassimport settings
155f6a1c42SSimon Glass
16e49f14afSSimon Glass# True to use --no-decorate - we check this in Setup()
17e49f14afSSimon Glassuse_no_decorate = True
18e49f14afSSimon Glass
19cda2a611SSimon Glassdef LogCmd(commit_range, git_dir=None, oneline=False, reverse=False,
20cda2a611SSimon Glass           count=None):
21cda2a611SSimon Glass    """Create a command to perform a 'git log'
22cda2a611SSimon Glass
23cda2a611SSimon Glass    Args:
24cda2a611SSimon Glass        commit_range: Range expression to use for log, None for none
25cda2a611SSimon Glass        git_dir: Path to git repositiory (None to use default)
26cda2a611SSimon Glass        oneline: True to use --oneline, else False
27cda2a611SSimon Glass        reverse: True to reverse the log (--reverse)
28cda2a611SSimon Glass        count: Number of commits to list, or None for no limit
29cda2a611SSimon Glass    Return:
30cda2a611SSimon Glass        List containing command and arguments to run
31cda2a611SSimon Glass    """
32cda2a611SSimon Glass    cmd = ['git']
33cda2a611SSimon Glass    if git_dir:
34cda2a611SSimon Glass        cmd += ['--git-dir', git_dir]
359447a6b2SSimon Glass    cmd += ['--no-pager', 'log', '--no-color']
36cda2a611SSimon Glass    if oneline:
37cda2a611SSimon Glass        cmd.append('--oneline')
38e49f14afSSimon Glass    if use_no_decorate:
39cda2a611SSimon Glass        cmd.append('--no-decorate')
40042a732cSSimon Glass    if reverse:
41042a732cSSimon Glass        cmd.append('--reverse')
42cda2a611SSimon Glass    if count is not None:
43cda2a611SSimon Glass        cmd.append('-n%d' % count)
44cda2a611SSimon Glass    if commit_range:
45cda2a611SSimon Glass        cmd.append(commit_range)
46d4c8572bSSimon Glass
47d4c8572bSSimon Glass    # Add this in case we have a branch with the same name as a directory.
48d4c8572bSSimon Glass    # This avoids messages like this, for example:
49d4c8572bSSimon Glass    #   fatal: ambiguous argument 'test': both revision and filename
50d4c8572bSSimon Glass    cmd.append('--')
51cda2a611SSimon Glass    return cmd
520d24de9dSSimon Glass
530d24de9dSSimon Glassdef CountCommitsToBranch():
540d24de9dSSimon Glass    """Returns number of commits between HEAD and the tracking branch.
550d24de9dSSimon Glass
560d24de9dSSimon Glass    This looks back to the tracking branch and works out the number of commits
570d24de9dSSimon Glass    since then.
580d24de9dSSimon Glass
590d24de9dSSimon Glass    Return:
600d24de9dSSimon Glass        Number of patches that exist on top of the branch
610d24de9dSSimon Glass    """
62cda2a611SSimon Glass    pipe = [LogCmd('@{upstream}..', oneline=True),
630d24de9dSSimon Glass            ['wc', '-l']]
64a10fd93cSSimon Glass    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
650d24de9dSSimon Glass    patch_count = int(stdout)
660d24de9dSSimon Glass    return patch_count
670d24de9dSSimon Glass
682a9e2c6aSSimon Glassdef NameRevision(commit_hash):
692a9e2c6aSSimon Glass    """Gets the revision name for a commit
702a9e2c6aSSimon Glass
712a9e2c6aSSimon Glass    Args:
722a9e2c6aSSimon Glass        commit_hash: Commit hash to look up
732a9e2c6aSSimon Glass
742a9e2c6aSSimon Glass    Return:
752a9e2c6aSSimon Glass        Name of revision, if any, else None
762a9e2c6aSSimon Glass    """
772a9e2c6aSSimon Glass    pipe = ['git', 'name-rev', commit_hash]
782a9e2c6aSSimon Glass    stdout = command.RunPipe([pipe], capture=True, oneline=True).stdout
792a9e2c6aSSimon Glass
802a9e2c6aSSimon Glass    # We expect a commit, a space, then a revision name
812a9e2c6aSSimon Glass    name = stdout.split(' ')[1].strip()
822a9e2c6aSSimon Glass    return name
832a9e2c6aSSimon Glass
842a9e2c6aSSimon Glassdef GuessUpstream(git_dir, branch):
852a9e2c6aSSimon Glass    """Tries to guess the upstream for a branch
862a9e2c6aSSimon Glass
872a9e2c6aSSimon Glass    This lists out top commits on a branch and tries to find a suitable
882a9e2c6aSSimon Glass    upstream. It does this by looking for the first commit where
892a9e2c6aSSimon Glass    'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
902a9e2c6aSSimon Glass
912a9e2c6aSSimon Glass    Args:
922a9e2c6aSSimon Glass        git_dir: Git directory containing repo
932a9e2c6aSSimon Glass        branch: Name of branch
942a9e2c6aSSimon Glass
952a9e2c6aSSimon Glass    Returns:
962a9e2c6aSSimon Glass        Tuple:
972a9e2c6aSSimon Glass            Name of upstream branch (e.g. 'upstream/master') or None if none
982a9e2c6aSSimon Glass            Warning/error message, or None if none
992a9e2c6aSSimon Glass    """
1002a9e2c6aSSimon Glass    pipe = [LogCmd(branch, git_dir=git_dir, oneline=True, count=100)]
1012a9e2c6aSSimon Glass    result = command.RunPipe(pipe, capture=True, capture_stderr=True,
1022a9e2c6aSSimon Glass                             raise_on_error=False)
1032a9e2c6aSSimon Glass    if result.return_code:
1042a9e2c6aSSimon Glass        return None, "Branch '%s' not found" % branch
1052a9e2c6aSSimon Glass    for line in result.stdout.splitlines()[1:]:
1062a9e2c6aSSimon Glass        commit_hash = line.split(' ')[0]
1072a9e2c6aSSimon Glass        name = NameRevision(commit_hash)
1082a9e2c6aSSimon Glass        if '~' not in name and '^' not in name:
1092a9e2c6aSSimon Glass            if name.startswith('remotes/'):
1102a9e2c6aSSimon Glass                name = name[8:]
1112a9e2c6aSSimon Glass            return name, "Guessing upstream as '%s'" % name
1122a9e2c6aSSimon Glass    return None, "Cannot find a suitable upstream for branch '%s'" % branch
1132a9e2c6aSSimon Glass
1145f6a1c42SSimon Glassdef GetUpstream(git_dir, branch):
1155f6a1c42SSimon Glass    """Returns the name of the upstream for a branch
1165f6a1c42SSimon Glass
1175f6a1c42SSimon Glass    Args:
1185f6a1c42SSimon Glass        git_dir: Git directory containing repo
1195f6a1c42SSimon Glass        branch: Name of branch
1205f6a1c42SSimon Glass
1215f6a1c42SSimon Glass    Returns:
1222a9e2c6aSSimon Glass        Tuple:
1235f6a1c42SSimon Glass            Name of upstream branch (e.g. 'upstream/master') or None if none
1242a9e2c6aSSimon Glass            Warning/error message, or None if none
1255f6a1c42SSimon Glass    """
126cce717a9SSimon Glass    try:
1275f6a1c42SSimon Glass        remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
1285f6a1c42SSimon Glass                                       'branch.%s.remote' % branch)
1295f6a1c42SSimon Glass        merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
1305f6a1c42SSimon Glass                                      'branch.%s.merge' % branch)
131cce717a9SSimon Glass    except:
1322a9e2c6aSSimon Glass        upstream, msg = GuessUpstream(git_dir, branch)
1332a9e2c6aSSimon Glass        return upstream, msg
134cce717a9SSimon Glass
1355f6a1c42SSimon Glass    if remote == '.':
13671edbe5cSSimon Glass        return merge, None
1375f6a1c42SSimon Glass    elif remote and merge:
1385f6a1c42SSimon Glass        leaf = merge.split('/')[-1]
1392a9e2c6aSSimon Glass        return '%s/%s' % (remote, leaf), None
1405f6a1c42SSimon Glass    else:
141ac3fde93SPaul Burton        raise ValueError("Cannot determine upstream branch for branch "
1425f6a1c42SSimon Glass                "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
1435f6a1c42SSimon Glass
1445f6a1c42SSimon Glass
1455f6a1c42SSimon Glassdef GetRangeInBranch(git_dir, branch, include_upstream=False):
1465f6a1c42SSimon Glass    """Returns an expression for the commits in the given branch.
1475f6a1c42SSimon Glass
1485f6a1c42SSimon Glass    Args:
1495f6a1c42SSimon Glass        git_dir: Directory containing git repo
1505f6a1c42SSimon Glass        branch: Name of branch
1515f6a1c42SSimon Glass    Return:
1525f6a1c42SSimon Glass        Expression in the form 'upstream..branch' which can be used to
153cce717a9SSimon Glass        access the commits. If the branch does not exist, returns None.
1545f6a1c42SSimon Glass    """
1552a9e2c6aSSimon Glass    upstream, msg = GetUpstream(git_dir, branch)
156cce717a9SSimon Glass    if not upstream:
1572a9e2c6aSSimon Glass        return None, msg
1582a9e2c6aSSimon Glass    rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
1592a9e2c6aSSimon Glass    return rstr, msg
1605f6a1c42SSimon Glass
1615abab20dSSimon Glassdef CountCommitsInRange(git_dir, range_expr):
1625abab20dSSimon Glass    """Returns the number of commits in the given range.
1635abab20dSSimon Glass
1645abab20dSSimon Glass    Args:
1655abab20dSSimon Glass        git_dir: Directory containing git repo
1665abab20dSSimon Glass        range_expr: Range to check
1675abab20dSSimon Glass    Return:
1685abab20dSSimon Glass        Number of patches that exist in the supplied rangem or None if none
1695abab20dSSimon Glass        were found
1705abab20dSSimon Glass    """
1715abab20dSSimon Glass    pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True)]
1725abab20dSSimon Glass    result = command.RunPipe(pipe, capture=True, capture_stderr=True,
1735abab20dSSimon Glass                             raise_on_error=False)
1745abab20dSSimon Glass    if result.return_code:
1755abab20dSSimon Glass        return None, "Range '%s' not found or is invalid" % range_expr
1765abab20dSSimon Glass    patch_count = len(result.stdout.splitlines())
1775abab20dSSimon Glass    return patch_count, None
1785abab20dSSimon Glass
1795f6a1c42SSimon Glassdef CountCommitsInBranch(git_dir, branch, include_upstream=False):
1805f6a1c42SSimon Glass    """Returns the number of commits in the given branch.
1815f6a1c42SSimon Glass
1825f6a1c42SSimon Glass    Args:
1835f6a1c42SSimon Glass        git_dir: Directory containing git repo
1845f6a1c42SSimon Glass        branch: Name of branch
1855f6a1c42SSimon Glass    Return:
186cce717a9SSimon Glass        Number of patches that exist on top of the branch, or None if the
187cce717a9SSimon Glass        branch does not exist.
1885f6a1c42SSimon Glass    """
1892a9e2c6aSSimon Glass    range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream)
190cce717a9SSimon Glass    if not range_expr:
1912a9e2c6aSSimon Glass        return None, msg
1925abab20dSSimon Glass    return CountCommitsInRange(git_dir, range_expr)
1935f6a1c42SSimon Glass
1945f6a1c42SSimon Glassdef CountCommits(commit_range):
1955f6a1c42SSimon Glass    """Returns the number of commits in the given range.
1965f6a1c42SSimon Glass
1975f6a1c42SSimon Glass    Args:
1985f6a1c42SSimon Glass        commit_range: Range of commits to count (e.g. 'HEAD..base')
1995f6a1c42SSimon Glass    Return:
2005f6a1c42SSimon Glass        Number of patches that exist on top of the branch
2015f6a1c42SSimon Glass    """
202cda2a611SSimon Glass    pipe = [LogCmd(commit_range, oneline=True),
2035f6a1c42SSimon Glass            ['wc', '-l']]
2045f6a1c42SSimon Glass    stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
2055f6a1c42SSimon Glass    patch_count = int(stdout)
2065f6a1c42SSimon Glass    return patch_count
2075f6a1c42SSimon Glass
2085f6a1c42SSimon Glassdef Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
2095f6a1c42SSimon Glass    """Checkout the selected commit for this build
2105f6a1c42SSimon Glass
2115f6a1c42SSimon Glass    Args:
2125f6a1c42SSimon Glass        commit_hash: Commit hash to check out
2135f6a1c42SSimon Glass    """
2145f6a1c42SSimon Glass    pipe = ['git']
2155f6a1c42SSimon Glass    if git_dir:
2165f6a1c42SSimon Glass        pipe.extend(['--git-dir', git_dir])
2175f6a1c42SSimon Glass    if work_tree:
2185f6a1c42SSimon Glass        pipe.extend(['--work-tree', work_tree])
2195f6a1c42SSimon Glass    pipe.append('checkout')
2205f6a1c42SSimon Glass    if force:
2215f6a1c42SSimon Glass        pipe.append('-f')
2225f6a1c42SSimon Glass    pipe.append(commit_hash)
223ddaf5c8fSSimon Glass    result = command.RunPipe([pipe], capture=True, raise_on_error=False,
224ddaf5c8fSSimon Glass                             capture_stderr=True)
2255f6a1c42SSimon Glass    if result.return_code != 0:
226ac3fde93SPaul Burton        raise OSError('git checkout (%s): %s' % (pipe, result.stderr))
2275f6a1c42SSimon Glass
2285f6a1c42SSimon Glassdef Clone(git_dir, output_dir):
2295f6a1c42SSimon Glass    """Checkout the selected commit for this build
2305f6a1c42SSimon Glass
2315f6a1c42SSimon Glass    Args:
2325f6a1c42SSimon Glass        commit_hash: Commit hash to check out
2335f6a1c42SSimon Glass    """
2345f6a1c42SSimon Glass    pipe = ['git', 'clone', git_dir, '.']
235ddaf5c8fSSimon Glass    result = command.RunPipe([pipe], capture=True, cwd=output_dir,
236ddaf5c8fSSimon Glass                             capture_stderr=True)
2375f6a1c42SSimon Glass    if result.return_code != 0:
238ac3fde93SPaul Burton        raise OSError('git clone: %s' % result.stderr)
2395f6a1c42SSimon Glass
2405f6a1c42SSimon Glassdef Fetch(git_dir=None, work_tree=None):
2415f6a1c42SSimon Glass    """Fetch from the origin repo
2425f6a1c42SSimon Glass
2435f6a1c42SSimon Glass    Args:
2445f6a1c42SSimon Glass        commit_hash: Commit hash to check out
2455f6a1c42SSimon Glass    """
2465f6a1c42SSimon Glass    pipe = ['git']
2475f6a1c42SSimon Glass    if git_dir:
2485f6a1c42SSimon Glass        pipe.extend(['--git-dir', git_dir])
2495f6a1c42SSimon Glass    if work_tree:
2505f6a1c42SSimon Glass        pipe.extend(['--work-tree', work_tree])
2515f6a1c42SSimon Glass    pipe.append('fetch')
252ddaf5c8fSSimon Glass    result = command.RunPipe([pipe], capture=True, capture_stderr=True)
2535f6a1c42SSimon Glass    if result.return_code != 0:
254ac3fde93SPaul Burton        raise OSError('git fetch: %s' % result.stderr)
2555f6a1c42SSimon Glass
2560d24de9dSSimon Glassdef CreatePatches(start, count, series):
2570d24de9dSSimon Glass    """Create a series of patches from the top of the current branch.
2580d24de9dSSimon Glass
2590d24de9dSSimon Glass    The patch files are written to the current directory using
2600d24de9dSSimon Glass    git format-patch.
2610d24de9dSSimon Glass
2620d24de9dSSimon Glass    Args:
2630d24de9dSSimon Glass        start: Commit to start from: 0=HEAD, 1=next one, etc.
2640d24de9dSSimon Glass        count: number of commits to include
2650d24de9dSSimon Glass    Return:
2660d24de9dSSimon Glass        Filename of cover letter
2670d24de9dSSimon Glass        List of filenames of patch files
2680d24de9dSSimon Glass    """
2690d24de9dSSimon Glass    if series.get('version'):
2700d24de9dSSimon Glass        version = '%s ' % series['version']
2718d3595a4SMasahiro Yamada    cmd = ['git', 'format-patch', '-M', '--signoff']
2720d24de9dSSimon Glass    if series.get('cover'):
2730d24de9dSSimon Glass        cmd.append('--cover-letter')
2740d24de9dSSimon Glass    prefix = series.GetPatchPrefix()
2750d24de9dSSimon Glass    if prefix:
2760d24de9dSSimon Glass        cmd += ['--subject-prefix=%s' % prefix]
2770d24de9dSSimon Glass    cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
2780d24de9dSSimon Glass
2790d24de9dSSimon Glass    stdout = command.RunList(cmd)
2800d24de9dSSimon Glass    files = stdout.splitlines()
2810d24de9dSSimon Glass
2820d24de9dSSimon Glass    # We have an extra file if there is a cover letter
2830d24de9dSSimon Glass    if series.get('cover'):
2840d24de9dSSimon Glass       return files[0], files[1:]
2850d24de9dSSimon Glass    else:
2860d24de9dSSimon Glass       return None, files
2870d24de9dSSimon Glass
288a1318f7cSSimon Glassdef BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
2890d24de9dSSimon Glass    """Build a list of email addresses based on an input list.
2900d24de9dSSimon Glass
2910d24de9dSSimon Glass    Takes a list of email addresses and aliases, and turns this into a list
2920d24de9dSSimon Glass    of only email address, by resolving any aliases that are present.
2930d24de9dSSimon Glass
2940d24de9dSSimon Glass    If the tag is given, then each email address is prepended with this
2950d24de9dSSimon Glass    tag and a space. If the tag starts with a minus sign (indicating a
2960d24de9dSSimon Glass    command line parameter) then the email address is quoted.
2970d24de9dSSimon Glass
2980d24de9dSSimon Glass    Args:
2990d24de9dSSimon Glass        in_list:        List of aliases/email addresses
3000d24de9dSSimon Glass        tag:            Text to put before each address
301a1318f7cSSimon Glass        alias:          Alias dictionary
302a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
303a1318f7cSSimon Glass                False to just print a message.
3040d24de9dSSimon Glass
3050d24de9dSSimon Glass    Returns:
3060d24de9dSSimon Glass        List of email addresses
3070d24de9dSSimon Glass
3080d24de9dSSimon Glass    >>> alias = {}
3090d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
3100d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
3110d24de9dSSimon Glass    >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
3120d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john']
3130d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
3140d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], None, alias)
3150d24de9dSSimon Glass    ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
3160d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], '--to', alias)
3170d24de9dSSimon Glass    ['--to "j.bloggs@napier.co.nz"', \
3180d24de9dSSimon Glass'--to "Mary Poppins <m.poppins@cloud.net>"']
3190d24de9dSSimon Glass    >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
3200d24de9dSSimon Glass    ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
3210d24de9dSSimon Glass    """
3220d24de9dSSimon Glass    quote = '"' if tag and tag[0] == '-' else ''
3230d24de9dSSimon Glass    raw = []
3240d24de9dSSimon Glass    for item in in_list:
325a1318f7cSSimon Glass        raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
3260d24de9dSSimon Glass    result = []
3270d24de9dSSimon Glass    for item in raw:
3280d24de9dSSimon Glass        if not item in result:
3290d24de9dSSimon Glass            result.append(item)
3300d24de9dSSimon Glass    if tag:
3310d24de9dSSimon Glass        return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
3320d24de9dSSimon Glass    return result
3330d24de9dSSimon Glass
334a1318f7cSSimon Glassdef EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
335*a60aedfdSSimon Glass        self_only=False, alias=None, in_reply_to=None, thread=False,
336*a60aedfdSSimon Glass        smtp_server=None):
3370d24de9dSSimon Glass    """Email a patch series.
3380d24de9dSSimon Glass
3390d24de9dSSimon Glass    Args:
3400d24de9dSSimon Glass        series: Series object containing destination info
3410d24de9dSSimon Glass        cover_fname: filename of cover letter
3420d24de9dSSimon Glass        args: list of filenames of patch files
3430d24de9dSSimon Glass        dry_run: Just return the command that would be run
344a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
345a1318f7cSSimon Glass                False to just print a message.
3460d24de9dSSimon Glass        cc_fname: Filename of Cc file for per-commit Cc
3470d24de9dSSimon Glass        self_only: True to just email to yourself as a test
3486d819925SDoug Anderson        in_reply_to: If set we'll pass this to git as --in-reply-to.
3496d819925SDoug Anderson            Should be a message ID that this is in reply to.
35027067a46SMateusz Kulikowski        thread: True to add --thread to git send-email (make
35127067a46SMateusz Kulikowski            all patches reply to cover-letter or first patch in series)
352*a60aedfdSSimon Glass        smtp_server: SMTP server to use to send patches
3530d24de9dSSimon Glass
3540d24de9dSSimon Glass    Returns:
3550d24de9dSSimon Glass        Git command that was/would be run
3560d24de9dSSimon Glass
357a970048eSDoug Anderson    # For the duration of this doctest pretend that we ran patman with ./patman
358a970048eSDoug Anderson    >>> _old_argv0 = sys.argv[0]
359a970048eSDoug Anderson    >>> sys.argv[0] = './patman'
360a970048eSDoug Anderson
3610d24de9dSSimon Glass    >>> alias = {}
3620d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
3630d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
3640d24de9dSSimon Glass    >>> alias['mary'] = ['m.poppins@cloud.net']
3650d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john']
3660d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
3670d24de9dSSimon Glass    >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
3680d24de9dSSimon Glass    >>> series = series.Series()
3690d24de9dSSimon Glass    >>> series.to = ['fred']
3700d24de9dSSimon Glass    >>> series.cc = ['mary']
371a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
372a1318f7cSSimon Glass            False, alias)
3730d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3740d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
375a1318f7cSSimon Glass    >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
376a1318f7cSSimon Glass            alias)
3770d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3780d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
3790d24de9dSSimon Glass    >>> series.cc = ['all']
380a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
381a1318f7cSSimon Glass            True, alias)
3820d24de9dSSimon Glass    'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
3830d24de9dSSimon Glass--cc-cmd cc-fname" cover p1 p2'
384a1318f7cSSimon Glass    >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
385a1318f7cSSimon Glass            False, alias)
3860d24de9dSSimon Glass    'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
3870d24de9dSSimon Glass"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
3880d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
389a970048eSDoug Anderson
390a970048eSDoug Anderson    # Restore argv[0] since we clobbered it.
391a970048eSDoug Anderson    >>> sys.argv[0] = _old_argv0
3920d24de9dSSimon Glass    """
393a1318f7cSSimon Glass    to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
3940d24de9dSSimon Glass    if not to:
395785f1548SSimon Glass        git_config_to = command.Output('git', 'config', 'sendemail.to',
396785f1548SSimon Glass                                       raise_on_error=False)
397ee860c60SMasahiro Yamada        if not git_config_to:
398ee860c60SMasahiro Yamada            print ("No recipient.\n"
399ee860c60SMasahiro Yamada                   "Please add something like this to a commit\n"
400ee860c60SMasahiro Yamada                   "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
401ee860c60SMasahiro Yamada                   "Or do something like this\n"
402ee860c60SMasahiro Yamada                   "git config sendemail.to u-boot@lists.denx.de")
4030d24de9dSSimon Glass            return
4042181830fSPeter Tyser    cc = BuildEmailList(list(set(series.get('cc')) - set(series.get('to'))),
4052181830fSPeter Tyser                        '--cc', alias, raise_on_error)
4060d24de9dSSimon Glass    if self_only:
407a1318f7cSSimon Glass        to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
4080d24de9dSSimon Glass        cc = []
4090d24de9dSSimon Glass    cmd = ['git', 'send-email', '--annotate']
410*a60aedfdSSimon Glass    if smtp_server:
411*a60aedfdSSimon Glass        cmd.append('--smtp-server=%s' % smtp_server)
4126d819925SDoug Anderson    if in_reply_to:
4136f8abf76SSimon Glass        if type(in_reply_to) != str:
4146f8abf76SSimon Glass            in_reply_to = in_reply_to.encode('utf-8')
4156d819925SDoug Anderson        cmd.append('--in-reply-to="%s"' % in_reply_to)
41627067a46SMateusz Kulikowski    if thread:
41727067a46SMateusz Kulikowski        cmd.append('--thread')
4186d819925SDoug Anderson
4190d24de9dSSimon Glass    cmd += to
4200d24de9dSSimon Glass    cmd += cc
4210d24de9dSSimon Glass    cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
4220d24de9dSSimon Glass    if cover_fname:
4230d24de9dSSimon Glass        cmd.append(cover_fname)
4240d24de9dSSimon Glass    cmd += args
4252df3a019SSimon Glass    cmdstr = ' '.join(cmd)
4260d24de9dSSimon Glass    if not dry_run:
4272df3a019SSimon Glass        os.system(cmdstr)
4282df3a019SSimon Glass    return cmdstr
4290d24de9dSSimon Glass
4300d24de9dSSimon Glass
431a1318f7cSSimon Glassdef LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
4320d24de9dSSimon Glass    """If an email address is an alias, look it up and return the full name
4330d24de9dSSimon Glass
4340d24de9dSSimon Glass    TODO: Why not just use git's own alias feature?
4350d24de9dSSimon Glass
4360d24de9dSSimon Glass    Args:
4370d24de9dSSimon Glass        lookup_name: Alias or email address to look up
438a1318f7cSSimon Glass        alias: Dictionary containing aliases (None to use settings default)
439a1318f7cSSimon Glass        raise_on_error: True to raise an error when an alias fails to match,
440a1318f7cSSimon Glass                False to just print a message.
4410d24de9dSSimon Glass
4420d24de9dSSimon Glass    Returns:
4430d24de9dSSimon Glass        tuple:
4440d24de9dSSimon Glass            list containing a list of email addresses
4450d24de9dSSimon Glass
4460d24de9dSSimon Glass    Raises:
4470d24de9dSSimon Glass        OSError if a recursive alias reference was found
4480d24de9dSSimon Glass        ValueError if an alias was not found
4490d24de9dSSimon Glass
4500d24de9dSSimon Glass    >>> alias = {}
4510d24de9dSSimon Glass    >>> alias['fred'] = ['f.bloggs@napier.co.nz']
4520d24de9dSSimon Glass    >>> alias['john'] = ['j.bloggs@napier.co.nz']
4530d24de9dSSimon Glass    >>> alias['mary'] = ['m.poppins@cloud.net']
4540d24de9dSSimon Glass    >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
4550d24de9dSSimon Glass    >>> alias['all'] = ['fred ', 'john', '   mary   ']
4560d24de9dSSimon Glass    >>> alias['loop'] = ['other', 'john', '   mary   ']
4570d24de9dSSimon Glass    >>> alias['other'] = ['loop', 'john', '   mary   ']
4580d24de9dSSimon Glass    >>> LookupEmail('mary', alias)
4590d24de9dSSimon Glass    ['m.poppins@cloud.net']
4600d24de9dSSimon Glass    >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
4610d24de9dSSimon Glass    ['arthur.wellesley@howe.ro.uk']
4620d24de9dSSimon Glass    >>> LookupEmail('boys', alias)
4630d24de9dSSimon Glass    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
4640d24de9dSSimon Glass    >>> LookupEmail('all', alias)
4650d24de9dSSimon Glass    ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
4660d24de9dSSimon Glass    >>> LookupEmail('odd', alias)
4670d24de9dSSimon Glass    Traceback (most recent call last):
4680d24de9dSSimon Glass    ...
4690d24de9dSSimon Glass    ValueError: Alias 'odd' not found
4700d24de9dSSimon Glass    >>> LookupEmail('loop', alias)
4710d24de9dSSimon Glass    Traceback (most recent call last):
4720d24de9dSSimon Glass    ...
4730d24de9dSSimon Glass    OSError: Recursive email alias at 'other'
474a1318f7cSSimon Glass    >>> LookupEmail('odd', alias, raise_on_error=False)
475e752edcbSSimon Glass    Alias 'odd' not found
476a1318f7cSSimon Glass    []
477a1318f7cSSimon Glass    >>> # In this case the loop part will effectively be ignored.
478a1318f7cSSimon Glass    >>> LookupEmail('loop', alias, raise_on_error=False)
479e752edcbSSimon Glass    Recursive email alias at 'other'
480e752edcbSSimon Glass    Recursive email alias at 'john'
481e752edcbSSimon Glass    Recursive email alias at 'mary'
482a1318f7cSSimon Glass    ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
4830d24de9dSSimon Glass    """
4840d24de9dSSimon Glass    if not alias:
4850d24de9dSSimon Glass        alias = settings.alias
4860d24de9dSSimon Glass    lookup_name = lookup_name.strip()
4870d24de9dSSimon Glass    if '@' in lookup_name: # Perhaps a real email address
4880d24de9dSSimon Glass        return [lookup_name]
4890d24de9dSSimon Glass
4900d24de9dSSimon Glass    lookup_name = lookup_name.lower()
491a1318f7cSSimon Glass    col = terminal.Color()
4920d24de9dSSimon Glass
4930d24de9dSSimon Glass    out_list = []
494a1318f7cSSimon Glass    if level > 10:
495a1318f7cSSimon Glass        msg = "Recursive email alias at '%s'" % lookup_name
496a1318f7cSSimon Glass        if raise_on_error:
497ac3fde93SPaul Burton            raise OSError(msg)
498a1318f7cSSimon Glass        else:
499a920a17bSPaul Burton            print(col.Color(col.RED, msg))
500a1318f7cSSimon Glass            return out_list
501a1318f7cSSimon Glass
5020d24de9dSSimon Glass    if lookup_name:
5030d24de9dSSimon Glass        if not lookup_name in alias:
504a1318f7cSSimon Glass            msg = "Alias '%s' not found" % lookup_name
505a1318f7cSSimon Glass            if raise_on_error:
506ac3fde93SPaul Burton                raise ValueError(msg)
507a1318f7cSSimon Glass            else:
508a920a17bSPaul Burton                print(col.Color(col.RED, msg))
509a1318f7cSSimon Glass                return out_list
5100d24de9dSSimon Glass        for item in alias[lookup_name]:
511a1318f7cSSimon Glass            todo = LookupEmail(item, alias, raise_on_error, level + 1)
5120d24de9dSSimon Glass            for new_item in todo:
5130d24de9dSSimon Glass                if not new_item in out_list:
5140d24de9dSSimon Glass                    out_list.append(new_item)
5150d24de9dSSimon Glass
516a920a17bSPaul Burton    #print("No match for alias '%s'" % lookup_name)
5170d24de9dSSimon Glass    return out_list
5180d24de9dSSimon Glass
5190d24de9dSSimon Glassdef GetTopLevel():
5200d24de9dSSimon Glass    """Return name of top-level directory for this git repo.
5210d24de9dSSimon Glass
5220d24de9dSSimon Glass    Returns:
5230d24de9dSSimon Glass        Full path to git top-level directory
5240d24de9dSSimon Glass
5250d24de9dSSimon Glass    This test makes sure that we are running tests in the right subdir
5260d24de9dSSimon Glass
527a970048eSDoug Anderson    >>> os.path.realpath(os.path.dirname(__file__)) == \
528a970048eSDoug Anderson            os.path.join(GetTopLevel(), 'tools', 'patman')
5290d24de9dSSimon Glass    True
5300d24de9dSSimon Glass    """
5310d24de9dSSimon Glass    return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
5320d24de9dSSimon Glass
5330d24de9dSSimon Glassdef GetAliasFile():
5340d24de9dSSimon Glass    """Gets the name of the git alias file.
5350d24de9dSSimon Glass
5360d24de9dSSimon Glass    Returns:
5370d24de9dSSimon Glass        Filename of git alias file, or None if none
5380d24de9dSSimon Glass    """
539dc191505SSimon Glass    fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
540dc191505SSimon Glass            raise_on_error=False)
5410d24de9dSSimon Glass    if fname:
5420d24de9dSSimon Glass        fname = os.path.join(GetTopLevel(), fname.strip())
5430d24de9dSSimon Glass    return fname
5440d24de9dSSimon Glass
54587d65558SVikram Narayanandef GetDefaultUserName():
54687d65558SVikram Narayanan    """Gets the user.name from .gitconfig file.
54787d65558SVikram Narayanan
54887d65558SVikram Narayanan    Returns:
54987d65558SVikram Narayanan        User name found in .gitconfig file, or None if none
55087d65558SVikram Narayanan    """
55187d65558SVikram Narayanan    uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
55287d65558SVikram Narayanan    return uname
55387d65558SVikram Narayanan
55487d65558SVikram Narayanandef GetDefaultUserEmail():
55587d65558SVikram Narayanan    """Gets the user.email from the global .gitconfig file.
55687d65558SVikram Narayanan
55787d65558SVikram Narayanan    Returns:
55887d65558SVikram Narayanan        User's email found in .gitconfig file, or None if none
55987d65558SVikram Narayanan    """
56087d65558SVikram Narayanan    uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
56187d65558SVikram Narayanan    return uemail
56287d65558SVikram Narayanan
5633871cd85SWu, Joshdef GetDefaultSubjectPrefix():
5643871cd85SWu, Josh    """Gets the format.subjectprefix from local .git/config file.
5653871cd85SWu, Josh
5663871cd85SWu, Josh    Returns:
5673871cd85SWu, Josh        Subject prefix found in local .git/config file, or None if none
5683871cd85SWu, Josh    """
5693871cd85SWu, Josh    sub_prefix = command.OutputOneLine('git', 'config', 'format.subjectprefix',
5703871cd85SWu, Josh                 raise_on_error=False)
5713871cd85SWu, Josh
5723871cd85SWu, Josh    return sub_prefix
5733871cd85SWu, Josh
5740d24de9dSSimon Glassdef Setup():
5750d24de9dSSimon Glass    """Set up git utils, by reading the alias files."""
5760d24de9dSSimon Glass    # Check for a git alias file also
5770b703dbcSSimon Glass    global use_no_decorate
5780b703dbcSSimon Glass
5790d24de9dSSimon Glass    alias_fname = GetAliasFile()
5800d24de9dSSimon Glass    if alias_fname:
5810d24de9dSSimon Glass        settings.ReadGitAliases(alias_fname)
582e49f14afSSimon Glass    cmd = LogCmd(None, count=0)
583e49f14afSSimon Glass    use_no_decorate = (command.RunPipe([cmd], raise_on_error=False)
584e49f14afSSimon Glass                       .return_code == 0)
5850d24de9dSSimon Glass
5865f6a1c42SSimon Glassdef GetHead():
5875f6a1c42SSimon Glass    """Get the hash of the current HEAD
5885f6a1c42SSimon Glass
5895f6a1c42SSimon Glass    Returns:
5905f6a1c42SSimon Glass        Hash of HEAD
5915f6a1c42SSimon Glass    """
5925f6a1c42SSimon Glass    return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
5935f6a1c42SSimon Glass
5940d24de9dSSimon Glassif __name__ == "__main__":
5950d24de9dSSimon Glass    import doctest
5960d24de9dSSimon Glass
5970d24de9dSSimon Glass    doctest.testmod()
598