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) 47*d4c8572bSSimon Glass 48*d4c8572bSSimon Glass # Add this in case we have a branch with the same name as a directory. 49*d4c8572bSSimon Glass # This avoids messages like this, for example: 50*d4c8572bSSimon Glass # fatal: ambiguous argument 'test': both revision and filename 51*d4c8572bSSimon Glass cmd.append('--') 52cda2a611SSimon Glass return cmd 530d24de9dSSimon Glass 540d24de9dSSimon Glassdef CountCommitsToBranch(): 550d24de9dSSimon Glass """Returns number of commits between HEAD and the tracking branch. 560d24de9dSSimon Glass 570d24de9dSSimon Glass This looks back to the tracking branch and works out the number of commits 580d24de9dSSimon Glass since then. 590d24de9dSSimon Glass 600d24de9dSSimon Glass Return: 610d24de9dSSimon Glass Number of patches that exist on top of the branch 620d24de9dSSimon Glass """ 63cda2a611SSimon Glass pipe = [LogCmd('@{upstream}..', oneline=True), 640d24de9dSSimon Glass ['wc', '-l']] 65a10fd93cSSimon Glass stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout 660d24de9dSSimon Glass patch_count = int(stdout) 670d24de9dSSimon Glass return patch_count 680d24de9dSSimon Glass 692a9e2c6aSSimon Glassdef NameRevision(commit_hash): 702a9e2c6aSSimon Glass """Gets the revision name for a commit 712a9e2c6aSSimon Glass 722a9e2c6aSSimon Glass Args: 732a9e2c6aSSimon Glass commit_hash: Commit hash to look up 742a9e2c6aSSimon Glass 752a9e2c6aSSimon Glass Return: 762a9e2c6aSSimon Glass Name of revision, if any, else None 772a9e2c6aSSimon Glass """ 782a9e2c6aSSimon Glass pipe = ['git', 'name-rev', commit_hash] 792a9e2c6aSSimon Glass stdout = command.RunPipe([pipe], capture=True, oneline=True).stdout 802a9e2c6aSSimon Glass 812a9e2c6aSSimon Glass # We expect a commit, a space, then a revision name 822a9e2c6aSSimon Glass name = stdout.split(' ')[1].strip() 832a9e2c6aSSimon Glass return name 842a9e2c6aSSimon Glass 852a9e2c6aSSimon Glassdef GuessUpstream(git_dir, branch): 862a9e2c6aSSimon Glass """Tries to guess the upstream for a branch 872a9e2c6aSSimon Glass 882a9e2c6aSSimon Glass This lists out top commits on a branch and tries to find a suitable 892a9e2c6aSSimon Glass upstream. It does this by looking for the first commit where 902a9e2c6aSSimon Glass 'git name-rev' returns a plain branch name, with no ! or ^ modifiers. 912a9e2c6aSSimon Glass 922a9e2c6aSSimon Glass Args: 932a9e2c6aSSimon Glass git_dir: Git directory containing repo 942a9e2c6aSSimon Glass branch: Name of branch 952a9e2c6aSSimon Glass 962a9e2c6aSSimon Glass Returns: 972a9e2c6aSSimon Glass Tuple: 982a9e2c6aSSimon Glass Name of upstream branch (e.g. 'upstream/master') or None if none 992a9e2c6aSSimon Glass Warning/error message, or None if none 1002a9e2c6aSSimon Glass """ 1012a9e2c6aSSimon Glass pipe = [LogCmd(branch, git_dir=git_dir, oneline=True, count=100)] 1022a9e2c6aSSimon Glass result = command.RunPipe(pipe, capture=True, capture_stderr=True, 1032a9e2c6aSSimon Glass raise_on_error=False) 1042a9e2c6aSSimon Glass if result.return_code: 1052a9e2c6aSSimon Glass return None, "Branch '%s' not found" % branch 1062a9e2c6aSSimon Glass for line in result.stdout.splitlines()[1:]: 1072a9e2c6aSSimon Glass commit_hash = line.split(' ')[0] 1082a9e2c6aSSimon Glass name = NameRevision(commit_hash) 1092a9e2c6aSSimon Glass if '~' not in name and '^' not in name: 1102a9e2c6aSSimon Glass if name.startswith('remotes/'): 1112a9e2c6aSSimon Glass name = name[8:] 1122a9e2c6aSSimon Glass return name, "Guessing upstream as '%s'" % name 1132a9e2c6aSSimon Glass return None, "Cannot find a suitable upstream for branch '%s'" % branch 1142a9e2c6aSSimon Glass 1155f6a1c42SSimon Glassdef GetUpstream(git_dir, branch): 1165f6a1c42SSimon Glass """Returns the name of the upstream for a branch 1175f6a1c42SSimon Glass 1185f6a1c42SSimon Glass Args: 1195f6a1c42SSimon Glass git_dir: Git directory containing repo 1205f6a1c42SSimon Glass branch: Name of branch 1215f6a1c42SSimon Glass 1225f6a1c42SSimon Glass Returns: 1232a9e2c6aSSimon Glass Tuple: 1245f6a1c42SSimon Glass Name of upstream branch (e.g. 'upstream/master') or None if none 1252a9e2c6aSSimon Glass Warning/error message, or None if none 1265f6a1c42SSimon Glass """ 127cce717a9SSimon Glass try: 1285f6a1c42SSimon Glass remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config', 1295f6a1c42SSimon Glass 'branch.%s.remote' % branch) 1305f6a1c42SSimon Glass merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config', 1315f6a1c42SSimon Glass 'branch.%s.merge' % branch) 132cce717a9SSimon Glass except: 1332a9e2c6aSSimon Glass upstream, msg = GuessUpstream(git_dir, branch) 1342a9e2c6aSSimon Glass return upstream, msg 135cce717a9SSimon Glass 1365f6a1c42SSimon Glass if remote == '.': 13771edbe5cSSimon Glass return merge, None 1385f6a1c42SSimon Glass elif remote and merge: 1395f6a1c42SSimon Glass leaf = merge.split('/')[-1] 1402a9e2c6aSSimon Glass return '%s/%s' % (remote, leaf), None 1415f6a1c42SSimon Glass else: 1425f6a1c42SSimon Glass raise ValueError, ("Cannot determine upstream branch for branch " 1435f6a1c42SSimon Glass "'%s' remote='%s', merge='%s'" % (branch, remote, merge)) 1445f6a1c42SSimon Glass 1455f6a1c42SSimon Glass 1465f6a1c42SSimon Glassdef GetRangeInBranch(git_dir, branch, include_upstream=False): 1475f6a1c42SSimon Glass """Returns an expression for the commits in the given branch. 1485f6a1c42SSimon Glass 1495f6a1c42SSimon Glass Args: 1505f6a1c42SSimon Glass git_dir: Directory containing git repo 1515f6a1c42SSimon Glass branch: Name of branch 1525f6a1c42SSimon Glass Return: 1535f6a1c42SSimon Glass Expression in the form 'upstream..branch' which can be used to 154cce717a9SSimon Glass access the commits. If the branch does not exist, returns None. 1555f6a1c42SSimon Glass """ 1562a9e2c6aSSimon Glass upstream, msg = GetUpstream(git_dir, branch) 157cce717a9SSimon Glass if not upstream: 1582a9e2c6aSSimon Glass return None, msg 1592a9e2c6aSSimon Glass rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch) 1602a9e2c6aSSimon Glass return rstr, msg 1615f6a1c42SSimon Glass 1625abab20dSSimon Glassdef CountCommitsInRange(git_dir, range_expr): 1635abab20dSSimon Glass """Returns the number of commits in the given range. 1645abab20dSSimon Glass 1655abab20dSSimon Glass Args: 1665abab20dSSimon Glass git_dir: Directory containing git repo 1675abab20dSSimon Glass range_expr: Range to check 1685abab20dSSimon Glass Return: 1695abab20dSSimon Glass Number of patches that exist in the supplied rangem or None if none 1705abab20dSSimon Glass were found 1715abab20dSSimon Glass """ 1725abab20dSSimon Glass pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True)] 1735abab20dSSimon Glass result = command.RunPipe(pipe, capture=True, capture_stderr=True, 1745abab20dSSimon Glass raise_on_error=False) 1755abab20dSSimon Glass if result.return_code: 1765abab20dSSimon Glass return None, "Range '%s' not found or is invalid" % range_expr 1775abab20dSSimon Glass patch_count = len(result.stdout.splitlines()) 1785abab20dSSimon Glass return patch_count, None 1795abab20dSSimon Glass 1805f6a1c42SSimon Glassdef CountCommitsInBranch(git_dir, branch, include_upstream=False): 1815f6a1c42SSimon Glass """Returns the number of commits in the given branch. 1825f6a1c42SSimon Glass 1835f6a1c42SSimon Glass Args: 1845f6a1c42SSimon Glass git_dir: Directory containing git repo 1855f6a1c42SSimon Glass branch: Name of branch 1865f6a1c42SSimon Glass Return: 187cce717a9SSimon Glass Number of patches that exist on top of the branch, or None if the 188cce717a9SSimon Glass branch does not exist. 1895f6a1c42SSimon Glass """ 1902a9e2c6aSSimon Glass range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream) 191cce717a9SSimon Glass if not range_expr: 1922a9e2c6aSSimon Glass return None, msg 1935abab20dSSimon Glass return CountCommitsInRange(git_dir, range_expr) 1945f6a1c42SSimon Glass 1955f6a1c42SSimon Glassdef CountCommits(commit_range): 1965f6a1c42SSimon Glass """Returns the number of commits in the given range. 1975f6a1c42SSimon Glass 1985f6a1c42SSimon Glass Args: 1995f6a1c42SSimon Glass commit_range: Range of commits to count (e.g. 'HEAD..base') 2005f6a1c42SSimon Glass Return: 2015f6a1c42SSimon Glass Number of patches that exist on top of the branch 2025f6a1c42SSimon Glass """ 203cda2a611SSimon Glass pipe = [LogCmd(commit_range, oneline=True), 2045f6a1c42SSimon Glass ['wc', '-l']] 2055f6a1c42SSimon Glass stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout 2065f6a1c42SSimon Glass patch_count = int(stdout) 2075f6a1c42SSimon Glass return patch_count 2085f6a1c42SSimon Glass 2095f6a1c42SSimon Glassdef Checkout(commit_hash, git_dir=None, work_tree=None, force=False): 2105f6a1c42SSimon Glass """Checkout the selected commit for this build 2115f6a1c42SSimon Glass 2125f6a1c42SSimon Glass Args: 2135f6a1c42SSimon Glass commit_hash: Commit hash to check out 2145f6a1c42SSimon Glass """ 2155f6a1c42SSimon Glass pipe = ['git'] 2165f6a1c42SSimon Glass if git_dir: 2175f6a1c42SSimon Glass pipe.extend(['--git-dir', git_dir]) 2185f6a1c42SSimon Glass if work_tree: 2195f6a1c42SSimon Glass pipe.extend(['--work-tree', work_tree]) 2205f6a1c42SSimon Glass pipe.append('checkout') 2215f6a1c42SSimon Glass if force: 2225f6a1c42SSimon Glass pipe.append('-f') 2235f6a1c42SSimon Glass pipe.append(commit_hash) 224ddaf5c8fSSimon Glass result = command.RunPipe([pipe], capture=True, raise_on_error=False, 225ddaf5c8fSSimon Glass capture_stderr=True) 2265f6a1c42SSimon Glass if result.return_code != 0: 2275f6a1c42SSimon Glass raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr) 2285f6a1c42SSimon Glass 2295f6a1c42SSimon Glassdef Clone(git_dir, output_dir): 2305f6a1c42SSimon Glass """Checkout the selected commit for this build 2315f6a1c42SSimon Glass 2325f6a1c42SSimon Glass Args: 2335f6a1c42SSimon Glass commit_hash: Commit hash to check out 2345f6a1c42SSimon Glass """ 2355f6a1c42SSimon Glass pipe = ['git', 'clone', git_dir, '.'] 236ddaf5c8fSSimon Glass result = command.RunPipe([pipe], capture=True, cwd=output_dir, 237ddaf5c8fSSimon Glass capture_stderr=True) 2385f6a1c42SSimon Glass if result.return_code != 0: 2395f6a1c42SSimon Glass raise OSError, 'git clone: %s' % result.stderr 2405f6a1c42SSimon Glass 2415f6a1c42SSimon Glassdef Fetch(git_dir=None, work_tree=None): 2425f6a1c42SSimon Glass """Fetch from the origin repo 2435f6a1c42SSimon Glass 2445f6a1c42SSimon Glass Args: 2455f6a1c42SSimon Glass commit_hash: Commit hash to check out 2465f6a1c42SSimon Glass """ 2475f6a1c42SSimon Glass pipe = ['git'] 2485f6a1c42SSimon Glass if git_dir: 2495f6a1c42SSimon Glass pipe.extend(['--git-dir', git_dir]) 2505f6a1c42SSimon Glass if work_tree: 2515f6a1c42SSimon Glass pipe.extend(['--work-tree', work_tree]) 2525f6a1c42SSimon Glass pipe.append('fetch') 253ddaf5c8fSSimon Glass result = command.RunPipe([pipe], capture=True, capture_stderr=True) 2545f6a1c42SSimon Glass if result.return_code != 0: 2555f6a1c42SSimon Glass raise OSError, 'git fetch: %s' % result.stderr 2565f6a1c42SSimon Glass 2570d24de9dSSimon Glassdef CreatePatches(start, count, series): 2580d24de9dSSimon Glass """Create a series of patches from the top of the current branch. 2590d24de9dSSimon Glass 2600d24de9dSSimon Glass The patch files are written to the current directory using 2610d24de9dSSimon Glass git format-patch. 2620d24de9dSSimon Glass 2630d24de9dSSimon Glass Args: 2640d24de9dSSimon Glass start: Commit to start from: 0=HEAD, 1=next one, etc. 2650d24de9dSSimon Glass count: number of commits to include 2660d24de9dSSimon Glass Return: 2670d24de9dSSimon Glass Filename of cover letter 2680d24de9dSSimon Glass List of filenames of patch files 2690d24de9dSSimon Glass """ 2700d24de9dSSimon Glass if series.get('version'): 2710d24de9dSSimon Glass version = '%s ' % series['version'] 2728d3595a4SMasahiro Yamada cmd = ['git', 'format-patch', '-M', '--signoff'] 2730d24de9dSSimon Glass if series.get('cover'): 2740d24de9dSSimon Glass cmd.append('--cover-letter') 2750d24de9dSSimon Glass prefix = series.GetPatchPrefix() 2760d24de9dSSimon Glass if prefix: 2770d24de9dSSimon Glass cmd += ['--subject-prefix=%s' % prefix] 2780d24de9dSSimon Glass cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)] 2790d24de9dSSimon Glass 2800d24de9dSSimon Glass stdout = command.RunList(cmd) 2810d24de9dSSimon Glass files = stdout.splitlines() 2820d24de9dSSimon Glass 2830d24de9dSSimon Glass # We have an extra file if there is a cover letter 2840d24de9dSSimon Glass if series.get('cover'): 2850d24de9dSSimon Glass return files[0], files[1:] 2860d24de9dSSimon Glass else: 2870d24de9dSSimon Glass return None, files 2880d24de9dSSimon Glass 289a1318f7cSSimon Glassdef BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True): 2900d24de9dSSimon Glass """Build a list of email addresses based on an input list. 2910d24de9dSSimon Glass 2920d24de9dSSimon Glass Takes a list of email addresses and aliases, and turns this into a list 2930d24de9dSSimon Glass of only email address, by resolving any aliases that are present. 2940d24de9dSSimon Glass 2950d24de9dSSimon Glass If the tag is given, then each email address is prepended with this 2960d24de9dSSimon Glass tag and a space. If the tag starts with a minus sign (indicating a 2970d24de9dSSimon Glass command line parameter) then the email address is quoted. 2980d24de9dSSimon Glass 2990d24de9dSSimon Glass Args: 3000d24de9dSSimon Glass in_list: List of aliases/email addresses 3010d24de9dSSimon Glass tag: Text to put before each address 302a1318f7cSSimon Glass alias: Alias dictionary 303a1318f7cSSimon Glass raise_on_error: True to raise an error when an alias fails to match, 304a1318f7cSSimon Glass False to just print a message. 3050d24de9dSSimon Glass 3060d24de9dSSimon Glass Returns: 3070d24de9dSSimon Glass List of email addresses 3080d24de9dSSimon Glass 3090d24de9dSSimon Glass >>> alias = {} 3100d24de9dSSimon Glass >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 3110d24de9dSSimon Glass >>> alias['john'] = ['j.bloggs@napier.co.nz'] 3120d24de9dSSimon Glass >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>'] 3130d24de9dSSimon Glass >>> alias['boys'] = ['fred', ' john'] 3140d24de9dSSimon Glass >>> alias['all'] = ['fred ', 'john', ' mary '] 3150d24de9dSSimon Glass >>> BuildEmailList(['john', 'mary'], None, alias) 3160d24de9dSSimon Glass ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>'] 3170d24de9dSSimon Glass >>> BuildEmailList(['john', 'mary'], '--to', alias) 3180d24de9dSSimon Glass ['--to "j.bloggs@napier.co.nz"', \ 3190d24de9dSSimon Glass'--to "Mary Poppins <m.poppins@cloud.net>"'] 3200d24de9dSSimon Glass >>> BuildEmailList(['john', 'mary'], 'Cc', alias) 3210d24de9dSSimon Glass ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>'] 3220d24de9dSSimon Glass """ 3230d24de9dSSimon Glass quote = '"' if tag and tag[0] == '-' else '' 3240d24de9dSSimon Glass raw = [] 3250d24de9dSSimon Glass for item in in_list: 326a1318f7cSSimon Glass raw += LookupEmail(item, alias, raise_on_error=raise_on_error) 3270d24de9dSSimon Glass result = [] 3280d24de9dSSimon Glass for item in raw: 3290d24de9dSSimon Glass if not item in result: 3300d24de9dSSimon Glass result.append(item) 3310d24de9dSSimon Glass if tag: 3320d24de9dSSimon Glass return ['%s %s%s%s' % (tag, quote, email, quote) for email in result] 3330d24de9dSSimon Glass return result 3340d24de9dSSimon Glass 335a1318f7cSSimon Glassdef EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname, 33627067a46SMateusz Kulikowski self_only=False, alias=None, in_reply_to=None, thread=False): 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) 3520d24de9dSSimon Glass 3530d24de9dSSimon Glass Returns: 3540d24de9dSSimon Glass Git command that was/would be run 3550d24de9dSSimon Glass 356a970048eSDoug Anderson # For the duration of this doctest pretend that we ran patman with ./patman 357a970048eSDoug Anderson >>> _old_argv0 = sys.argv[0] 358a970048eSDoug Anderson >>> sys.argv[0] = './patman' 359a970048eSDoug Anderson 3600d24de9dSSimon Glass >>> alias = {} 3610d24de9dSSimon Glass >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 3620d24de9dSSimon Glass >>> alias['john'] = ['j.bloggs@napier.co.nz'] 3630d24de9dSSimon Glass >>> alias['mary'] = ['m.poppins@cloud.net'] 3640d24de9dSSimon Glass >>> alias['boys'] = ['fred', ' john'] 3650d24de9dSSimon Glass >>> alias['all'] = ['fred ', 'john', ' mary '] 3660d24de9dSSimon Glass >>> alias[os.getenv('USER')] = ['this-is-me@me.com'] 3670d24de9dSSimon Glass >>> series = series.Series() 3680d24de9dSSimon Glass >>> series.to = ['fred'] 3690d24de9dSSimon Glass >>> series.cc = ['mary'] 370a1318f7cSSimon Glass >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 371a1318f7cSSimon Glass False, alias) 3720d24de9dSSimon Glass 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 3730d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 374a1318f7cSSimon Glass >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \ 375a1318f7cSSimon Glass alias) 3760d24de9dSSimon Glass 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 3770d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1' 3780d24de9dSSimon Glass >>> series.cc = ['all'] 379a1318f7cSSimon Glass >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 380a1318f7cSSimon Glass True, alias) 3810d24de9dSSimon Glass 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \ 3820d24de9dSSimon Glass--cc-cmd cc-fname" cover p1 p2' 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"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \ 3870d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 388a970048eSDoug Anderson 389a970048eSDoug Anderson # Restore argv[0] since we clobbered it. 390a970048eSDoug Anderson >>> sys.argv[0] = _old_argv0 3910d24de9dSSimon Glass """ 392a1318f7cSSimon Glass to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error) 3930d24de9dSSimon Glass if not to: 394ee860c60SMasahiro Yamada git_config_to = command.Output('git', 'config', 'sendemail.to') 395ee860c60SMasahiro Yamada if not git_config_to: 396ee860c60SMasahiro Yamada print ("No recipient.\n" 397ee860c60SMasahiro Yamada "Please add something like this to a commit\n" 398ee860c60SMasahiro Yamada "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n" 399ee860c60SMasahiro Yamada "Or do something like this\n" 400ee860c60SMasahiro Yamada "git config sendemail.to u-boot@lists.denx.de") 4010d24de9dSSimon Glass return 4022181830fSPeter Tyser cc = BuildEmailList(list(set(series.get('cc')) - set(series.get('to'))), 4032181830fSPeter Tyser '--cc', alias, raise_on_error) 4040d24de9dSSimon Glass if self_only: 405a1318f7cSSimon Glass to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error) 4060d24de9dSSimon Glass cc = [] 4070d24de9dSSimon Glass cmd = ['git', 'send-email', '--annotate'] 4086d819925SDoug Anderson if in_reply_to: 4096d819925SDoug Anderson cmd.append('--in-reply-to="%s"' % in_reply_to) 41027067a46SMateusz Kulikowski if thread: 41127067a46SMateusz Kulikowski cmd.append('--thread') 4126d819925SDoug Anderson 4130d24de9dSSimon Glass cmd += to 4140d24de9dSSimon Glass cmd += cc 4150d24de9dSSimon Glass cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)] 4160d24de9dSSimon Glass if cover_fname: 4170d24de9dSSimon Glass cmd.append(cover_fname) 4180d24de9dSSimon Glass cmd += args 4190d24de9dSSimon Glass str = ' '.join(cmd) 4200d24de9dSSimon Glass if not dry_run: 4210d24de9dSSimon Glass os.system(str) 4220d24de9dSSimon Glass return str 4230d24de9dSSimon Glass 4240d24de9dSSimon Glass 425a1318f7cSSimon Glassdef LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0): 4260d24de9dSSimon Glass """If an email address is an alias, look it up and return the full name 4270d24de9dSSimon Glass 4280d24de9dSSimon Glass TODO: Why not just use git's own alias feature? 4290d24de9dSSimon Glass 4300d24de9dSSimon Glass Args: 4310d24de9dSSimon Glass lookup_name: Alias or email address to look up 432a1318f7cSSimon Glass alias: Dictionary containing aliases (None to use settings default) 433a1318f7cSSimon Glass raise_on_error: True to raise an error when an alias fails to match, 434a1318f7cSSimon Glass False to just print a message. 4350d24de9dSSimon Glass 4360d24de9dSSimon Glass Returns: 4370d24de9dSSimon Glass tuple: 4380d24de9dSSimon Glass list containing a list of email addresses 4390d24de9dSSimon Glass 4400d24de9dSSimon Glass Raises: 4410d24de9dSSimon Glass OSError if a recursive alias reference was found 4420d24de9dSSimon Glass ValueError if an alias was not found 4430d24de9dSSimon Glass 4440d24de9dSSimon Glass >>> alias = {} 4450d24de9dSSimon Glass >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 4460d24de9dSSimon Glass >>> alias['john'] = ['j.bloggs@napier.co.nz'] 4470d24de9dSSimon Glass >>> alias['mary'] = ['m.poppins@cloud.net'] 4480d24de9dSSimon Glass >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz'] 4490d24de9dSSimon Glass >>> alias['all'] = ['fred ', 'john', ' mary '] 4500d24de9dSSimon Glass >>> alias['loop'] = ['other', 'john', ' mary '] 4510d24de9dSSimon Glass >>> alias['other'] = ['loop', 'john', ' mary '] 4520d24de9dSSimon Glass >>> LookupEmail('mary', alias) 4530d24de9dSSimon Glass ['m.poppins@cloud.net'] 4540d24de9dSSimon Glass >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias) 4550d24de9dSSimon Glass ['arthur.wellesley@howe.ro.uk'] 4560d24de9dSSimon Glass >>> LookupEmail('boys', alias) 4570d24de9dSSimon Glass ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz'] 4580d24de9dSSimon Glass >>> LookupEmail('all', alias) 4590d24de9dSSimon Glass ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net'] 4600d24de9dSSimon Glass >>> LookupEmail('odd', alias) 4610d24de9dSSimon Glass Traceback (most recent call last): 4620d24de9dSSimon Glass ... 4630d24de9dSSimon Glass ValueError: Alias 'odd' not found 4640d24de9dSSimon Glass >>> LookupEmail('loop', alias) 4650d24de9dSSimon Glass Traceback (most recent call last): 4660d24de9dSSimon Glass ... 4670d24de9dSSimon Glass OSError: Recursive email alias at 'other' 468a1318f7cSSimon Glass >>> LookupEmail('odd', alias, raise_on_error=False) 469e752edcbSSimon Glass Alias 'odd' not found 470a1318f7cSSimon Glass [] 471a1318f7cSSimon Glass >>> # In this case the loop part will effectively be ignored. 472a1318f7cSSimon Glass >>> LookupEmail('loop', alias, raise_on_error=False) 473e752edcbSSimon Glass Recursive email alias at 'other' 474e752edcbSSimon Glass Recursive email alias at 'john' 475e752edcbSSimon Glass Recursive email alias at 'mary' 476a1318f7cSSimon Glass ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net'] 4770d24de9dSSimon Glass """ 4780d24de9dSSimon Glass if not alias: 4790d24de9dSSimon Glass alias = settings.alias 4800d24de9dSSimon Glass lookup_name = lookup_name.strip() 4810d24de9dSSimon Glass if '@' in lookup_name: # Perhaps a real email address 4820d24de9dSSimon Glass return [lookup_name] 4830d24de9dSSimon Glass 4840d24de9dSSimon Glass lookup_name = lookup_name.lower() 485a1318f7cSSimon Glass col = terminal.Color() 4860d24de9dSSimon Glass 4870d24de9dSSimon Glass out_list = [] 488a1318f7cSSimon Glass if level > 10: 489a1318f7cSSimon Glass msg = "Recursive email alias at '%s'" % lookup_name 490a1318f7cSSimon Glass if raise_on_error: 491a1318f7cSSimon Glass raise OSError, msg 492a1318f7cSSimon Glass else: 493a1318f7cSSimon Glass print col.Color(col.RED, msg) 494a1318f7cSSimon Glass return out_list 495a1318f7cSSimon Glass 4960d24de9dSSimon Glass if lookup_name: 4970d24de9dSSimon Glass if not lookup_name in alias: 498a1318f7cSSimon Glass msg = "Alias '%s' not found" % lookup_name 499a1318f7cSSimon Glass if raise_on_error: 500a1318f7cSSimon Glass raise ValueError, msg 501a1318f7cSSimon Glass else: 502a1318f7cSSimon Glass print col.Color(col.RED, msg) 503a1318f7cSSimon Glass return out_list 5040d24de9dSSimon Glass for item in alias[lookup_name]: 505a1318f7cSSimon Glass todo = LookupEmail(item, alias, raise_on_error, level + 1) 5060d24de9dSSimon Glass for new_item in todo: 5070d24de9dSSimon Glass if not new_item in out_list: 5080d24de9dSSimon Glass out_list.append(new_item) 5090d24de9dSSimon Glass 5100d24de9dSSimon Glass #print "No match for alias '%s'" % lookup_name 5110d24de9dSSimon Glass return out_list 5120d24de9dSSimon Glass 5130d24de9dSSimon Glassdef GetTopLevel(): 5140d24de9dSSimon Glass """Return name of top-level directory for this git repo. 5150d24de9dSSimon Glass 5160d24de9dSSimon Glass Returns: 5170d24de9dSSimon Glass Full path to git top-level directory 5180d24de9dSSimon Glass 5190d24de9dSSimon Glass This test makes sure that we are running tests in the right subdir 5200d24de9dSSimon Glass 521a970048eSDoug Anderson >>> os.path.realpath(os.path.dirname(__file__)) == \ 522a970048eSDoug Anderson os.path.join(GetTopLevel(), 'tools', 'patman') 5230d24de9dSSimon Glass True 5240d24de9dSSimon Glass """ 5250d24de9dSSimon Glass return command.OutputOneLine('git', 'rev-parse', '--show-toplevel') 5260d24de9dSSimon Glass 5270d24de9dSSimon Glassdef GetAliasFile(): 5280d24de9dSSimon Glass """Gets the name of the git alias file. 5290d24de9dSSimon Glass 5300d24de9dSSimon Glass Returns: 5310d24de9dSSimon Glass Filename of git alias file, or None if none 5320d24de9dSSimon Glass """ 533dc191505SSimon Glass fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile', 534dc191505SSimon Glass raise_on_error=False) 5350d24de9dSSimon Glass if fname: 5360d24de9dSSimon Glass fname = os.path.join(GetTopLevel(), fname.strip()) 5370d24de9dSSimon Glass return fname 5380d24de9dSSimon Glass 53987d65558SVikram Narayanandef GetDefaultUserName(): 54087d65558SVikram Narayanan """Gets the user.name from .gitconfig file. 54187d65558SVikram Narayanan 54287d65558SVikram Narayanan Returns: 54387d65558SVikram Narayanan User name found in .gitconfig file, or None if none 54487d65558SVikram Narayanan """ 54587d65558SVikram Narayanan uname = command.OutputOneLine('git', 'config', '--global', 'user.name') 54687d65558SVikram Narayanan return uname 54787d65558SVikram Narayanan 54887d65558SVikram Narayanandef GetDefaultUserEmail(): 54987d65558SVikram Narayanan """Gets the user.email from the global .gitconfig file. 55087d65558SVikram Narayanan 55187d65558SVikram Narayanan Returns: 55287d65558SVikram Narayanan User's email found in .gitconfig file, or None if none 55387d65558SVikram Narayanan """ 55487d65558SVikram Narayanan uemail = command.OutputOneLine('git', 'config', '--global', 'user.email') 55587d65558SVikram Narayanan return uemail 55687d65558SVikram Narayanan 5573871cd85SWu, Joshdef GetDefaultSubjectPrefix(): 5583871cd85SWu, Josh """Gets the format.subjectprefix from local .git/config file. 5593871cd85SWu, Josh 5603871cd85SWu, Josh Returns: 5613871cd85SWu, Josh Subject prefix found in local .git/config file, or None if none 5623871cd85SWu, Josh """ 5633871cd85SWu, Josh sub_prefix = command.OutputOneLine('git', 'config', 'format.subjectprefix', 5643871cd85SWu, Josh raise_on_error=False) 5653871cd85SWu, Josh 5663871cd85SWu, Josh return sub_prefix 5673871cd85SWu, Josh 5680d24de9dSSimon Glassdef Setup(): 5690d24de9dSSimon Glass """Set up git utils, by reading the alias files.""" 5700d24de9dSSimon Glass # Check for a git alias file also 5710b703dbcSSimon Glass global use_no_decorate 5720b703dbcSSimon Glass 5730d24de9dSSimon Glass alias_fname = GetAliasFile() 5740d24de9dSSimon Glass if alias_fname: 5750d24de9dSSimon Glass settings.ReadGitAliases(alias_fname) 576e49f14afSSimon Glass cmd = LogCmd(None, count=0) 577e49f14afSSimon Glass use_no_decorate = (command.RunPipe([cmd], raise_on_error=False) 578e49f14afSSimon Glass .return_code == 0) 5790d24de9dSSimon Glass 5805f6a1c42SSimon Glassdef GetHead(): 5815f6a1c42SSimon Glass """Get the hash of the current HEAD 5825f6a1c42SSimon Glass 5835f6a1c42SSimon Glass Returns: 5845f6a1c42SSimon Glass Hash of HEAD 5855f6a1c42SSimon Glass """ 5865f6a1c42SSimon Glass return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H') 5875f6a1c42SSimon Glass 5880d24de9dSSimon Glassif __name__ == "__main__": 5890d24de9dSSimon Glass import doctest 5900d24de9dSSimon Glass 5910d24de9dSSimon Glass doctest.testmod() 592