1*4882a593Smuzhiyun# Copyright (c) 2011 The Chromium OS Authors. 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0+ 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun 6*4882a593Smuzhiyunimport command 7*4882a593Smuzhiyunimport re 8*4882a593Smuzhiyunimport os 9*4882a593Smuzhiyunimport series 10*4882a593Smuzhiyunimport subprocess 11*4882a593Smuzhiyunimport sys 12*4882a593Smuzhiyunimport terminal 13*4882a593Smuzhiyun 14*4882a593Smuzhiyunimport checkpatch 15*4882a593Smuzhiyunimport settings 16*4882a593Smuzhiyun 17*4882a593Smuzhiyun# True to use --no-decorate - we check this in Setup() 18*4882a593Smuzhiyunuse_no_decorate = True 19*4882a593Smuzhiyun 20*4882a593Smuzhiyundef LogCmd(commit_range, git_dir=None, oneline=False, reverse=False, 21*4882a593Smuzhiyun count=None): 22*4882a593Smuzhiyun """Create a command to perform a 'git log' 23*4882a593Smuzhiyun 24*4882a593Smuzhiyun Args: 25*4882a593Smuzhiyun commit_range: Range expression to use for log, None for none 26*4882a593Smuzhiyun git_dir: Path to git repositiory (None to use default) 27*4882a593Smuzhiyun oneline: True to use --oneline, else False 28*4882a593Smuzhiyun reverse: True to reverse the log (--reverse) 29*4882a593Smuzhiyun count: Number of commits to list, or None for no limit 30*4882a593Smuzhiyun Return: 31*4882a593Smuzhiyun List containing command and arguments to run 32*4882a593Smuzhiyun """ 33*4882a593Smuzhiyun cmd = ['git'] 34*4882a593Smuzhiyun if git_dir: 35*4882a593Smuzhiyun cmd += ['--git-dir', git_dir] 36*4882a593Smuzhiyun cmd += ['--no-pager', 'log', '--no-color'] 37*4882a593Smuzhiyun if oneline: 38*4882a593Smuzhiyun cmd.append('--oneline') 39*4882a593Smuzhiyun if use_no_decorate: 40*4882a593Smuzhiyun cmd.append('--no-decorate') 41*4882a593Smuzhiyun if reverse: 42*4882a593Smuzhiyun cmd.append('--reverse') 43*4882a593Smuzhiyun if count is not None: 44*4882a593Smuzhiyun cmd.append('-n%d' % count) 45*4882a593Smuzhiyun if commit_range: 46*4882a593Smuzhiyun cmd.append(commit_range) 47*4882a593Smuzhiyun 48*4882a593Smuzhiyun # Add this in case we have a branch with the same name as a directory. 49*4882a593Smuzhiyun # This avoids messages like this, for example: 50*4882a593Smuzhiyun # fatal: ambiguous argument 'test': both revision and filename 51*4882a593Smuzhiyun cmd.append('--') 52*4882a593Smuzhiyun return cmd 53*4882a593Smuzhiyun 54*4882a593Smuzhiyundef CountCommitsToBranch(): 55*4882a593Smuzhiyun """Returns number of commits between HEAD and the tracking branch. 56*4882a593Smuzhiyun 57*4882a593Smuzhiyun This looks back to the tracking branch and works out the number of commits 58*4882a593Smuzhiyun since then. 59*4882a593Smuzhiyun 60*4882a593Smuzhiyun Return: 61*4882a593Smuzhiyun Number of patches that exist on top of the branch 62*4882a593Smuzhiyun """ 63*4882a593Smuzhiyun pipe = [LogCmd('@{upstream}..', oneline=True), 64*4882a593Smuzhiyun ['wc', '-l']] 65*4882a593Smuzhiyun stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout 66*4882a593Smuzhiyun patch_count = int(stdout) 67*4882a593Smuzhiyun return patch_count 68*4882a593Smuzhiyun 69*4882a593Smuzhiyundef NameRevision(commit_hash): 70*4882a593Smuzhiyun """Gets the revision name for a commit 71*4882a593Smuzhiyun 72*4882a593Smuzhiyun Args: 73*4882a593Smuzhiyun commit_hash: Commit hash to look up 74*4882a593Smuzhiyun 75*4882a593Smuzhiyun Return: 76*4882a593Smuzhiyun Name of revision, if any, else None 77*4882a593Smuzhiyun """ 78*4882a593Smuzhiyun pipe = ['git', 'name-rev', commit_hash] 79*4882a593Smuzhiyun stdout = command.RunPipe([pipe], capture=True, oneline=True).stdout 80*4882a593Smuzhiyun 81*4882a593Smuzhiyun # We expect a commit, a space, then a revision name 82*4882a593Smuzhiyun name = stdout.split(' ')[1].strip() 83*4882a593Smuzhiyun return name 84*4882a593Smuzhiyun 85*4882a593Smuzhiyundef GuessUpstream(git_dir, branch): 86*4882a593Smuzhiyun """Tries to guess the upstream for a branch 87*4882a593Smuzhiyun 88*4882a593Smuzhiyun This lists out top commits on a branch and tries to find a suitable 89*4882a593Smuzhiyun upstream. It does this by looking for the first commit where 90*4882a593Smuzhiyun 'git name-rev' returns a plain branch name, with no ! or ^ modifiers. 91*4882a593Smuzhiyun 92*4882a593Smuzhiyun Args: 93*4882a593Smuzhiyun git_dir: Git directory containing repo 94*4882a593Smuzhiyun branch: Name of branch 95*4882a593Smuzhiyun 96*4882a593Smuzhiyun Returns: 97*4882a593Smuzhiyun Tuple: 98*4882a593Smuzhiyun Name of upstream branch (e.g. 'upstream/master') or None if none 99*4882a593Smuzhiyun Warning/error message, or None if none 100*4882a593Smuzhiyun """ 101*4882a593Smuzhiyun pipe = [LogCmd(branch, git_dir=git_dir, oneline=True, count=100)] 102*4882a593Smuzhiyun result = command.RunPipe(pipe, capture=True, capture_stderr=True, 103*4882a593Smuzhiyun raise_on_error=False) 104*4882a593Smuzhiyun if result.return_code: 105*4882a593Smuzhiyun return None, "Branch '%s' not found" % branch 106*4882a593Smuzhiyun for line in result.stdout.splitlines()[1:]: 107*4882a593Smuzhiyun commit_hash = line.split(' ')[0] 108*4882a593Smuzhiyun name = NameRevision(commit_hash) 109*4882a593Smuzhiyun if '~' not in name and '^' not in name: 110*4882a593Smuzhiyun if name.startswith('remotes/'): 111*4882a593Smuzhiyun name = name[8:] 112*4882a593Smuzhiyun return name, "Guessing upstream as '%s'" % name 113*4882a593Smuzhiyun return None, "Cannot find a suitable upstream for branch '%s'" % branch 114*4882a593Smuzhiyun 115*4882a593Smuzhiyundef GetUpstream(git_dir, branch): 116*4882a593Smuzhiyun """Returns the name of the upstream for a branch 117*4882a593Smuzhiyun 118*4882a593Smuzhiyun Args: 119*4882a593Smuzhiyun git_dir: Git directory containing repo 120*4882a593Smuzhiyun branch: Name of branch 121*4882a593Smuzhiyun 122*4882a593Smuzhiyun Returns: 123*4882a593Smuzhiyun Tuple: 124*4882a593Smuzhiyun Name of upstream branch (e.g. 'upstream/master') or None if none 125*4882a593Smuzhiyun Warning/error message, or None if none 126*4882a593Smuzhiyun """ 127*4882a593Smuzhiyun try: 128*4882a593Smuzhiyun remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config', 129*4882a593Smuzhiyun 'branch.%s.remote' % branch) 130*4882a593Smuzhiyun merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config', 131*4882a593Smuzhiyun 'branch.%s.merge' % branch) 132*4882a593Smuzhiyun except: 133*4882a593Smuzhiyun upstream, msg = GuessUpstream(git_dir, branch) 134*4882a593Smuzhiyun return upstream, msg 135*4882a593Smuzhiyun 136*4882a593Smuzhiyun if remote == '.': 137*4882a593Smuzhiyun return merge, None 138*4882a593Smuzhiyun elif remote and merge: 139*4882a593Smuzhiyun leaf = merge.split('/')[-1] 140*4882a593Smuzhiyun return '%s/%s' % (remote, leaf), None 141*4882a593Smuzhiyun else: 142*4882a593Smuzhiyun raise ValueError("Cannot determine upstream branch for branch " 143*4882a593Smuzhiyun "'%s' remote='%s', merge='%s'" % (branch, remote, merge)) 144*4882a593Smuzhiyun 145*4882a593Smuzhiyun 146*4882a593Smuzhiyundef GetRangeInBranch(git_dir, branch, include_upstream=False): 147*4882a593Smuzhiyun """Returns an expression for the commits in the given branch. 148*4882a593Smuzhiyun 149*4882a593Smuzhiyun Args: 150*4882a593Smuzhiyun git_dir: Directory containing git repo 151*4882a593Smuzhiyun branch: Name of branch 152*4882a593Smuzhiyun Return: 153*4882a593Smuzhiyun Expression in the form 'upstream..branch' which can be used to 154*4882a593Smuzhiyun access the commits. If the branch does not exist, returns None. 155*4882a593Smuzhiyun """ 156*4882a593Smuzhiyun upstream, msg = GetUpstream(git_dir, branch) 157*4882a593Smuzhiyun if not upstream: 158*4882a593Smuzhiyun return None, msg 159*4882a593Smuzhiyun rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch) 160*4882a593Smuzhiyun return rstr, msg 161*4882a593Smuzhiyun 162*4882a593Smuzhiyundef CountCommitsInRange(git_dir, range_expr): 163*4882a593Smuzhiyun """Returns the number of commits in the given range. 164*4882a593Smuzhiyun 165*4882a593Smuzhiyun Args: 166*4882a593Smuzhiyun git_dir: Directory containing git repo 167*4882a593Smuzhiyun range_expr: Range to check 168*4882a593Smuzhiyun Return: 169*4882a593Smuzhiyun Number of patches that exist in the supplied rangem or None if none 170*4882a593Smuzhiyun were found 171*4882a593Smuzhiyun """ 172*4882a593Smuzhiyun pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True)] 173*4882a593Smuzhiyun result = command.RunPipe(pipe, capture=True, capture_stderr=True, 174*4882a593Smuzhiyun raise_on_error=False) 175*4882a593Smuzhiyun if result.return_code: 176*4882a593Smuzhiyun return None, "Range '%s' not found or is invalid" % range_expr 177*4882a593Smuzhiyun patch_count = len(result.stdout.splitlines()) 178*4882a593Smuzhiyun return patch_count, None 179*4882a593Smuzhiyun 180*4882a593Smuzhiyundef CountCommitsInBranch(git_dir, branch, include_upstream=False): 181*4882a593Smuzhiyun """Returns the number of commits in the given branch. 182*4882a593Smuzhiyun 183*4882a593Smuzhiyun Args: 184*4882a593Smuzhiyun git_dir: Directory containing git repo 185*4882a593Smuzhiyun branch: Name of branch 186*4882a593Smuzhiyun Return: 187*4882a593Smuzhiyun Number of patches that exist on top of the branch, or None if the 188*4882a593Smuzhiyun branch does not exist. 189*4882a593Smuzhiyun """ 190*4882a593Smuzhiyun range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream) 191*4882a593Smuzhiyun if not range_expr: 192*4882a593Smuzhiyun return None, msg 193*4882a593Smuzhiyun return CountCommitsInRange(git_dir, range_expr) 194*4882a593Smuzhiyun 195*4882a593Smuzhiyundef CountCommits(commit_range): 196*4882a593Smuzhiyun """Returns the number of commits in the given range. 197*4882a593Smuzhiyun 198*4882a593Smuzhiyun Args: 199*4882a593Smuzhiyun commit_range: Range of commits to count (e.g. 'HEAD..base') 200*4882a593Smuzhiyun Return: 201*4882a593Smuzhiyun Number of patches that exist on top of the branch 202*4882a593Smuzhiyun """ 203*4882a593Smuzhiyun pipe = [LogCmd(commit_range, oneline=True), 204*4882a593Smuzhiyun ['wc', '-l']] 205*4882a593Smuzhiyun stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout 206*4882a593Smuzhiyun patch_count = int(stdout) 207*4882a593Smuzhiyun return patch_count 208*4882a593Smuzhiyun 209*4882a593Smuzhiyundef Checkout(commit_hash, git_dir=None, work_tree=None, force=False): 210*4882a593Smuzhiyun """Checkout the selected commit for this build 211*4882a593Smuzhiyun 212*4882a593Smuzhiyun Args: 213*4882a593Smuzhiyun commit_hash: Commit hash to check out 214*4882a593Smuzhiyun """ 215*4882a593Smuzhiyun pipe = ['git'] 216*4882a593Smuzhiyun if git_dir: 217*4882a593Smuzhiyun pipe.extend(['--git-dir', git_dir]) 218*4882a593Smuzhiyun if work_tree: 219*4882a593Smuzhiyun pipe.extend(['--work-tree', work_tree]) 220*4882a593Smuzhiyun pipe.append('checkout') 221*4882a593Smuzhiyun if force: 222*4882a593Smuzhiyun pipe.append('-f') 223*4882a593Smuzhiyun pipe.append(commit_hash) 224*4882a593Smuzhiyun result = command.RunPipe([pipe], capture=True, raise_on_error=False, 225*4882a593Smuzhiyun capture_stderr=True) 226*4882a593Smuzhiyun if result.return_code != 0: 227*4882a593Smuzhiyun raise OSError('git checkout (%s): %s' % (pipe, result.stderr)) 228*4882a593Smuzhiyun 229*4882a593Smuzhiyundef Clone(git_dir, output_dir): 230*4882a593Smuzhiyun """Checkout the selected commit for this build 231*4882a593Smuzhiyun 232*4882a593Smuzhiyun Args: 233*4882a593Smuzhiyun commit_hash: Commit hash to check out 234*4882a593Smuzhiyun """ 235*4882a593Smuzhiyun pipe = ['git', 'clone', git_dir, '.'] 236*4882a593Smuzhiyun result = command.RunPipe([pipe], capture=True, cwd=output_dir, 237*4882a593Smuzhiyun capture_stderr=True) 238*4882a593Smuzhiyun if result.return_code != 0: 239*4882a593Smuzhiyun raise OSError('git clone: %s' % result.stderr) 240*4882a593Smuzhiyun 241*4882a593Smuzhiyundef Fetch(git_dir=None, work_tree=None): 242*4882a593Smuzhiyun """Fetch from the origin repo 243*4882a593Smuzhiyun 244*4882a593Smuzhiyun Args: 245*4882a593Smuzhiyun commit_hash: Commit hash to check out 246*4882a593Smuzhiyun """ 247*4882a593Smuzhiyun pipe = ['git'] 248*4882a593Smuzhiyun if git_dir: 249*4882a593Smuzhiyun pipe.extend(['--git-dir', git_dir]) 250*4882a593Smuzhiyun if work_tree: 251*4882a593Smuzhiyun pipe.extend(['--work-tree', work_tree]) 252*4882a593Smuzhiyun pipe.append('fetch') 253*4882a593Smuzhiyun result = command.RunPipe([pipe], capture=True, capture_stderr=True) 254*4882a593Smuzhiyun if result.return_code != 0: 255*4882a593Smuzhiyun raise OSError('git fetch: %s' % result.stderr) 256*4882a593Smuzhiyun 257*4882a593Smuzhiyundef CreatePatches(start, count, series): 258*4882a593Smuzhiyun """Create a series of patches from the top of the current branch. 259*4882a593Smuzhiyun 260*4882a593Smuzhiyun The patch files are written to the current directory using 261*4882a593Smuzhiyun git format-patch. 262*4882a593Smuzhiyun 263*4882a593Smuzhiyun Args: 264*4882a593Smuzhiyun start: Commit to start from: 0=HEAD, 1=next one, etc. 265*4882a593Smuzhiyun count: number of commits to include 266*4882a593Smuzhiyun Return: 267*4882a593Smuzhiyun Filename of cover letter 268*4882a593Smuzhiyun List of filenames of patch files 269*4882a593Smuzhiyun """ 270*4882a593Smuzhiyun if series.get('version'): 271*4882a593Smuzhiyun version = '%s ' % series['version'] 272*4882a593Smuzhiyun cmd = ['git', 'format-patch', '-M', '--signoff'] 273*4882a593Smuzhiyun if series.get('cover'): 274*4882a593Smuzhiyun cmd.append('--cover-letter') 275*4882a593Smuzhiyun prefix = series.GetPatchPrefix() 276*4882a593Smuzhiyun if prefix: 277*4882a593Smuzhiyun cmd += ['--subject-prefix=%s' % prefix] 278*4882a593Smuzhiyun cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)] 279*4882a593Smuzhiyun 280*4882a593Smuzhiyun stdout = command.RunList(cmd) 281*4882a593Smuzhiyun files = stdout.splitlines() 282*4882a593Smuzhiyun 283*4882a593Smuzhiyun # We have an extra file if there is a cover letter 284*4882a593Smuzhiyun if series.get('cover'): 285*4882a593Smuzhiyun return files[0], files[1:] 286*4882a593Smuzhiyun else: 287*4882a593Smuzhiyun return None, files 288*4882a593Smuzhiyun 289*4882a593Smuzhiyundef BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True): 290*4882a593Smuzhiyun """Build a list of email addresses based on an input list. 291*4882a593Smuzhiyun 292*4882a593Smuzhiyun Takes a list of email addresses and aliases, and turns this into a list 293*4882a593Smuzhiyun of only email address, by resolving any aliases that are present. 294*4882a593Smuzhiyun 295*4882a593Smuzhiyun If the tag is given, then each email address is prepended with this 296*4882a593Smuzhiyun tag and a space. If the tag starts with a minus sign (indicating a 297*4882a593Smuzhiyun command line parameter) then the email address is quoted. 298*4882a593Smuzhiyun 299*4882a593Smuzhiyun Args: 300*4882a593Smuzhiyun in_list: List of aliases/email addresses 301*4882a593Smuzhiyun tag: Text to put before each address 302*4882a593Smuzhiyun alias: Alias dictionary 303*4882a593Smuzhiyun raise_on_error: True to raise an error when an alias fails to match, 304*4882a593Smuzhiyun False to just print a message. 305*4882a593Smuzhiyun 306*4882a593Smuzhiyun Returns: 307*4882a593Smuzhiyun List of email addresses 308*4882a593Smuzhiyun 309*4882a593Smuzhiyun >>> alias = {} 310*4882a593Smuzhiyun >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 311*4882a593Smuzhiyun >>> alias['john'] = ['j.bloggs@napier.co.nz'] 312*4882a593Smuzhiyun >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>'] 313*4882a593Smuzhiyun >>> alias['boys'] = ['fred', ' john'] 314*4882a593Smuzhiyun >>> alias['all'] = ['fred ', 'john', ' mary '] 315*4882a593Smuzhiyun >>> BuildEmailList(['john', 'mary'], None, alias) 316*4882a593Smuzhiyun ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>'] 317*4882a593Smuzhiyun >>> BuildEmailList(['john', 'mary'], '--to', alias) 318*4882a593Smuzhiyun ['--to "j.bloggs@napier.co.nz"', \ 319*4882a593Smuzhiyun'--to "Mary Poppins <m.poppins@cloud.net>"'] 320*4882a593Smuzhiyun >>> BuildEmailList(['john', 'mary'], 'Cc', alias) 321*4882a593Smuzhiyun ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>'] 322*4882a593Smuzhiyun """ 323*4882a593Smuzhiyun quote = '"' if tag and tag[0] == '-' else '' 324*4882a593Smuzhiyun raw = [] 325*4882a593Smuzhiyun for item in in_list: 326*4882a593Smuzhiyun raw += LookupEmail(item, alias, raise_on_error=raise_on_error) 327*4882a593Smuzhiyun result = [] 328*4882a593Smuzhiyun for item in raw: 329*4882a593Smuzhiyun if not item in result: 330*4882a593Smuzhiyun result.append(item) 331*4882a593Smuzhiyun if tag: 332*4882a593Smuzhiyun return ['%s %s%s%s' % (tag, quote, email, quote) for email in result] 333*4882a593Smuzhiyun return result 334*4882a593Smuzhiyun 335*4882a593Smuzhiyundef EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname, 336*4882a593Smuzhiyun self_only=False, alias=None, in_reply_to=None, thread=False): 337*4882a593Smuzhiyun """Email a patch series. 338*4882a593Smuzhiyun 339*4882a593Smuzhiyun Args: 340*4882a593Smuzhiyun series: Series object containing destination info 341*4882a593Smuzhiyun cover_fname: filename of cover letter 342*4882a593Smuzhiyun args: list of filenames of patch files 343*4882a593Smuzhiyun dry_run: Just return the command that would be run 344*4882a593Smuzhiyun raise_on_error: True to raise an error when an alias fails to match, 345*4882a593Smuzhiyun False to just print a message. 346*4882a593Smuzhiyun cc_fname: Filename of Cc file for per-commit Cc 347*4882a593Smuzhiyun self_only: True to just email to yourself as a test 348*4882a593Smuzhiyun in_reply_to: If set we'll pass this to git as --in-reply-to. 349*4882a593Smuzhiyun Should be a message ID that this is in reply to. 350*4882a593Smuzhiyun thread: True to add --thread to git send-email (make 351*4882a593Smuzhiyun all patches reply to cover-letter or first patch in series) 352*4882a593Smuzhiyun 353*4882a593Smuzhiyun Returns: 354*4882a593Smuzhiyun Git command that was/would be run 355*4882a593Smuzhiyun 356*4882a593Smuzhiyun # For the duration of this doctest pretend that we ran patman with ./patman 357*4882a593Smuzhiyun >>> _old_argv0 = sys.argv[0] 358*4882a593Smuzhiyun >>> sys.argv[0] = './patman' 359*4882a593Smuzhiyun 360*4882a593Smuzhiyun >>> alias = {} 361*4882a593Smuzhiyun >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 362*4882a593Smuzhiyun >>> alias['john'] = ['j.bloggs@napier.co.nz'] 363*4882a593Smuzhiyun >>> alias['mary'] = ['m.poppins@cloud.net'] 364*4882a593Smuzhiyun >>> alias['boys'] = ['fred', ' john'] 365*4882a593Smuzhiyun >>> alias['all'] = ['fred ', 'john', ' mary '] 366*4882a593Smuzhiyun >>> alias[os.getenv('USER')] = ['this-is-me@me.com'] 367*4882a593Smuzhiyun >>> series = series.Series() 368*4882a593Smuzhiyun >>> series.to = ['fred'] 369*4882a593Smuzhiyun >>> series.cc = ['mary'] 370*4882a593Smuzhiyun >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 371*4882a593Smuzhiyun False, alias) 372*4882a593Smuzhiyun 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 373*4882a593Smuzhiyun"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 374*4882a593Smuzhiyun >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \ 375*4882a593Smuzhiyun alias) 376*4882a593Smuzhiyun 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 377*4882a593Smuzhiyun"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1' 378*4882a593Smuzhiyun >>> series.cc = ['all'] 379*4882a593Smuzhiyun >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 380*4882a593Smuzhiyun True, alias) 381*4882a593Smuzhiyun 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \ 382*4882a593Smuzhiyun--cc-cmd cc-fname" cover p1 p2' 383*4882a593Smuzhiyun >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 384*4882a593Smuzhiyun False, alias) 385*4882a593Smuzhiyun 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 386*4882a593Smuzhiyun"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \ 387*4882a593Smuzhiyun"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 388*4882a593Smuzhiyun 389*4882a593Smuzhiyun # Restore argv[0] since we clobbered it. 390*4882a593Smuzhiyun >>> sys.argv[0] = _old_argv0 391*4882a593Smuzhiyun """ 392*4882a593Smuzhiyun to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error) 393*4882a593Smuzhiyun if not to: 394*4882a593Smuzhiyun git_config_to = command.Output('git', 'config', 'sendemail.to', 395*4882a593Smuzhiyun raise_on_error=False) 396*4882a593Smuzhiyun if not git_config_to: 397*4882a593Smuzhiyun print ("No recipient.\n" 398*4882a593Smuzhiyun "Please add something like this to a commit\n" 399*4882a593Smuzhiyun "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n" 400*4882a593Smuzhiyun "Or do something like this\n" 401*4882a593Smuzhiyun "git config sendemail.to u-boot@lists.denx.de") 402*4882a593Smuzhiyun return 403*4882a593Smuzhiyun cc = BuildEmailList(list(set(series.get('cc')) - set(series.get('to'))), 404*4882a593Smuzhiyun '--cc', alias, raise_on_error) 405*4882a593Smuzhiyun if self_only: 406*4882a593Smuzhiyun to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error) 407*4882a593Smuzhiyun cc = [] 408*4882a593Smuzhiyun cmd = ['git', 'send-email', '--annotate'] 409*4882a593Smuzhiyun if in_reply_to: 410*4882a593Smuzhiyun if type(in_reply_to) != str: 411*4882a593Smuzhiyun in_reply_to = in_reply_to.encode('utf-8') 412*4882a593Smuzhiyun cmd.append('--in-reply-to="%s"' % in_reply_to) 413*4882a593Smuzhiyun if thread: 414*4882a593Smuzhiyun cmd.append('--thread') 415*4882a593Smuzhiyun 416*4882a593Smuzhiyun cmd += to 417*4882a593Smuzhiyun cmd += cc 418*4882a593Smuzhiyun cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)] 419*4882a593Smuzhiyun if cover_fname: 420*4882a593Smuzhiyun cmd.append(cover_fname) 421*4882a593Smuzhiyun cmd += args 422*4882a593Smuzhiyun cmdstr = ' '.join(cmd) 423*4882a593Smuzhiyun if not dry_run: 424*4882a593Smuzhiyun os.system(cmdstr) 425*4882a593Smuzhiyun return cmdstr 426*4882a593Smuzhiyun 427*4882a593Smuzhiyun 428*4882a593Smuzhiyundef LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0): 429*4882a593Smuzhiyun """If an email address is an alias, look it up and return the full name 430*4882a593Smuzhiyun 431*4882a593Smuzhiyun TODO: Why not just use git's own alias feature? 432*4882a593Smuzhiyun 433*4882a593Smuzhiyun Args: 434*4882a593Smuzhiyun lookup_name: Alias or email address to look up 435*4882a593Smuzhiyun alias: Dictionary containing aliases (None to use settings default) 436*4882a593Smuzhiyun raise_on_error: True to raise an error when an alias fails to match, 437*4882a593Smuzhiyun False to just print a message. 438*4882a593Smuzhiyun 439*4882a593Smuzhiyun Returns: 440*4882a593Smuzhiyun tuple: 441*4882a593Smuzhiyun list containing a list of email addresses 442*4882a593Smuzhiyun 443*4882a593Smuzhiyun Raises: 444*4882a593Smuzhiyun OSError if a recursive alias reference was found 445*4882a593Smuzhiyun ValueError if an alias was not found 446*4882a593Smuzhiyun 447*4882a593Smuzhiyun >>> alias = {} 448*4882a593Smuzhiyun >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 449*4882a593Smuzhiyun >>> alias['john'] = ['j.bloggs@napier.co.nz'] 450*4882a593Smuzhiyun >>> alias['mary'] = ['m.poppins@cloud.net'] 451*4882a593Smuzhiyun >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz'] 452*4882a593Smuzhiyun >>> alias['all'] = ['fred ', 'john', ' mary '] 453*4882a593Smuzhiyun >>> alias['loop'] = ['other', 'john', ' mary '] 454*4882a593Smuzhiyun >>> alias['other'] = ['loop', 'john', ' mary '] 455*4882a593Smuzhiyun >>> LookupEmail('mary', alias) 456*4882a593Smuzhiyun ['m.poppins@cloud.net'] 457*4882a593Smuzhiyun >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias) 458*4882a593Smuzhiyun ['arthur.wellesley@howe.ro.uk'] 459*4882a593Smuzhiyun >>> LookupEmail('boys', alias) 460*4882a593Smuzhiyun ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz'] 461*4882a593Smuzhiyun >>> LookupEmail('all', alias) 462*4882a593Smuzhiyun ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net'] 463*4882a593Smuzhiyun >>> LookupEmail('odd', alias) 464*4882a593Smuzhiyun Traceback (most recent call last): 465*4882a593Smuzhiyun ... 466*4882a593Smuzhiyun ValueError: Alias 'odd' not found 467*4882a593Smuzhiyun >>> LookupEmail('loop', alias) 468*4882a593Smuzhiyun Traceback (most recent call last): 469*4882a593Smuzhiyun ... 470*4882a593Smuzhiyun OSError: Recursive email alias at 'other' 471*4882a593Smuzhiyun >>> LookupEmail('odd', alias, raise_on_error=False) 472*4882a593Smuzhiyun Alias 'odd' not found 473*4882a593Smuzhiyun [] 474*4882a593Smuzhiyun >>> # In this case the loop part will effectively be ignored. 475*4882a593Smuzhiyun >>> LookupEmail('loop', alias, raise_on_error=False) 476*4882a593Smuzhiyun Recursive email alias at 'other' 477*4882a593Smuzhiyun Recursive email alias at 'john' 478*4882a593Smuzhiyun Recursive email alias at 'mary' 479*4882a593Smuzhiyun ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net'] 480*4882a593Smuzhiyun """ 481*4882a593Smuzhiyun if not alias: 482*4882a593Smuzhiyun alias = settings.alias 483*4882a593Smuzhiyun lookup_name = lookup_name.strip() 484*4882a593Smuzhiyun if '@' in lookup_name: # Perhaps a real email address 485*4882a593Smuzhiyun return [lookup_name] 486*4882a593Smuzhiyun 487*4882a593Smuzhiyun lookup_name = lookup_name.lower() 488*4882a593Smuzhiyun col = terminal.Color() 489*4882a593Smuzhiyun 490*4882a593Smuzhiyun out_list = [] 491*4882a593Smuzhiyun if level > 10: 492*4882a593Smuzhiyun msg = "Recursive email alias at '%s'" % lookup_name 493*4882a593Smuzhiyun if raise_on_error: 494*4882a593Smuzhiyun raise OSError(msg) 495*4882a593Smuzhiyun else: 496*4882a593Smuzhiyun print(col.Color(col.RED, msg)) 497*4882a593Smuzhiyun return out_list 498*4882a593Smuzhiyun 499*4882a593Smuzhiyun if lookup_name: 500*4882a593Smuzhiyun if not lookup_name in alias: 501*4882a593Smuzhiyun msg = "Alias '%s' not found" % lookup_name 502*4882a593Smuzhiyun if raise_on_error: 503*4882a593Smuzhiyun raise ValueError(msg) 504*4882a593Smuzhiyun else: 505*4882a593Smuzhiyun print(col.Color(col.RED, msg)) 506*4882a593Smuzhiyun return out_list 507*4882a593Smuzhiyun for item in alias[lookup_name]: 508*4882a593Smuzhiyun todo = LookupEmail(item, alias, raise_on_error, level + 1) 509*4882a593Smuzhiyun for new_item in todo: 510*4882a593Smuzhiyun if not new_item in out_list: 511*4882a593Smuzhiyun out_list.append(new_item) 512*4882a593Smuzhiyun 513*4882a593Smuzhiyun #print("No match for alias '%s'" % lookup_name) 514*4882a593Smuzhiyun return out_list 515*4882a593Smuzhiyun 516*4882a593Smuzhiyundef GetTopLevel(): 517*4882a593Smuzhiyun """Return name of top-level directory for this git repo. 518*4882a593Smuzhiyun 519*4882a593Smuzhiyun Returns: 520*4882a593Smuzhiyun Full path to git top-level directory 521*4882a593Smuzhiyun 522*4882a593Smuzhiyun This test makes sure that we are running tests in the right subdir 523*4882a593Smuzhiyun 524*4882a593Smuzhiyun >>> os.path.realpath(os.path.dirname(__file__)) == \ 525*4882a593Smuzhiyun os.path.join(GetTopLevel(), 'tools', 'patman') 526*4882a593Smuzhiyun True 527*4882a593Smuzhiyun """ 528*4882a593Smuzhiyun return command.OutputOneLine('git', 'rev-parse', '--show-toplevel') 529*4882a593Smuzhiyun 530*4882a593Smuzhiyundef GetAliasFile(): 531*4882a593Smuzhiyun """Gets the name of the git alias file. 532*4882a593Smuzhiyun 533*4882a593Smuzhiyun Returns: 534*4882a593Smuzhiyun Filename of git alias file, or None if none 535*4882a593Smuzhiyun """ 536*4882a593Smuzhiyun fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile', 537*4882a593Smuzhiyun raise_on_error=False) 538*4882a593Smuzhiyun if fname: 539*4882a593Smuzhiyun fname = os.path.join(GetTopLevel(), fname.strip()) 540*4882a593Smuzhiyun return fname 541*4882a593Smuzhiyun 542*4882a593Smuzhiyundef GetDefaultUserName(): 543*4882a593Smuzhiyun """Gets the user.name from .gitconfig file. 544*4882a593Smuzhiyun 545*4882a593Smuzhiyun Returns: 546*4882a593Smuzhiyun User name found in .gitconfig file, or None if none 547*4882a593Smuzhiyun """ 548*4882a593Smuzhiyun uname = command.OutputOneLine('git', 'config', '--global', 'user.name') 549*4882a593Smuzhiyun return uname 550*4882a593Smuzhiyun 551*4882a593Smuzhiyundef GetDefaultUserEmail(): 552*4882a593Smuzhiyun """Gets the user.email from the global .gitconfig file. 553*4882a593Smuzhiyun 554*4882a593Smuzhiyun Returns: 555*4882a593Smuzhiyun User's email found in .gitconfig file, or None if none 556*4882a593Smuzhiyun """ 557*4882a593Smuzhiyun uemail = command.OutputOneLine('git', 'config', '--global', 'user.email') 558*4882a593Smuzhiyun return uemail 559*4882a593Smuzhiyun 560*4882a593Smuzhiyundef GetDefaultSubjectPrefix(): 561*4882a593Smuzhiyun """Gets the format.subjectprefix from local .git/config file. 562*4882a593Smuzhiyun 563*4882a593Smuzhiyun Returns: 564*4882a593Smuzhiyun Subject prefix found in local .git/config file, or None if none 565*4882a593Smuzhiyun """ 566*4882a593Smuzhiyun sub_prefix = command.OutputOneLine('git', 'config', 'format.subjectprefix', 567*4882a593Smuzhiyun raise_on_error=False) 568*4882a593Smuzhiyun 569*4882a593Smuzhiyun return sub_prefix 570*4882a593Smuzhiyun 571*4882a593Smuzhiyundef Setup(): 572*4882a593Smuzhiyun """Set up git utils, by reading the alias files.""" 573*4882a593Smuzhiyun # Check for a git alias file also 574*4882a593Smuzhiyun global use_no_decorate 575*4882a593Smuzhiyun 576*4882a593Smuzhiyun alias_fname = GetAliasFile() 577*4882a593Smuzhiyun if alias_fname: 578*4882a593Smuzhiyun settings.ReadGitAliases(alias_fname) 579*4882a593Smuzhiyun cmd = LogCmd(None, count=0) 580*4882a593Smuzhiyun use_no_decorate = (command.RunPipe([cmd], raise_on_error=False) 581*4882a593Smuzhiyun .return_code == 0) 582*4882a593Smuzhiyun 583*4882a593Smuzhiyundef GetHead(): 584*4882a593Smuzhiyun """Get the hash of the current HEAD 585*4882a593Smuzhiyun 586*4882a593Smuzhiyun Returns: 587*4882a593Smuzhiyun Hash of HEAD 588*4882a593Smuzhiyun """ 589*4882a593Smuzhiyun return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H') 590*4882a593Smuzhiyun 591*4882a593Smuzhiyunif __name__ == "__main__": 592*4882a593Smuzhiyun import doctest 593*4882a593Smuzhiyun 594*4882a593Smuzhiyun doctest.testmod() 595