10d24de9dSSimon Glass# Copyright (c) 2011 The Chromium OS Authors. 20d24de9dSSimon Glass# 31a459660SWolfgang Denk# SPDX-License-Identifier: GPL-2.0+ 40d24de9dSSimon Glass# 50d24de9dSSimon Glass 60d24de9dSSimon Glassimport command 70d24de9dSSimon Glassimport re 80d24de9dSSimon Glassimport os 90d24de9dSSimon Glassimport series 100d24de9dSSimon Glassimport subprocess 110d24de9dSSimon Glassimport sys 120d24de9dSSimon Glassimport terminal 130d24de9dSSimon Glass 14757f64a8SSimon Glassimport checkpatch 155f6a1c42SSimon Glassimport settings 165f6a1c42SSimon Glass 17*cda2a611SSimon Glassdef LogCmd(commit_range, git_dir=None, oneline=False, reverse=False, 18*cda2a611SSimon Glass count=None): 19*cda2a611SSimon Glass """Create a command to perform a 'git log' 20*cda2a611SSimon Glass 21*cda2a611SSimon Glass Args: 22*cda2a611SSimon Glass commit_range: Range expression to use for log, None for none 23*cda2a611SSimon Glass git_dir: Path to git repositiory (None to use default) 24*cda2a611SSimon Glass oneline: True to use --oneline, else False 25*cda2a611SSimon Glass reverse: True to reverse the log (--reverse) 26*cda2a611SSimon Glass count: Number of commits to list, or None for no limit 27*cda2a611SSimon Glass Return: 28*cda2a611SSimon Glass List containing command and arguments to run 29*cda2a611SSimon Glass """ 30*cda2a611SSimon Glass cmd = ['git'] 31*cda2a611SSimon Glass if git_dir: 32*cda2a611SSimon Glass cmd += ['--git-dir', git_dir] 33*cda2a611SSimon Glass cmd += ['log', '--no-color'] 34*cda2a611SSimon Glass if oneline: 35*cda2a611SSimon Glass cmd.append('--oneline') 36*cda2a611SSimon Glass cmd.append('--no-decorate') 37*cda2a611SSimon Glass if count is not None: 38*cda2a611SSimon Glass cmd.append('-n%d' % count) 39*cda2a611SSimon Glass if commit_range: 40*cda2a611SSimon Glass cmd.append(commit_range) 41*cda2a611SSimon Glass return cmd 420d24de9dSSimon Glass 430d24de9dSSimon Glassdef CountCommitsToBranch(): 440d24de9dSSimon Glass """Returns number of commits between HEAD and the tracking branch. 450d24de9dSSimon Glass 460d24de9dSSimon Glass This looks back to the tracking branch and works out the number of commits 470d24de9dSSimon Glass since then. 480d24de9dSSimon Glass 490d24de9dSSimon Glass Return: 500d24de9dSSimon Glass Number of patches that exist on top of the branch 510d24de9dSSimon Glass """ 52*cda2a611SSimon Glass pipe = [LogCmd('@{upstream}..', oneline=True), 530d24de9dSSimon Glass ['wc', '-l']] 54a10fd93cSSimon Glass stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout 550d24de9dSSimon Glass patch_count = int(stdout) 560d24de9dSSimon Glass return patch_count 570d24de9dSSimon Glass 585f6a1c42SSimon Glassdef GetUpstream(git_dir, branch): 595f6a1c42SSimon Glass """Returns the name of the upstream for a branch 605f6a1c42SSimon Glass 615f6a1c42SSimon Glass Args: 625f6a1c42SSimon Glass git_dir: Git directory containing repo 635f6a1c42SSimon Glass branch: Name of branch 645f6a1c42SSimon Glass 655f6a1c42SSimon Glass Returns: 665f6a1c42SSimon Glass Name of upstream branch (e.g. 'upstream/master') or None if none 675f6a1c42SSimon Glass """ 68cce717a9SSimon Glass try: 695f6a1c42SSimon Glass remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config', 705f6a1c42SSimon Glass 'branch.%s.remote' % branch) 715f6a1c42SSimon Glass merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config', 725f6a1c42SSimon Glass 'branch.%s.merge' % branch) 73cce717a9SSimon Glass except: 74cce717a9SSimon Glass return None 75cce717a9SSimon Glass 765f6a1c42SSimon Glass if remote == '.': 775f6a1c42SSimon Glass return merge 785f6a1c42SSimon Glass elif remote and merge: 795f6a1c42SSimon Glass leaf = merge.split('/')[-1] 805f6a1c42SSimon Glass return '%s/%s' % (remote, leaf) 815f6a1c42SSimon Glass else: 825f6a1c42SSimon Glass raise ValueError, ("Cannot determine upstream branch for branch " 835f6a1c42SSimon Glass "'%s' remote='%s', merge='%s'" % (branch, remote, merge)) 845f6a1c42SSimon Glass 855f6a1c42SSimon Glass 865f6a1c42SSimon Glassdef GetRangeInBranch(git_dir, branch, include_upstream=False): 875f6a1c42SSimon Glass """Returns an expression for the commits in the given branch. 885f6a1c42SSimon Glass 895f6a1c42SSimon Glass Args: 905f6a1c42SSimon Glass git_dir: Directory containing git repo 915f6a1c42SSimon Glass branch: Name of branch 925f6a1c42SSimon Glass Return: 935f6a1c42SSimon Glass Expression in the form 'upstream..branch' which can be used to 94cce717a9SSimon Glass access the commits. If the branch does not exist, returns None. 955f6a1c42SSimon Glass """ 965f6a1c42SSimon Glass upstream = GetUpstream(git_dir, branch) 97cce717a9SSimon Glass if not upstream: 98cce717a9SSimon Glass return None 995f6a1c42SSimon Glass return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch) 1005f6a1c42SSimon Glass 1015f6a1c42SSimon Glassdef CountCommitsInBranch(git_dir, branch, include_upstream=False): 1025f6a1c42SSimon Glass """Returns the number of commits in the given branch. 1035f6a1c42SSimon Glass 1045f6a1c42SSimon Glass Args: 1055f6a1c42SSimon Glass git_dir: Directory containing git repo 1065f6a1c42SSimon Glass branch: Name of branch 1075f6a1c42SSimon Glass Return: 108cce717a9SSimon Glass Number of patches that exist on top of the branch, or None if the 109cce717a9SSimon Glass branch does not exist. 1105f6a1c42SSimon Glass """ 1115f6a1c42SSimon Glass range_expr = GetRangeInBranch(git_dir, branch, include_upstream) 112cce717a9SSimon Glass if not range_expr: 113cce717a9SSimon Glass return None 114*cda2a611SSimon Glass pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True), 1155f6a1c42SSimon Glass ['wc', '-l']] 1165f6a1c42SSimon Glass result = command.RunPipe(pipe, capture=True, oneline=True) 1175f6a1c42SSimon Glass patch_count = int(result.stdout) 1185f6a1c42SSimon Glass return patch_count 1195f6a1c42SSimon Glass 1205f6a1c42SSimon Glassdef CountCommits(commit_range): 1215f6a1c42SSimon Glass """Returns the number of commits in the given range. 1225f6a1c42SSimon Glass 1235f6a1c42SSimon Glass Args: 1245f6a1c42SSimon Glass commit_range: Range of commits to count (e.g. 'HEAD..base') 1255f6a1c42SSimon Glass Return: 1265f6a1c42SSimon Glass Number of patches that exist on top of the branch 1275f6a1c42SSimon Glass """ 128*cda2a611SSimon Glass pipe = [LogCmd(commit_range, oneline=True), 1295f6a1c42SSimon Glass ['wc', '-l']] 1305f6a1c42SSimon Glass stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout 1315f6a1c42SSimon Glass patch_count = int(stdout) 1325f6a1c42SSimon Glass return patch_count 1335f6a1c42SSimon Glass 1345f6a1c42SSimon Glassdef Checkout(commit_hash, git_dir=None, work_tree=None, force=False): 1355f6a1c42SSimon Glass """Checkout the selected commit for this build 1365f6a1c42SSimon Glass 1375f6a1c42SSimon Glass Args: 1385f6a1c42SSimon Glass commit_hash: Commit hash to check out 1395f6a1c42SSimon Glass """ 1405f6a1c42SSimon Glass pipe = ['git'] 1415f6a1c42SSimon Glass if git_dir: 1425f6a1c42SSimon Glass pipe.extend(['--git-dir', git_dir]) 1435f6a1c42SSimon Glass if work_tree: 1445f6a1c42SSimon Glass pipe.extend(['--work-tree', work_tree]) 1455f6a1c42SSimon Glass pipe.append('checkout') 1465f6a1c42SSimon Glass if force: 1475f6a1c42SSimon Glass pipe.append('-f') 1485f6a1c42SSimon Glass pipe.append(commit_hash) 1495f6a1c42SSimon Glass result = command.RunPipe([pipe], capture=True, raise_on_error=False) 1505f6a1c42SSimon Glass if result.return_code != 0: 1515f6a1c42SSimon Glass raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr) 1525f6a1c42SSimon Glass 1535f6a1c42SSimon Glassdef Clone(git_dir, output_dir): 1545f6a1c42SSimon Glass """Checkout the selected commit for this build 1555f6a1c42SSimon Glass 1565f6a1c42SSimon Glass Args: 1575f6a1c42SSimon Glass commit_hash: Commit hash to check out 1585f6a1c42SSimon Glass """ 1595f6a1c42SSimon Glass pipe = ['git', 'clone', git_dir, '.'] 1605f6a1c42SSimon Glass result = command.RunPipe([pipe], capture=True, cwd=output_dir) 1615f6a1c42SSimon Glass if result.return_code != 0: 1625f6a1c42SSimon Glass raise OSError, 'git clone: %s' % result.stderr 1635f6a1c42SSimon Glass 1645f6a1c42SSimon Glassdef Fetch(git_dir=None, work_tree=None): 1655f6a1c42SSimon Glass """Fetch from the origin repo 1665f6a1c42SSimon Glass 1675f6a1c42SSimon Glass Args: 1685f6a1c42SSimon Glass commit_hash: Commit hash to check out 1695f6a1c42SSimon Glass """ 1705f6a1c42SSimon Glass pipe = ['git'] 1715f6a1c42SSimon Glass if git_dir: 1725f6a1c42SSimon Glass pipe.extend(['--git-dir', git_dir]) 1735f6a1c42SSimon Glass if work_tree: 1745f6a1c42SSimon Glass pipe.extend(['--work-tree', work_tree]) 1755f6a1c42SSimon Glass pipe.append('fetch') 1765f6a1c42SSimon Glass result = command.RunPipe([pipe], capture=True) 1775f6a1c42SSimon Glass if result.return_code != 0: 1785f6a1c42SSimon Glass raise OSError, 'git fetch: %s' % result.stderr 1795f6a1c42SSimon Glass 1800d24de9dSSimon Glassdef CreatePatches(start, count, series): 1810d24de9dSSimon Glass """Create a series of patches from the top of the current branch. 1820d24de9dSSimon Glass 1830d24de9dSSimon Glass The patch files are written to the current directory using 1840d24de9dSSimon Glass git format-patch. 1850d24de9dSSimon Glass 1860d24de9dSSimon Glass Args: 1870d24de9dSSimon Glass start: Commit to start from: 0=HEAD, 1=next one, etc. 1880d24de9dSSimon Glass count: number of commits to include 1890d24de9dSSimon Glass Return: 1900d24de9dSSimon Glass Filename of cover letter 1910d24de9dSSimon Glass List of filenames of patch files 1920d24de9dSSimon Glass """ 1930d24de9dSSimon Glass if series.get('version'): 1940d24de9dSSimon Glass version = '%s ' % series['version'] 1950d24de9dSSimon Glass cmd = ['git', 'format-patch', '-M', '--signoff'] 1960d24de9dSSimon Glass if series.get('cover'): 1970d24de9dSSimon Glass cmd.append('--cover-letter') 1980d24de9dSSimon Glass prefix = series.GetPatchPrefix() 1990d24de9dSSimon Glass if prefix: 2000d24de9dSSimon Glass cmd += ['--subject-prefix=%s' % prefix] 2010d24de9dSSimon Glass cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)] 2020d24de9dSSimon Glass 2030d24de9dSSimon Glass stdout = command.RunList(cmd) 2040d24de9dSSimon Glass files = stdout.splitlines() 2050d24de9dSSimon Glass 2060d24de9dSSimon Glass # We have an extra file if there is a cover letter 2070d24de9dSSimon Glass if series.get('cover'): 2080d24de9dSSimon Glass return files[0], files[1:] 2090d24de9dSSimon Glass else: 2100d24de9dSSimon Glass return None, files 2110d24de9dSSimon Glass 2120d24de9dSSimon Glassdef ApplyPatch(verbose, fname): 2130d24de9dSSimon Glass """Apply a patch with git am to test it 2140d24de9dSSimon Glass 2150d24de9dSSimon Glass TODO: Convert these to use command, with stderr option 2160d24de9dSSimon Glass 2170d24de9dSSimon Glass Args: 2180d24de9dSSimon Glass fname: filename of patch file to apply 2190d24de9dSSimon Glass """ 220757f64a8SSimon Glass col = terminal.Color() 2210d24de9dSSimon Glass cmd = ['git', 'am', fname] 2220d24de9dSSimon Glass pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, 2230d24de9dSSimon Glass stderr=subprocess.PIPE) 2240d24de9dSSimon Glass stdout, stderr = pipe.communicate() 2250d24de9dSSimon Glass re_error = re.compile('^error: patch failed: (.+):(\d+)') 2260d24de9dSSimon Glass for line in stderr.splitlines(): 2270d24de9dSSimon Glass if verbose: 2280d24de9dSSimon Glass print line 2290d24de9dSSimon Glass match = re_error.match(line) 2300d24de9dSSimon Glass if match: 231757f64a8SSimon Glass print checkpatch.GetWarningMsg(col, 'warning', match.group(1), 232757f64a8SSimon Glass int(match.group(2)), 'Patch failed') 2330d24de9dSSimon Glass return pipe.returncode == 0, stdout 2340d24de9dSSimon Glass 2350d24de9dSSimon Glassdef ApplyPatches(verbose, args, start_point): 2360d24de9dSSimon Glass """Apply the patches with git am to make sure all is well 2370d24de9dSSimon Glass 2380d24de9dSSimon Glass Args: 2390d24de9dSSimon Glass verbose: Print out 'git am' output verbatim 2400d24de9dSSimon Glass args: List of patch files to apply 2410d24de9dSSimon Glass start_point: Number of commits back from HEAD to start applying. 2420d24de9dSSimon Glass Normally this is len(args), but it can be larger if a start 2430d24de9dSSimon Glass offset was given. 2440d24de9dSSimon Glass """ 2450d24de9dSSimon Glass error_count = 0 2460d24de9dSSimon Glass col = terminal.Color() 2470d24de9dSSimon Glass 2480d24de9dSSimon Glass # Figure out our current position 2490d24de9dSSimon Glass cmd = ['git', 'name-rev', 'HEAD', '--name-only'] 2500d24de9dSSimon Glass pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE) 2510d24de9dSSimon Glass stdout, stderr = pipe.communicate() 2520d24de9dSSimon Glass if pipe.returncode: 2530d24de9dSSimon Glass str = 'Could not find current commit name' 2540d24de9dSSimon Glass print col.Color(col.RED, str) 2550d24de9dSSimon Glass print stdout 2560d24de9dSSimon Glass return False 2570d24de9dSSimon Glass old_head = stdout.splitlines()[0] 2584251978aSSimon Glass if old_head == 'undefined': 2594251978aSSimon Glass str = "Invalid HEAD '%s'" % stdout.strip() 2604251978aSSimon Glass print col.Color(col.RED, str) 2614251978aSSimon Glass return False 2620d24de9dSSimon Glass 2630d24de9dSSimon Glass # Checkout the required start point 2640d24de9dSSimon Glass cmd = ['git', 'checkout', 'HEAD~%d' % start_point] 2650d24de9dSSimon Glass pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, 2660d24de9dSSimon Glass stderr=subprocess.PIPE) 2670d24de9dSSimon Glass stdout, stderr = pipe.communicate() 2680d24de9dSSimon Glass if pipe.returncode: 2690d24de9dSSimon Glass str = 'Could not move to commit before patch series' 2700d24de9dSSimon Glass print col.Color(col.RED, str) 2710d24de9dSSimon Glass print stdout, stderr 2720d24de9dSSimon Glass return False 2730d24de9dSSimon Glass 2740d24de9dSSimon Glass # Apply all the patches 2750d24de9dSSimon Glass for fname in args: 2760d24de9dSSimon Glass ok, stdout = ApplyPatch(verbose, fname) 2770d24de9dSSimon Glass if not ok: 2780d24de9dSSimon Glass print col.Color(col.RED, 'git am returned errors for %s: will ' 2790d24de9dSSimon Glass 'skip this patch' % fname) 2800d24de9dSSimon Glass if verbose: 2810d24de9dSSimon Glass print stdout 2820d24de9dSSimon Glass error_count += 1 2830d24de9dSSimon Glass cmd = ['git', 'am', '--skip'] 2840d24de9dSSimon Glass pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE) 2850d24de9dSSimon Glass stdout, stderr = pipe.communicate() 2860d24de9dSSimon Glass if pipe.returncode != 0: 2870d24de9dSSimon Glass print col.Color(col.RED, 'Unable to skip patch! Aborting...') 2880d24de9dSSimon Glass print stdout 2890d24de9dSSimon Glass break 2900d24de9dSSimon Glass 2910d24de9dSSimon Glass # Return to our previous position 2920d24de9dSSimon Glass cmd = ['git', 'checkout', old_head] 2930d24de9dSSimon Glass pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 2940d24de9dSSimon Glass stdout, stderr = pipe.communicate() 2950d24de9dSSimon Glass if pipe.returncode: 2960d24de9dSSimon Glass print col.Color(col.RED, 'Could not move back to head commit') 2970d24de9dSSimon Glass print stdout, stderr 2980d24de9dSSimon Glass return error_count == 0 2990d24de9dSSimon Glass 300a1318f7cSSimon Glassdef BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True): 3010d24de9dSSimon Glass """Build a list of email addresses based on an input list. 3020d24de9dSSimon Glass 3030d24de9dSSimon Glass Takes a list of email addresses and aliases, and turns this into a list 3040d24de9dSSimon Glass of only email address, by resolving any aliases that are present. 3050d24de9dSSimon Glass 3060d24de9dSSimon Glass If the tag is given, then each email address is prepended with this 3070d24de9dSSimon Glass tag and a space. If the tag starts with a minus sign (indicating a 3080d24de9dSSimon Glass command line parameter) then the email address is quoted. 3090d24de9dSSimon Glass 3100d24de9dSSimon Glass Args: 3110d24de9dSSimon Glass in_list: List of aliases/email addresses 3120d24de9dSSimon Glass tag: Text to put before each address 313a1318f7cSSimon Glass alias: Alias dictionary 314a1318f7cSSimon Glass raise_on_error: True to raise an error when an alias fails to match, 315a1318f7cSSimon Glass False to just print a message. 3160d24de9dSSimon Glass 3170d24de9dSSimon Glass Returns: 3180d24de9dSSimon Glass List of email addresses 3190d24de9dSSimon Glass 3200d24de9dSSimon Glass >>> alias = {} 3210d24de9dSSimon Glass >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 3220d24de9dSSimon Glass >>> alias['john'] = ['j.bloggs@napier.co.nz'] 3230d24de9dSSimon Glass >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>'] 3240d24de9dSSimon Glass >>> alias['boys'] = ['fred', ' john'] 3250d24de9dSSimon Glass >>> alias['all'] = ['fred ', 'john', ' mary '] 3260d24de9dSSimon Glass >>> BuildEmailList(['john', 'mary'], None, alias) 3270d24de9dSSimon Glass ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>'] 3280d24de9dSSimon Glass >>> BuildEmailList(['john', 'mary'], '--to', alias) 3290d24de9dSSimon Glass ['--to "j.bloggs@napier.co.nz"', \ 3300d24de9dSSimon Glass'--to "Mary Poppins <m.poppins@cloud.net>"'] 3310d24de9dSSimon Glass >>> BuildEmailList(['john', 'mary'], 'Cc', alias) 3320d24de9dSSimon Glass ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>'] 3330d24de9dSSimon Glass """ 3340d24de9dSSimon Glass quote = '"' if tag and tag[0] == '-' else '' 3350d24de9dSSimon Glass raw = [] 3360d24de9dSSimon Glass for item in in_list: 337a1318f7cSSimon Glass raw += LookupEmail(item, alias, raise_on_error=raise_on_error) 3380d24de9dSSimon Glass result = [] 3390d24de9dSSimon Glass for item in raw: 3400d24de9dSSimon Glass if not item in result: 3410d24de9dSSimon Glass result.append(item) 3420d24de9dSSimon Glass if tag: 3430d24de9dSSimon Glass return ['%s %s%s%s' % (tag, quote, email, quote) for email in result] 3440d24de9dSSimon Glass return result 3450d24de9dSSimon Glass 346a1318f7cSSimon Glassdef EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname, 3476d819925SDoug Anderson self_only=False, alias=None, in_reply_to=None): 3480d24de9dSSimon Glass """Email a patch series. 3490d24de9dSSimon Glass 3500d24de9dSSimon Glass Args: 3510d24de9dSSimon Glass series: Series object containing destination info 3520d24de9dSSimon Glass cover_fname: filename of cover letter 3530d24de9dSSimon Glass args: list of filenames of patch files 3540d24de9dSSimon Glass dry_run: Just return the command that would be run 355a1318f7cSSimon Glass raise_on_error: True to raise an error when an alias fails to match, 356a1318f7cSSimon Glass False to just print a message. 3570d24de9dSSimon Glass cc_fname: Filename of Cc file for per-commit Cc 3580d24de9dSSimon Glass self_only: True to just email to yourself as a test 3596d819925SDoug Anderson in_reply_to: If set we'll pass this to git as --in-reply-to. 3606d819925SDoug Anderson Should be a message ID that this is in reply to. 3610d24de9dSSimon Glass 3620d24de9dSSimon Glass Returns: 3630d24de9dSSimon Glass Git command that was/would be run 3640d24de9dSSimon Glass 365a970048eSDoug Anderson # For the duration of this doctest pretend that we ran patman with ./patman 366a970048eSDoug Anderson >>> _old_argv0 = sys.argv[0] 367a970048eSDoug Anderson >>> sys.argv[0] = './patman' 368a970048eSDoug Anderson 3690d24de9dSSimon Glass >>> alias = {} 3700d24de9dSSimon Glass >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 3710d24de9dSSimon Glass >>> alias['john'] = ['j.bloggs@napier.co.nz'] 3720d24de9dSSimon Glass >>> alias['mary'] = ['m.poppins@cloud.net'] 3730d24de9dSSimon Glass >>> alias['boys'] = ['fred', ' john'] 3740d24de9dSSimon Glass >>> alias['all'] = ['fred ', 'john', ' mary '] 3750d24de9dSSimon Glass >>> alias[os.getenv('USER')] = ['this-is-me@me.com'] 3760d24de9dSSimon Glass >>> series = series.Series() 3770d24de9dSSimon Glass >>> series.to = ['fred'] 3780d24de9dSSimon Glass >>> series.cc = ['mary'] 379a1318f7cSSimon Glass >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 380a1318f7cSSimon Glass False, alias) 3810d24de9dSSimon Glass 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 3820d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 383a1318f7cSSimon Glass >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \ 384a1318f7cSSimon Glass alias) 3850d24de9dSSimon Glass 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 3860d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1' 3870d24de9dSSimon Glass >>> series.cc = ['all'] 388a1318f7cSSimon Glass >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 389a1318f7cSSimon Glass True, alias) 3900d24de9dSSimon Glass 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \ 3910d24de9dSSimon Glass--cc-cmd cc-fname" cover p1 p2' 392a1318f7cSSimon Glass >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 393a1318f7cSSimon Glass False, alias) 3940d24de9dSSimon Glass 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 3950d24de9dSSimon Glass"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \ 3960d24de9dSSimon Glass"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 397a970048eSDoug Anderson 398a970048eSDoug Anderson # Restore argv[0] since we clobbered it. 399a970048eSDoug Anderson >>> sys.argv[0] = _old_argv0 4000d24de9dSSimon Glass """ 401a1318f7cSSimon Glass to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error) 4020d24de9dSSimon Glass if not to: 403ee860c60SMasahiro Yamada git_config_to = command.Output('git', 'config', 'sendemail.to') 404ee860c60SMasahiro Yamada if not git_config_to: 405ee860c60SMasahiro Yamada print ("No recipient.\n" 406ee860c60SMasahiro Yamada "Please add something like this to a commit\n" 407ee860c60SMasahiro Yamada "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n" 408ee860c60SMasahiro Yamada "Or do something like this\n" 409ee860c60SMasahiro Yamada "git config sendemail.to u-boot@lists.denx.de") 4100d24de9dSSimon Glass return 411a1318f7cSSimon Glass cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error) 4120d24de9dSSimon Glass if self_only: 413a1318f7cSSimon Glass to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error) 4140d24de9dSSimon Glass cc = [] 4150d24de9dSSimon Glass cmd = ['git', 'send-email', '--annotate'] 4166d819925SDoug Anderson if in_reply_to: 4176d819925SDoug Anderson cmd.append('--in-reply-to="%s"' % in_reply_to) 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 4250d24de9dSSimon Glass str = ' '.join(cmd) 4260d24de9dSSimon Glass if not dry_run: 4270d24de9dSSimon Glass os.system(str) 4280d24de9dSSimon Glass return str 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) 475a1318f7cSSimon Glass \033[1;31mAlias 'odd' not found\033[0m 476a1318f7cSSimon Glass [] 477a1318f7cSSimon Glass >>> # In this case the loop part will effectively be ignored. 478a1318f7cSSimon Glass >>> LookupEmail('loop', alias, raise_on_error=False) 479a1318f7cSSimon Glass \033[1;31mRecursive email alias at 'other'\033[0m 480a1318f7cSSimon Glass \033[1;31mRecursive email alias at 'john'\033[0m 481a1318f7cSSimon Glass \033[1;31mRecursive email alias at 'mary'\033[0m 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: 497a1318f7cSSimon Glass raise OSError, msg 498a1318f7cSSimon Glass else: 499a1318f7cSSimon Glass 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: 506a1318f7cSSimon Glass raise ValueError, msg 507a1318f7cSSimon Glass else: 508a1318f7cSSimon Glass 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 5160d24de9dSSimon Glass #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 5630d24de9dSSimon Glassdef Setup(): 5640d24de9dSSimon Glass """Set up git utils, by reading the alias files.""" 5650d24de9dSSimon Glass # Check for a git alias file also 5660d24de9dSSimon Glass alias_fname = GetAliasFile() 5670d24de9dSSimon Glass if alias_fname: 5680d24de9dSSimon Glass settings.ReadGitAliases(alias_fname) 5690d24de9dSSimon Glass 5705f6a1c42SSimon Glassdef GetHead(): 5715f6a1c42SSimon Glass """Get the hash of the current HEAD 5725f6a1c42SSimon Glass 5735f6a1c42SSimon Glass Returns: 5745f6a1c42SSimon Glass Hash of HEAD 5755f6a1c42SSimon Glass """ 5765f6a1c42SSimon Glass return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H') 5775f6a1c42SSimon Glass 5780d24de9dSSimon Glassif __name__ == "__main__": 5790d24de9dSSimon Glass import doctest 5800d24de9dSSimon Glass 5810d24de9dSSimon Glass doctest.testmod() 582