blob: 27a0a9fbc1f94611c6a5c0b464fff4b99e8ec112 [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass0d24de92012-01-14 15:12:45 +00002# Copyright (c) 2011 The Chromium OS Authors.
3#
Simon Glass0d24de92012-01-14 15:12:45 +00004
Simon Glass0d24de92012-01-14 15:12:45 +00005import re
6import os
Simon Glass0d24de92012-01-14 15:12:45 +00007import subprocess
8import sys
Simon Glass0d24de92012-01-14 15:12:45 +00009
Simon Glassbf776672020-04-17 18:09:04 -060010from patman import command
Simon Glassbf776672020-04-17 18:09:04 -060011from patman import settings
12from patman import terminal
13from patman import tools
Simon Glass5f6a1c42012-12-15 10:42:07 +000014
Simon Glasse49f14a2014-08-09 15:33:11 -060015# True to use --no-decorate - we check this in Setup()
16use_no_decorate = True
17
Simon Glasscda2a612014-08-09 15:33:10 -060018def LogCmd(commit_range, git_dir=None, oneline=False, reverse=False,
19 count=None):
20 """Create a command to perform a 'git log'
21
22 Args:
23 commit_range: Range expression to use for log, None for none
Anatolij Gustschinab4a6ab2019-10-27 17:55:04 +010024 git_dir: Path to git repository (None to use default)
Simon Glasscda2a612014-08-09 15:33:10 -060025 oneline: True to use --oneline, else False
26 reverse: True to reverse the log (--reverse)
27 count: Number of commits to list, or None for no limit
28 Return:
29 List containing command and arguments to run
30 """
31 cmd = ['git']
32 if git_dir:
33 cmd += ['--git-dir', git_dir]
Simon Glass9447a6b2014-08-28 09:43:37 -060034 cmd += ['--no-pager', 'log', '--no-color']
Simon Glasscda2a612014-08-09 15:33:10 -060035 if oneline:
36 cmd.append('--oneline')
Simon Glasse49f14a2014-08-09 15:33:11 -060037 if use_no_decorate:
38 cmd.append('--no-decorate')
Simon Glass042a7322014-08-14 21:59:11 -060039 if reverse:
40 cmd.append('--reverse')
Simon Glasscda2a612014-08-09 15:33:10 -060041 if count is not None:
42 cmd.append('-n%d' % count)
43 if commit_range:
44 cmd.append(commit_range)
Simon Glassd4c85722016-03-12 18:50:31 -070045
46 # Add this in case we have a branch with the same name as a directory.
47 # This avoids messages like this, for example:
48 # fatal: ambiguous argument 'test': both revision and filename
49 cmd.append('--')
Simon Glasscda2a612014-08-09 15:33:10 -060050 return cmd
Simon Glass0d24de92012-01-14 15:12:45 +000051
Simon Glass262130f2020-07-05 21:41:51 -060052def CountCommitsToBranch(branch):
Simon Glass0d24de92012-01-14 15:12:45 +000053 """Returns number of commits between HEAD and the tracking branch.
54
55 This looks back to the tracking branch and works out the number of commits
56 since then.
57
Simon Glass262130f2020-07-05 21:41:51 -060058 Args:
59 branch: Branch to count from (None for current branch)
60
Simon Glass0d24de92012-01-14 15:12:45 +000061 Return:
62 Number of patches that exist on top of the branch
63 """
Simon Glass262130f2020-07-05 21:41:51 -060064 if branch:
65 us, msg = GetUpstream('.git', branch)
66 rev_range = '%s..%s' % (us, branch)
67 else:
68 rev_range = '@{upstream}..'
69 pipe = [LogCmd(rev_range, oneline=True), ['wc', '-l']]
Simon Glassa10fd932012-12-15 10:42:04 +000070 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
Simon Glass0d24de92012-01-14 15:12:45 +000071 patch_count = int(stdout)
72 return patch_count
73
Simon Glass2a9e2c62014-12-01 17:33:54 -070074def NameRevision(commit_hash):
75 """Gets the revision name for a commit
76
77 Args:
78 commit_hash: Commit hash to look up
79
80 Return:
81 Name of revision, if any, else None
82 """
83 pipe = ['git', 'name-rev', commit_hash]
84 stdout = command.RunPipe([pipe], capture=True, oneline=True).stdout
85
86 # We expect a commit, a space, then a revision name
87 name = stdout.split(' ')[1].strip()
88 return name
89
90def GuessUpstream(git_dir, branch):
91 """Tries to guess the upstream for a branch
92
93 This lists out top commits on a branch and tries to find a suitable
94 upstream. It does this by looking for the first commit where
95 'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
96
97 Args:
98 git_dir: Git directory containing repo
99 branch: Name of branch
100
101 Returns:
102 Tuple:
103 Name of upstream branch (e.g. 'upstream/master') or None if none
104 Warning/error message, or None if none
105 """
106 pipe = [LogCmd(branch, git_dir=git_dir, oneline=True, count=100)]
107 result = command.RunPipe(pipe, capture=True, capture_stderr=True,
108 raise_on_error=False)
109 if result.return_code:
110 return None, "Branch '%s' not found" % branch
111 for line in result.stdout.splitlines()[1:]:
112 commit_hash = line.split(' ')[0]
113 name = NameRevision(commit_hash)
114 if '~' not in name and '^' not in name:
115 if name.startswith('remotes/'):
116 name = name[8:]
117 return name, "Guessing upstream as '%s'" % name
118 return None, "Cannot find a suitable upstream for branch '%s'" % branch
119
Simon Glass5f6a1c42012-12-15 10:42:07 +0000120def GetUpstream(git_dir, branch):
121 """Returns the name of the upstream for a branch
122
123 Args:
124 git_dir: Git directory containing repo
125 branch: Name of branch
126
127 Returns:
Simon Glass2a9e2c62014-12-01 17:33:54 -0700128 Tuple:
129 Name of upstream branch (e.g. 'upstream/master') or None if none
130 Warning/error message, or None if none
Simon Glass5f6a1c42012-12-15 10:42:07 +0000131 """
Simon Glasscce717a2013-05-08 08:06:08 +0000132 try:
133 remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
134 'branch.%s.remote' % branch)
135 merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
136 'branch.%s.merge' % branch)
137 except:
Simon Glass2a9e2c62014-12-01 17:33:54 -0700138 upstream, msg = GuessUpstream(git_dir, branch)
139 return upstream, msg
Simon Glasscce717a2013-05-08 08:06:08 +0000140
Simon Glass5f6a1c42012-12-15 10:42:07 +0000141 if remote == '.':
Simon Glass71edbe52015-01-29 11:35:16 -0700142 return merge, None
Simon Glass5f6a1c42012-12-15 10:42:07 +0000143 elif remote and merge:
144 leaf = merge.split('/')[-1]
Simon Glass2a9e2c62014-12-01 17:33:54 -0700145 return '%s/%s' % (remote, leaf), None
Simon Glass5f6a1c42012-12-15 10:42:07 +0000146 else:
Paul Burtonac3fde92016-09-27 16:03:51 +0100147 raise ValueError("Cannot determine upstream branch for branch "
Simon Glass5f6a1c42012-12-15 10:42:07 +0000148 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
149
150
151def GetRangeInBranch(git_dir, branch, include_upstream=False):
152 """Returns an expression for the commits in the given branch.
153
154 Args:
155 git_dir: Directory containing git repo
156 branch: Name of branch
157 Return:
158 Expression in the form 'upstream..branch' which can be used to
Simon Glasscce717a2013-05-08 08:06:08 +0000159 access the commits. If the branch does not exist, returns None.
Simon Glass5f6a1c42012-12-15 10:42:07 +0000160 """
Simon Glass2a9e2c62014-12-01 17:33:54 -0700161 upstream, msg = GetUpstream(git_dir, branch)
Simon Glasscce717a2013-05-08 08:06:08 +0000162 if not upstream:
Simon Glass2a9e2c62014-12-01 17:33:54 -0700163 return None, msg
164 rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
165 return rstr, msg
Simon Glass5f6a1c42012-12-15 10:42:07 +0000166
Simon Glass5abab202014-12-01 17:33:57 -0700167def CountCommitsInRange(git_dir, range_expr):
168 """Returns the number of commits in the given range.
169
170 Args:
171 git_dir: Directory containing git repo
172 range_expr: Range to check
173 Return:
Anatolij Gustschinab4a6ab2019-10-27 17:55:04 +0100174 Number of patches that exist in the supplied range or None if none
Simon Glass5abab202014-12-01 17:33:57 -0700175 were found
176 """
177 pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True)]
178 result = command.RunPipe(pipe, capture=True, capture_stderr=True,
179 raise_on_error=False)
180 if result.return_code:
181 return None, "Range '%s' not found or is invalid" % range_expr
182 patch_count = len(result.stdout.splitlines())
183 return patch_count, None
184
Simon Glass5f6a1c42012-12-15 10:42:07 +0000185def CountCommitsInBranch(git_dir, branch, include_upstream=False):
186 """Returns the number of commits in the given branch.
187
188 Args:
189 git_dir: Directory containing git repo
190 branch: Name of branch
191 Return:
Simon Glasscce717a2013-05-08 08:06:08 +0000192 Number of patches that exist on top of the branch, or None if the
193 branch does not exist.
Simon Glass5f6a1c42012-12-15 10:42:07 +0000194 """
Simon Glass2a9e2c62014-12-01 17:33:54 -0700195 range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream)
Simon Glasscce717a2013-05-08 08:06:08 +0000196 if not range_expr:
Simon Glass2a9e2c62014-12-01 17:33:54 -0700197 return None, msg
Simon Glass5abab202014-12-01 17:33:57 -0700198 return CountCommitsInRange(git_dir, range_expr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000199
200def CountCommits(commit_range):
201 """Returns the number of commits in the given range.
202
203 Args:
204 commit_range: Range of commits to count (e.g. 'HEAD..base')
205 Return:
206 Number of patches that exist on top of the branch
207 """
Simon Glasscda2a612014-08-09 15:33:10 -0600208 pipe = [LogCmd(commit_range, oneline=True),
Simon Glass5f6a1c42012-12-15 10:42:07 +0000209 ['wc', '-l']]
210 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
211 patch_count = int(stdout)
212 return patch_count
213
214def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
215 """Checkout the selected commit for this build
216
217 Args:
218 commit_hash: Commit hash to check out
219 """
220 pipe = ['git']
221 if git_dir:
222 pipe.extend(['--git-dir', git_dir])
223 if work_tree:
224 pipe.extend(['--work-tree', work_tree])
225 pipe.append('checkout')
226 if force:
227 pipe.append('-f')
228 pipe.append(commit_hash)
Simon Glassddaf5c82014-09-05 19:00:09 -0600229 result = command.RunPipe([pipe], capture=True, raise_on_error=False,
230 capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000231 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100232 raise OSError('git checkout (%s): %s' % (pipe, result.stderr))
Simon Glass5f6a1c42012-12-15 10:42:07 +0000233
234def Clone(git_dir, output_dir):
235 """Checkout the selected commit for this build
236
237 Args:
238 commit_hash: Commit hash to check out
239 """
240 pipe = ['git', 'clone', git_dir, '.']
Simon Glassddaf5c82014-09-05 19:00:09 -0600241 result = command.RunPipe([pipe], capture=True, cwd=output_dir,
242 capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000243 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100244 raise OSError('git clone: %s' % result.stderr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000245
246def Fetch(git_dir=None, work_tree=None):
247 """Fetch from the origin repo
248
249 Args:
250 commit_hash: Commit hash to check out
251 """
252 pipe = ['git']
253 if git_dir:
254 pipe.extend(['--git-dir', git_dir])
255 if work_tree:
256 pipe.extend(['--work-tree', work_tree])
257 pipe.append('fetch')
Simon Glassddaf5c82014-09-05 19:00:09 -0600258 result = command.RunPipe([pipe], capture=True, capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000259 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100260 raise OSError('git fetch: %s' % result.stderr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000261
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300262def CheckWorktreeIsAvailable(git_dir):
263 """Check if git-worktree functionality is available
264
265 Args:
266 git_dir: The repository to test in
267
268 Returns:
269 True if git-worktree commands will work, False otherwise.
270 """
271 pipe = ['git', '--git-dir', git_dir, 'worktree', 'list']
272 result = command.RunPipe([pipe], capture=True, capture_stderr=True,
273 raise_on_error=False)
274 return result.return_code == 0
275
276def AddWorktree(git_dir, output_dir, commit_hash=None):
277 """Create and checkout a new git worktree for this build
278
279 Args:
280 git_dir: The repository to checkout the worktree from
281 output_dir: Path for the new worktree
282 commit_hash: Commit hash to checkout
283 """
284 # We need to pass --detach to avoid creating a new branch
285 pipe = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach']
286 if commit_hash:
287 pipe.append(commit_hash)
288 result = command.RunPipe([pipe], capture=True, cwd=output_dir,
289 capture_stderr=True)
290 if result.return_code != 0:
291 raise OSError('git worktree add: %s' % result.stderr)
292
293def PruneWorktrees(git_dir):
294 """Remove administrative files for deleted worktrees
295
296 Args:
297 git_dir: The repository whose deleted worktrees should be pruned
298 """
299 pipe = ['git', '--git-dir', git_dir, 'worktree', 'prune']
300 result = command.RunPipe([pipe], capture=True, capture_stderr=True)
301 if result.return_code != 0:
302 raise OSError('git worktree prune: %s' % result.stderr)
303
Simon Glass262130f2020-07-05 21:41:51 -0600304def CreatePatches(branch, start, count, ignore_binary, series):
Simon Glass0d24de92012-01-14 15:12:45 +0000305 """Create a series of patches from the top of the current branch.
306
307 The patch files are written to the current directory using
308 git format-patch.
309
310 Args:
Simon Glass262130f2020-07-05 21:41:51 -0600311 branch: Branch to create patches from (None for current branch)
Simon Glass0d24de92012-01-14 15:12:45 +0000312 start: Commit to start from: 0=HEAD, 1=next one, etc.
313 count: number of commits to include
Simon Glass7d5b04e2020-07-05 21:41:49 -0600314 ignore_binary: Don't generate patches for binary files
315 series: Series object for this series (set of patches)
Simon Glass0d24de92012-01-14 15:12:45 +0000316 Return:
Simon Glass7d5b04e2020-07-05 21:41:49 -0600317 Filename of cover letter (None if none)
Simon Glass0d24de92012-01-14 15:12:45 +0000318 List of filenames of patch files
319 """
320 if series.get('version'):
321 version = '%s ' % series['version']
Masahiro Yamada8d3595a2015-08-31 01:23:32 +0900322 cmd = ['git', 'format-patch', '-M', '--signoff']
Bin Meng14aa35a2020-05-04 00:52:44 -0700323 if ignore_binary:
324 cmd.append('--no-binary')
Simon Glass0d24de92012-01-14 15:12:45 +0000325 if series.get('cover'):
326 cmd.append('--cover-letter')
327 prefix = series.GetPatchPrefix()
328 if prefix:
329 cmd += ['--subject-prefix=%s' % prefix]
Simon Glass262130f2020-07-05 21:41:51 -0600330 brname = branch or 'HEAD'
331 cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
Simon Glass0d24de92012-01-14 15:12:45 +0000332
333 stdout = command.RunList(cmd)
334 files = stdout.splitlines()
335
336 # We have an extra file if there is a cover letter
337 if series.get('cover'):
338 return files[0], files[1:]
339 else:
340 return None, files
341
Simon Glassa1318f72013-03-26 13:09:42 +0000342def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
Simon Glass0d24de92012-01-14 15:12:45 +0000343 """Build a list of email addresses based on an input list.
344
345 Takes a list of email addresses and aliases, and turns this into a list
346 of only email address, by resolving any aliases that are present.
347
348 If the tag is given, then each email address is prepended with this
349 tag and a space. If the tag starts with a minus sign (indicating a
350 command line parameter) then the email address is quoted.
351
352 Args:
353 in_list: List of aliases/email addresses
354 tag: Text to put before each address
Simon Glassa1318f72013-03-26 13:09:42 +0000355 alias: Alias dictionary
356 raise_on_error: True to raise an error when an alias fails to match,
357 False to just print a message.
Simon Glass0d24de92012-01-14 15:12:45 +0000358
359 Returns:
360 List of email addresses
361
362 >>> alias = {}
363 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
364 >>> alias['john'] = ['j.bloggs@napier.co.nz']
365 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
366 >>> alias['boys'] = ['fred', ' john']
367 >>> alias['all'] = ['fred ', 'john', ' mary ']
368 >>> BuildEmailList(['john', 'mary'], None, alias)
369 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
370 >>> BuildEmailList(['john', 'mary'], '--to', alias)
371 ['--to "j.bloggs@napier.co.nz"', \
372'--to "Mary Poppins <m.poppins@cloud.net>"']
373 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
374 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
375 """
376 quote = '"' if tag and tag[0] == '-' else ''
377 raw = []
378 for item in in_list:
Simon Glassa1318f72013-03-26 13:09:42 +0000379 raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000380 result = []
381 for item in raw:
Simon Glass513eace2019-05-14 15:53:50 -0600382 item = tools.FromUnicode(item)
Simon Glass0d24de92012-01-14 15:12:45 +0000383 if not item in result:
384 result.append(item)
385 if tag:
386 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
387 return result
388
Nicolas Boichat94977562020-07-13 10:50:00 +0800389def CheckSuppressCCConfig():
390 """Check if sendemail.suppresscc is configured correctly.
391
392 Returns:
393 True if the option is configured correctly, False otherwise.
394 """
395 suppresscc = command.OutputOneLine('git', 'config', 'sendemail.suppresscc',
396 raise_on_error=False)
397
398 # Other settings should be fine.
399 if suppresscc == 'all' or suppresscc == 'cccmd':
400 col = terminal.Color()
401
402 print((col.Color(col.RED, "error") +
403 ": git config sendemail.suppresscc set to %s\n" % (suppresscc)) +
404 " patman needs --cc-cmd to be run to set the cc list.\n" +
405 " Please run:\n" +
406 " git config --unset sendemail.suppresscc\n" +
407 " Or read the man page:\n" +
408 " git send-email --help\n" +
409 " and set an option that runs --cc-cmd\n")
410 return False
411
412 return True
413
Simon Glassa1318f72013-03-26 13:09:42 +0000414def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
Simon Glassa60aedf2018-06-19 09:56:07 -0600415 self_only=False, alias=None, in_reply_to=None, thread=False,
416 smtp_server=None):
Simon Glass0d24de92012-01-14 15:12:45 +0000417 """Email a patch series.
418
419 Args:
420 series: Series object containing destination info
421 cover_fname: filename of cover letter
422 args: list of filenames of patch files
423 dry_run: Just return the command that would be run
Simon Glassa1318f72013-03-26 13:09:42 +0000424 raise_on_error: True to raise an error when an alias fails to match,
425 False to just print a message.
Simon Glass0d24de92012-01-14 15:12:45 +0000426 cc_fname: Filename of Cc file for per-commit Cc
427 self_only: True to just email to yourself as a test
Doug Anderson6d819922013-03-17 10:31:04 +0000428 in_reply_to: If set we'll pass this to git as --in-reply-to.
429 Should be a message ID that this is in reply to.
Mateusz Kulikowski27067a42016-01-14 20:37:41 +0100430 thread: True to add --thread to git send-email (make
431 all patches reply to cover-letter or first patch in series)
Simon Glassa60aedf2018-06-19 09:56:07 -0600432 smtp_server: SMTP server to use to send patches
Simon Glass0d24de92012-01-14 15:12:45 +0000433
434 Returns:
435 Git command that was/would be run
436
Doug Andersona9700482012-11-26 15:21:40 +0000437 # For the duration of this doctest pretend that we ran patman with ./patman
438 >>> _old_argv0 = sys.argv[0]
439 >>> sys.argv[0] = './patman'
440
Simon Glass0d24de92012-01-14 15:12:45 +0000441 >>> alias = {}
442 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
443 >>> alias['john'] = ['j.bloggs@napier.co.nz']
444 >>> alias['mary'] = ['m.poppins@cloud.net']
445 >>> alias['boys'] = ['fred', ' john']
446 >>> alias['all'] = ['fred ', 'john', ' mary ']
447 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
Simon Glass38a9d3b2020-06-07 06:45:47 -0600448 >>> series = {}
449 >>> series['to'] = ['fred']
450 >>> series['cc'] = ['mary']
Simon Glassa1318f72013-03-26 13:09:42 +0000451 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
452 False, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000453 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
454"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
Simon Glassa1318f72013-03-26 13:09:42 +0000455 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
456 alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000457 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
458"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
Simon Glass38a9d3b2020-06-07 06:45:47 -0600459 >>> series['cc'] = ['all']
Simon Glassa1318f72013-03-26 13:09:42 +0000460 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
461 True, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000462 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
463--cc-cmd cc-fname" cover p1 p2'
Simon Glassa1318f72013-03-26 13:09:42 +0000464 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
465 False, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000466 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
467"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
468"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
Doug Andersona9700482012-11-26 15:21:40 +0000469
470 # Restore argv[0] since we clobbered it.
471 >>> sys.argv[0] = _old_argv0
Simon Glass0d24de92012-01-14 15:12:45 +0000472 """
Simon Glassa1318f72013-03-26 13:09:42 +0000473 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000474 if not to:
Simon Glass785f1542016-07-25 18:59:00 -0600475 git_config_to = command.Output('git', 'config', 'sendemail.to',
476 raise_on_error=False)
Masahiro Yamadaee860c62014-07-18 14:23:20 +0900477 if not git_config_to:
Simon Glass5a1af1d2019-05-14 15:53:36 -0600478 print("No recipient.\n"
479 "Please add something like this to a commit\n"
480 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
481 "Or do something like this\n"
482 "git config sendemail.to u-boot@lists.denx.de")
Masahiro Yamadaee860c62014-07-18 14:23:20 +0900483 return
Peter Tyser21818302015-01-26 11:42:21 -0600484 cc = BuildEmailList(list(set(series.get('cc')) - set(series.get('to'))),
485 '--cc', alias, raise_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000486 if self_only:
Simon Glassa1318f72013-03-26 13:09:42 +0000487 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000488 cc = []
489 cmd = ['git', 'send-email', '--annotate']
Simon Glassa60aedf2018-06-19 09:56:07 -0600490 if smtp_server:
491 cmd.append('--smtp-server=%s' % smtp_server)
Doug Anderson6d819922013-03-17 10:31:04 +0000492 if in_reply_to:
Simon Glassf6a6aaf2019-05-14 15:53:54 -0600493 cmd.append('--in-reply-to="%s"' % tools.FromUnicode(in_reply_to))
Mateusz Kulikowski27067a42016-01-14 20:37:41 +0100494 if thread:
495 cmd.append('--thread')
Doug Anderson6d819922013-03-17 10:31:04 +0000496
Simon Glass0d24de92012-01-14 15:12:45 +0000497 cmd += to
498 cmd += cc
499 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
500 if cover_fname:
501 cmd.append(cover_fname)
502 cmd += args
Simon Glass2df3a012017-05-29 15:31:25 -0600503 cmdstr = ' '.join(cmd)
Simon Glass0d24de92012-01-14 15:12:45 +0000504 if not dry_run:
Simon Glass2df3a012017-05-29 15:31:25 -0600505 os.system(cmdstr)
506 return cmdstr
Simon Glass0d24de92012-01-14 15:12:45 +0000507
508
Simon Glassa1318f72013-03-26 13:09:42 +0000509def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
Simon Glass0d24de92012-01-14 15:12:45 +0000510 """If an email address is an alias, look it up and return the full name
511
512 TODO: Why not just use git's own alias feature?
513
514 Args:
515 lookup_name: Alias or email address to look up
Simon Glassa1318f72013-03-26 13:09:42 +0000516 alias: Dictionary containing aliases (None to use settings default)
517 raise_on_error: True to raise an error when an alias fails to match,
518 False to just print a message.
Simon Glass0d24de92012-01-14 15:12:45 +0000519
520 Returns:
521 tuple:
522 list containing a list of email addresses
523
524 Raises:
525 OSError if a recursive alias reference was found
526 ValueError if an alias was not found
527
528 >>> alias = {}
529 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
530 >>> alias['john'] = ['j.bloggs@napier.co.nz']
531 >>> alias['mary'] = ['m.poppins@cloud.net']
532 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
533 >>> alias['all'] = ['fred ', 'john', ' mary ']
534 >>> alias['loop'] = ['other', 'john', ' mary ']
535 >>> alias['other'] = ['loop', 'john', ' mary ']
536 >>> LookupEmail('mary', alias)
537 ['m.poppins@cloud.net']
538 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
539 ['arthur.wellesley@howe.ro.uk']
540 >>> LookupEmail('boys', alias)
541 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
542 >>> LookupEmail('all', alias)
543 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
544 >>> LookupEmail('odd', alias)
545 Traceback (most recent call last):
546 ...
547 ValueError: Alias 'odd' not found
548 >>> LookupEmail('loop', alias)
549 Traceback (most recent call last):
550 ...
551 OSError: Recursive email alias at 'other'
Simon Glassa1318f72013-03-26 13:09:42 +0000552 >>> LookupEmail('odd', alias, raise_on_error=False)
Simon Glasse752edc2014-08-28 09:43:35 -0600553 Alias 'odd' not found
Simon Glassa1318f72013-03-26 13:09:42 +0000554 []
555 >>> # In this case the loop part will effectively be ignored.
556 >>> LookupEmail('loop', alias, raise_on_error=False)
Simon Glasse752edc2014-08-28 09:43:35 -0600557 Recursive email alias at 'other'
558 Recursive email alias at 'john'
559 Recursive email alias at 'mary'
Simon Glassa1318f72013-03-26 13:09:42 +0000560 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass0d24de92012-01-14 15:12:45 +0000561 """
562 if not alias:
563 alias = settings.alias
564 lookup_name = lookup_name.strip()
565 if '@' in lookup_name: # Perhaps a real email address
566 return [lookup_name]
567
568 lookup_name = lookup_name.lower()
Simon Glassa1318f72013-03-26 13:09:42 +0000569 col = terminal.Color()
Simon Glass0d24de92012-01-14 15:12:45 +0000570
571 out_list = []
Simon Glassa1318f72013-03-26 13:09:42 +0000572 if level > 10:
573 msg = "Recursive email alias at '%s'" % lookup_name
574 if raise_on_error:
Paul Burtonac3fde92016-09-27 16:03:51 +0100575 raise OSError(msg)
Simon Glassa1318f72013-03-26 13:09:42 +0000576 else:
Paul Burtona920a172016-09-27 16:03:50 +0100577 print(col.Color(col.RED, msg))
Simon Glassa1318f72013-03-26 13:09:42 +0000578 return out_list
579
Simon Glass0d24de92012-01-14 15:12:45 +0000580 if lookup_name:
581 if not lookup_name in alias:
Simon Glassa1318f72013-03-26 13:09:42 +0000582 msg = "Alias '%s' not found" % lookup_name
583 if raise_on_error:
Paul Burtonac3fde92016-09-27 16:03:51 +0100584 raise ValueError(msg)
Simon Glassa1318f72013-03-26 13:09:42 +0000585 else:
Paul Burtona920a172016-09-27 16:03:50 +0100586 print(col.Color(col.RED, msg))
Simon Glassa1318f72013-03-26 13:09:42 +0000587 return out_list
Simon Glass0d24de92012-01-14 15:12:45 +0000588 for item in alias[lookup_name]:
Simon Glassa1318f72013-03-26 13:09:42 +0000589 todo = LookupEmail(item, alias, raise_on_error, level + 1)
Simon Glass0d24de92012-01-14 15:12:45 +0000590 for new_item in todo:
591 if not new_item in out_list:
592 out_list.append(new_item)
593
Paul Burtona920a172016-09-27 16:03:50 +0100594 #print("No match for alias '%s'" % lookup_name)
Simon Glass0d24de92012-01-14 15:12:45 +0000595 return out_list
596
597def GetTopLevel():
598 """Return name of top-level directory for this git repo.
599
600 Returns:
601 Full path to git top-level directory
602
603 This test makes sure that we are running tests in the right subdir
604
Doug Andersona9700482012-11-26 15:21:40 +0000605 >>> os.path.realpath(os.path.dirname(__file__)) == \
606 os.path.join(GetTopLevel(), 'tools', 'patman')
Simon Glass0d24de92012-01-14 15:12:45 +0000607 True
608 """
609 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
610
611def GetAliasFile():
612 """Gets the name of the git alias file.
613
614 Returns:
615 Filename of git alias file, or None if none
616 """
Simon Glassdc191502012-12-15 10:42:05 +0000617 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
618 raise_on_error=False)
Simon Glass0d24de92012-01-14 15:12:45 +0000619 if fname:
620 fname = os.path.join(GetTopLevel(), fname.strip())
621 return fname
622
Vikram Narayanan87d65552012-05-23 09:01:06 +0000623def GetDefaultUserName():
624 """Gets the user.name from .gitconfig file.
625
626 Returns:
627 User name found in .gitconfig file, or None if none
628 """
629 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
630 return uname
631
632def GetDefaultUserEmail():
633 """Gets the user.email from the global .gitconfig file.
634
635 Returns:
636 User's email found in .gitconfig file, or None if none
637 """
638 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
639 return uemail
640
Wu, Josh3871cd82015-04-15 10:25:18 +0800641def GetDefaultSubjectPrefix():
642 """Gets the format.subjectprefix from local .git/config file.
643
644 Returns:
645 Subject prefix found in local .git/config file, or None if none
646 """
647 sub_prefix = command.OutputOneLine('git', 'config', 'format.subjectprefix',
648 raise_on_error=False)
649
650 return sub_prefix
651
Simon Glass0d24de92012-01-14 15:12:45 +0000652def Setup():
653 """Set up git utils, by reading the alias files."""
Simon Glass0d24de92012-01-14 15:12:45 +0000654 # Check for a git alias file also
Simon Glass0b703db2014-08-28 09:43:45 -0600655 global use_no_decorate
656
Simon Glass0d24de92012-01-14 15:12:45 +0000657 alias_fname = GetAliasFile()
658 if alias_fname:
659 settings.ReadGitAliases(alias_fname)
Simon Glasse49f14a2014-08-09 15:33:11 -0600660 cmd = LogCmd(None, count=0)
661 use_no_decorate = (command.RunPipe([cmd], raise_on_error=False)
662 .return_code == 0)
Simon Glass0d24de92012-01-14 15:12:45 +0000663
Simon Glass5f6a1c42012-12-15 10:42:07 +0000664def GetHead():
665 """Get the hash of the current HEAD
666
667 Returns:
668 Hash of HEAD
669 """
670 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
671
Simon Glass0d24de92012-01-14 15:12:45 +0000672if __name__ == "__main__":
673 import doctest
674
675 doctest.testmod()