blob: 31fb3b28299749fea46367f710216a042db798d7 [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass26132882012-01-14 15:12:45 +00002# Copyright (c) 2011 The Chromium OS Authors.
3#
Simon Glass26132882012-01-14 15:12:45 +00004
Simon Glass26132882012-01-14 15:12:45 +00005import re
6import os
Simon Glass26132882012-01-14 15:12:45 +00007import subprocess
8import sys
Simon Glass26132882012-01-14 15:12:45 +00009
Simon Glassa997ea52020-04-17 18:09:04 -060010from patman import command
Simon Glassa997ea52020-04-17 18:09:04 -060011from patman import settings
12from patman import terminal
13from patman import tools
Simon Glass11aba512012-12-15 10:42:07 +000014
Simon Glass6af913d2014-08-09 15:33:11 -060015# True to use --no-decorate - we check this in Setup()
16use_no_decorate = True
17
Simon Glassb9dbcb42014-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 Gustschinf2bcb322019-10-27 17:55:04 +010024 git_dir: Path to git repository (None to use default)
Simon Glassb9dbcb42014-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 Glass5f4e00d2014-08-28 09:43:37 -060034 cmd += ['--no-pager', 'log', '--no-color']
Simon Glassb9dbcb42014-08-09 15:33:10 -060035 if oneline:
36 cmd.append('--oneline')
Simon Glass6af913d2014-08-09 15:33:11 -060037 if use_no_decorate:
38 cmd.append('--no-decorate')
Simon Glass299b9092014-08-14 21:59:11 -060039 if reverse:
40 cmd.append('--reverse')
Simon Glassb9dbcb42014-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 Glass642e9a62016-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 Glassb9dbcb42014-08-09 15:33:10 -060050 return cmd
Simon Glass26132882012-01-14 15:12:45 +000051
Simon Glass2eb4da72020-07-05 21:41:51 -060052def CountCommitsToBranch(branch):
Simon Glass26132882012-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 Glass2eb4da72020-07-05 21:41:51 -060058 Args:
59 branch: Branch to count from (None for current branch)
60
Simon Glass26132882012-01-14 15:12:45 +000061 Return:
62 Number of patches that exist on top of the branch
63 """
Simon Glass2eb4da72020-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}..'
Simon Glass1c1f2072020-10-29 21:46:34 -060069 pipe = [LogCmd(rev_range, oneline=True)]
70 result = command.RunPipe(pipe, capture=True, capture_stderr=True,
71 oneline=True, raise_on_error=False)
72 if result.return_code:
73 raise ValueError('Failed to determine upstream: %s' %
74 result.stderr.strip())
75 patch_count = len(result.stdout.splitlines())
Simon Glass26132882012-01-14 15:12:45 +000076 return patch_count
77
Simon Glassf204ab12014-12-01 17:33:54 -070078def NameRevision(commit_hash):
79 """Gets the revision name for a commit
80
81 Args:
82 commit_hash: Commit hash to look up
83
84 Return:
85 Name of revision, if any, else None
86 """
87 pipe = ['git', 'name-rev', commit_hash]
88 stdout = command.RunPipe([pipe], capture=True, oneline=True).stdout
89
90 # We expect a commit, a space, then a revision name
91 name = stdout.split(' ')[1].strip()
92 return name
93
94def GuessUpstream(git_dir, branch):
95 """Tries to guess the upstream for a branch
96
97 This lists out top commits on a branch and tries to find a suitable
98 upstream. It does this by looking for the first commit where
99 'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
100
101 Args:
102 git_dir: Git directory containing repo
103 branch: Name of branch
104
105 Returns:
106 Tuple:
107 Name of upstream branch (e.g. 'upstream/master') or None if none
108 Warning/error message, or None if none
109 """
110 pipe = [LogCmd(branch, git_dir=git_dir, oneline=True, count=100)]
111 result = command.RunPipe(pipe, capture=True, capture_stderr=True,
112 raise_on_error=False)
113 if result.return_code:
114 return None, "Branch '%s' not found" % branch
115 for line in result.stdout.splitlines()[1:]:
116 commit_hash = line.split(' ')[0]
117 name = NameRevision(commit_hash)
118 if '~' not in name and '^' not in name:
119 if name.startswith('remotes/'):
120 name = name[8:]
121 return name, "Guessing upstream as '%s'" % name
122 return None, "Cannot find a suitable upstream for branch '%s'" % branch
123
Simon Glass11aba512012-12-15 10:42:07 +0000124def GetUpstream(git_dir, branch):
125 """Returns the name of the upstream for a branch
126
127 Args:
128 git_dir: Git directory containing repo
129 branch: Name of branch
130
131 Returns:
Simon Glassf204ab12014-12-01 17:33:54 -0700132 Tuple:
133 Name of upstream branch (e.g. 'upstream/master') or None if none
134 Warning/error message, or None if none
Simon Glass11aba512012-12-15 10:42:07 +0000135 """
Simon Glassd2e95382013-05-08 08:06:08 +0000136 try:
137 remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
138 'branch.%s.remote' % branch)
139 merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
140 'branch.%s.merge' % branch)
141 except:
Simon Glassf204ab12014-12-01 17:33:54 -0700142 upstream, msg = GuessUpstream(git_dir, branch)
143 return upstream, msg
Simon Glassd2e95382013-05-08 08:06:08 +0000144
Simon Glass11aba512012-12-15 10:42:07 +0000145 if remote == '.':
Simon Glass7e92f5c2015-01-29 11:35:16 -0700146 return merge, None
Simon Glass11aba512012-12-15 10:42:07 +0000147 elif remote and merge:
148 leaf = merge.split('/')[-1]
Simon Glassf204ab12014-12-01 17:33:54 -0700149 return '%s/%s' % (remote, leaf), None
Simon Glass11aba512012-12-15 10:42:07 +0000150 else:
Paul Burtonf14a1312016-09-27 16:03:51 +0100151 raise ValueError("Cannot determine upstream branch for branch "
Simon Glass11aba512012-12-15 10:42:07 +0000152 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
153
154
155def GetRangeInBranch(git_dir, branch, include_upstream=False):
156 """Returns an expression for the commits in the given branch.
157
158 Args:
159 git_dir: Directory containing git repo
160 branch: Name of branch
161 Return:
162 Expression in the form 'upstream..branch' which can be used to
Simon Glassd2e95382013-05-08 08:06:08 +0000163 access the commits. If the branch does not exist, returns None.
Simon Glass11aba512012-12-15 10:42:07 +0000164 """
Simon Glassf204ab12014-12-01 17:33:54 -0700165 upstream, msg = GetUpstream(git_dir, branch)
Simon Glassd2e95382013-05-08 08:06:08 +0000166 if not upstream:
Simon Glassf204ab12014-12-01 17:33:54 -0700167 return None, msg
168 rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
169 return rstr, msg
Simon Glass11aba512012-12-15 10:42:07 +0000170
Simon Glass5eeef462014-12-01 17:33:57 -0700171def CountCommitsInRange(git_dir, range_expr):
172 """Returns the number of commits in the given range.
173
174 Args:
175 git_dir: Directory containing git repo
176 range_expr: Range to check
177 Return:
Anatolij Gustschinf2bcb322019-10-27 17:55:04 +0100178 Number of patches that exist in the supplied range or None if none
Simon Glass5eeef462014-12-01 17:33:57 -0700179 were found
180 """
181 pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True)]
182 result = command.RunPipe(pipe, capture=True, capture_stderr=True,
183 raise_on_error=False)
184 if result.return_code:
185 return None, "Range '%s' not found or is invalid" % range_expr
186 patch_count = len(result.stdout.splitlines())
187 return patch_count, None
188
Simon Glass11aba512012-12-15 10:42:07 +0000189def CountCommitsInBranch(git_dir, branch, include_upstream=False):
190 """Returns the number of commits in the given branch.
191
192 Args:
193 git_dir: Directory containing git repo
194 branch: Name of branch
195 Return:
Simon Glassd2e95382013-05-08 08:06:08 +0000196 Number of patches that exist on top of the branch, or None if the
197 branch does not exist.
Simon Glass11aba512012-12-15 10:42:07 +0000198 """
Simon Glassf204ab12014-12-01 17:33:54 -0700199 range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream)
Simon Glassd2e95382013-05-08 08:06:08 +0000200 if not range_expr:
Simon Glassf204ab12014-12-01 17:33:54 -0700201 return None, msg
Simon Glass5eeef462014-12-01 17:33:57 -0700202 return CountCommitsInRange(git_dir, range_expr)
Simon Glass11aba512012-12-15 10:42:07 +0000203
204def CountCommits(commit_range):
205 """Returns the number of commits in the given range.
206
207 Args:
208 commit_range: Range of commits to count (e.g. 'HEAD..base')
209 Return:
210 Number of patches that exist on top of the branch
211 """
Simon Glassb9dbcb42014-08-09 15:33:10 -0600212 pipe = [LogCmd(commit_range, oneline=True),
Simon Glass11aba512012-12-15 10:42:07 +0000213 ['wc', '-l']]
214 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
215 patch_count = int(stdout)
216 return patch_count
217
218def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
219 """Checkout the selected commit for this build
220
221 Args:
222 commit_hash: Commit hash to check out
223 """
224 pipe = ['git']
225 if git_dir:
226 pipe.extend(['--git-dir', git_dir])
227 if work_tree:
228 pipe.extend(['--work-tree', work_tree])
229 pipe.append('checkout')
230 if force:
231 pipe.append('-f')
232 pipe.append(commit_hash)
Simon Glassf1bf6862014-09-05 19:00:09 -0600233 result = command.RunPipe([pipe], capture=True, raise_on_error=False,
234 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000235 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100236 raise OSError('git checkout (%s): %s' % (pipe, result.stderr))
Simon Glass11aba512012-12-15 10:42:07 +0000237
238def Clone(git_dir, output_dir):
239 """Checkout the selected commit for this build
240
241 Args:
242 commit_hash: Commit hash to check out
243 """
244 pipe = ['git', 'clone', git_dir, '.']
Simon Glassf1bf6862014-09-05 19:00:09 -0600245 result = command.RunPipe([pipe], capture=True, cwd=output_dir,
246 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000247 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100248 raise OSError('git clone: %s' % result.stderr)
Simon Glass11aba512012-12-15 10:42:07 +0000249
250def Fetch(git_dir=None, work_tree=None):
251 """Fetch from the origin repo
252
253 Args:
254 commit_hash: Commit hash to check out
255 """
256 pipe = ['git']
257 if git_dir:
258 pipe.extend(['--git-dir', git_dir])
259 if work_tree:
260 pipe.extend(['--work-tree', work_tree])
261 pipe.append('fetch')
Simon Glassf1bf6862014-09-05 19:00:09 -0600262 result = command.RunPipe([pipe], capture=True, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000263 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100264 raise OSError('git fetch: %s' % result.stderr)
Simon Glass11aba512012-12-15 10:42:07 +0000265
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300266def CheckWorktreeIsAvailable(git_dir):
267 """Check if git-worktree functionality is available
268
269 Args:
270 git_dir: The repository to test in
271
272 Returns:
273 True if git-worktree commands will work, False otherwise.
274 """
275 pipe = ['git', '--git-dir', git_dir, 'worktree', 'list']
276 result = command.RunPipe([pipe], capture=True, capture_stderr=True,
277 raise_on_error=False)
278 return result.return_code == 0
279
280def AddWorktree(git_dir, output_dir, commit_hash=None):
281 """Create and checkout a new git worktree for this build
282
283 Args:
284 git_dir: The repository to checkout the worktree from
285 output_dir: Path for the new worktree
286 commit_hash: Commit hash to checkout
287 """
288 # We need to pass --detach to avoid creating a new branch
289 pipe = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach']
290 if commit_hash:
291 pipe.append(commit_hash)
292 result = command.RunPipe([pipe], capture=True, cwd=output_dir,
293 capture_stderr=True)
294 if result.return_code != 0:
295 raise OSError('git worktree add: %s' % result.stderr)
296
297def PruneWorktrees(git_dir):
298 """Remove administrative files for deleted worktrees
299
300 Args:
301 git_dir: The repository whose deleted worktrees should be pruned
302 """
303 pipe = ['git', '--git-dir', git_dir, 'worktree', 'prune']
304 result = command.RunPipe([pipe], capture=True, capture_stderr=True)
305 if result.return_code != 0:
306 raise OSError('git worktree prune: %s' % result.stderr)
307
Simon Glass2eb4da72020-07-05 21:41:51 -0600308def CreatePatches(branch, start, count, ignore_binary, series):
Simon Glass26132882012-01-14 15:12:45 +0000309 """Create a series of patches from the top of the current branch.
310
311 The patch files are written to the current directory using
312 git format-patch.
313
314 Args:
Simon Glass2eb4da72020-07-05 21:41:51 -0600315 branch: Branch to create patches from (None for current branch)
Simon Glass26132882012-01-14 15:12:45 +0000316 start: Commit to start from: 0=HEAD, 1=next one, etc.
317 count: number of commits to include
Simon Glass24725af2020-07-05 21:41:49 -0600318 ignore_binary: Don't generate patches for binary files
319 series: Series object for this series (set of patches)
Simon Glass26132882012-01-14 15:12:45 +0000320 Return:
Simon Glass24725af2020-07-05 21:41:49 -0600321 Filename of cover letter (None if none)
Simon Glass26132882012-01-14 15:12:45 +0000322 List of filenames of patch files
323 """
324 if series.get('version'):
325 version = '%s ' % series['version']
Masahiro Yamada41d176f2015-08-31 01:23:32 +0900326 cmd = ['git', 'format-patch', '-M', '--signoff']
Bin Menga04f1212020-05-04 00:52:44 -0700327 if ignore_binary:
328 cmd.append('--no-binary')
Simon Glass26132882012-01-14 15:12:45 +0000329 if series.get('cover'):
330 cmd.append('--cover-letter')
331 prefix = series.GetPatchPrefix()
332 if prefix:
333 cmd += ['--subject-prefix=%s' % prefix]
Simon Glass2eb4da72020-07-05 21:41:51 -0600334 brname = branch or 'HEAD'
335 cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
Simon Glass26132882012-01-14 15:12:45 +0000336
337 stdout = command.RunList(cmd)
338 files = stdout.splitlines()
339
340 # We have an extra file if there is a cover letter
341 if series.get('cover'):
342 return files[0], files[1:]
343 else:
344 return None, files
345
Simon Glass12ea5f42013-03-26 13:09:42 +0000346def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
Simon Glass26132882012-01-14 15:12:45 +0000347 """Build a list of email addresses based on an input list.
348
349 Takes a list of email addresses and aliases, and turns this into a list
350 of only email address, by resolving any aliases that are present.
351
352 If the tag is given, then each email address is prepended with this
353 tag and a space. If the tag starts with a minus sign (indicating a
354 command line parameter) then the email address is quoted.
355
356 Args:
357 in_list: List of aliases/email addresses
358 tag: Text to put before each address
Simon Glass12ea5f42013-03-26 13:09:42 +0000359 alias: Alias dictionary
360 raise_on_error: True to raise an error when an alias fails to match,
361 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000362
363 Returns:
364 List of email addresses
365
366 >>> alias = {}
367 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
368 >>> alias['john'] = ['j.bloggs@napier.co.nz']
369 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
370 >>> alias['boys'] = ['fred', ' john']
371 >>> alias['all'] = ['fred ', 'john', ' mary ']
372 >>> BuildEmailList(['john', 'mary'], None, alias)
373 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
374 >>> BuildEmailList(['john', 'mary'], '--to', alias)
375 ['--to "j.bloggs@napier.co.nz"', \
376'--to "Mary Poppins <m.poppins@cloud.net>"']
377 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
378 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
379 """
380 quote = '"' if tag and tag[0] == '-' else ''
381 raw = []
382 for item in in_list:
Simon Glass12ea5f42013-03-26 13:09:42 +0000383 raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000384 result = []
385 for item in raw:
Simon Glass8bb7a7a2019-05-14 15:53:50 -0600386 item = tools.FromUnicode(item)
Simon Glass26132882012-01-14 15:12:45 +0000387 if not item in result:
388 result.append(item)
389 if tag:
390 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
391 return result
392
Nicolas Boichat0da95742020-07-13 10:50:00 +0800393def CheckSuppressCCConfig():
394 """Check if sendemail.suppresscc is configured correctly.
395
396 Returns:
397 True if the option is configured correctly, False otherwise.
398 """
399 suppresscc = command.OutputOneLine('git', 'config', 'sendemail.suppresscc',
400 raise_on_error=False)
401
402 # Other settings should be fine.
403 if suppresscc == 'all' or suppresscc == 'cccmd':
404 col = terminal.Color()
405
406 print((col.Color(col.RED, "error") +
407 ": git config sendemail.suppresscc set to %s\n" % (suppresscc)) +
408 " patman needs --cc-cmd to be run to set the cc list.\n" +
409 " Please run:\n" +
410 " git config --unset sendemail.suppresscc\n" +
411 " Or read the man page:\n" +
412 " git send-email --help\n" +
413 " and set an option that runs --cc-cmd\n")
414 return False
415
416 return True
417
Simon Glass12ea5f42013-03-26 13:09:42 +0000418def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
Simon Glass8137e302018-06-19 09:56:07 -0600419 self_only=False, alias=None, in_reply_to=None, thread=False,
420 smtp_server=None):
Simon Glass26132882012-01-14 15:12:45 +0000421 """Email a patch series.
422
423 Args:
424 series: Series object containing destination info
425 cover_fname: filename of cover letter
426 args: list of filenames of patch files
427 dry_run: Just return the command that would be run
Simon Glass12ea5f42013-03-26 13:09:42 +0000428 raise_on_error: True to raise an error when an alias fails to match,
429 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000430 cc_fname: Filename of Cc file for per-commit Cc
431 self_only: True to just email to yourself as a test
Doug Anderson06f27ac2013-03-17 10:31:04 +0000432 in_reply_to: If set we'll pass this to git as --in-reply-to.
433 Should be a message ID that this is in reply to.
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100434 thread: True to add --thread to git send-email (make
435 all patches reply to cover-letter or first patch in series)
Simon Glass8137e302018-06-19 09:56:07 -0600436 smtp_server: SMTP server to use to send patches
Simon Glass26132882012-01-14 15:12:45 +0000437
438 Returns:
439 Git command that was/would be run
440
Doug Anderson51d73212012-11-26 15:21:40 +0000441 # For the duration of this doctest pretend that we ran patman with ./patman
442 >>> _old_argv0 = sys.argv[0]
443 >>> sys.argv[0] = './patman'
444
Simon Glass26132882012-01-14 15:12:45 +0000445 >>> alias = {}
446 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
447 >>> alias['john'] = ['j.bloggs@napier.co.nz']
448 >>> alias['mary'] = ['m.poppins@cloud.net']
449 >>> alias['boys'] = ['fred', ' john']
450 >>> alias['all'] = ['fred ', 'john', ' mary ']
451 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
Simon Glass794c2592020-06-07 06:45:47 -0600452 >>> series = {}
453 >>> series['to'] = ['fred']
454 >>> series['cc'] = ['mary']
Simon Glass12ea5f42013-03-26 13:09:42 +0000455 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
456 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000457 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700458"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Simon Glass12ea5f42013-03-26 13:09:42 +0000459 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
460 alias)
Simon Glass26132882012-01-14 15:12:45 +0000461 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700462"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
Simon Glass794c2592020-06-07 06:45:47 -0600463 >>> series['cc'] = ['all']
Simon Glass12ea5f42013-03-26 13:09:42 +0000464 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
465 True, alias)
Simon Glass26132882012-01-14 15:12:45 +0000466 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
Simon Glass1ee91c12020-11-03 13:54:10 -0700467send --cc-cmd cc-fname" cover p1 p2'
Simon Glass12ea5f42013-03-26 13:09:42 +0000468 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
469 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000470 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
471"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700472"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Doug Anderson51d73212012-11-26 15:21:40 +0000473
474 # Restore argv[0] since we clobbered it.
475 >>> sys.argv[0] = _old_argv0
Simon Glass26132882012-01-14 15:12:45 +0000476 """
Simon Glass12ea5f42013-03-26 13:09:42 +0000477 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000478 if not to:
Simon Glassc55e0562016-07-25 18:59:00 -0600479 git_config_to = command.Output('git', 'config', 'sendemail.to',
480 raise_on_error=False)
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900481 if not git_config_to:
Simon Glass23b8a192019-05-14 15:53:36 -0600482 print("No recipient.\n"
483 "Please add something like this to a commit\n"
484 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
485 "Or do something like this\n"
486 "git config sendemail.to u-boot@lists.denx.de")
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900487 return
Peter Tyserc6af8022015-01-26 11:42:21 -0600488 cc = BuildEmailList(list(set(series.get('cc')) - set(series.get('to'))),
489 '--cc', alias, raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000490 if self_only:
Simon Glass12ea5f42013-03-26 13:09:42 +0000491 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000492 cc = []
493 cmd = ['git', 'send-email', '--annotate']
Simon Glass8137e302018-06-19 09:56:07 -0600494 if smtp_server:
495 cmd.append('--smtp-server=%s' % smtp_server)
Doug Anderson06f27ac2013-03-17 10:31:04 +0000496 if in_reply_to:
Simon Glassb0976962019-05-14 15:53:54 -0600497 cmd.append('--in-reply-to="%s"' % tools.FromUnicode(in_reply_to))
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100498 if thread:
499 cmd.append('--thread')
Doug Anderson06f27ac2013-03-17 10:31:04 +0000500
Simon Glass26132882012-01-14 15:12:45 +0000501 cmd += to
502 cmd += cc
Simon Glass1ee91c12020-11-03 13:54:10 -0700503 cmd += ['--cc-cmd', '"%s send --cc-cmd %s"' % (sys.argv[0], cc_fname)]
Simon Glass26132882012-01-14 15:12:45 +0000504 if cover_fname:
505 cmd.append(cover_fname)
506 cmd += args
Simon Glass47e308e2017-05-29 15:31:25 -0600507 cmdstr = ' '.join(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000508 if not dry_run:
Simon Glass47e308e2017-05-29 15:31:25 -0600509 os.system(cmdstr)
510 return cmdstr
Simon Glass26132882012-01-14 15:12:45 +0000511
512
Simon Glass12ea5f42013-03-26 13:09:42 +0000513def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
Simon Glass26132882012-01-14 15:12:45 +0000514 """If an email address is an alias, look it up and return the full name
515
516 TODO: Why not just use git's own alias feature?
517
518 Args:
519 lookup_name: Alias or email address to look up
Simon Glass12ea5f42013-03-26 13:09:42 +0000520 alias: Dictionary containing aliases (None to use settings default)
521 raise_on_error: True to raise an error when an alias fails to match,
522 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000523
524 Returns:
525 tuple:
526 list containing a list of email addresses
527
528 Raises:
529 OSError if a recursive alias reference was found
530 ValueError if an alias was not found
531
532 >>> alias = {}
533 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
534 >>> alias['john'] = ['j.bloggs@napier.co.nz']
535 >>> alias['mary'] = ['m.poppins@cloud.net']
536 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
537 >>> alias['all'] = ['fred ', 'john', ' mary ']
538 >>> alias['loop'] = ['other', 'john', ' mary ']
539 >>> alias['other'] = ['loop', 'john', ' mary ']
540 >>> LookupEmail('mary', alias)
541 ['m.poppins@cloud.net']
542 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
543 ['arthur.wellesley@howe.ro.uk']
544 >>> LookupEmail('boys', alias)
545 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
546 >>> LookupEmail('all', alias)
547 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
548 >>> LookupEmail('odd', alias)
549 Traceback (most recent call last):
550 ...
551 ValueError: Alias 'odd' not found
552 >>> LookupEmail('loop', alias)
553 Traceback (most recent call last):
554 ...
555 OSError: Recursive email alias at 'other'
Simon Glass12ea5f42013-03-26 13:09:42 +0000556 >>> LookupEmail('odd', alias, raise_on_error=False)
Simon Glassb0cd3412014-08-28 09:43:35 -0600557 Alias 'odd' not found
Simon Glass12ea5f42013-03-26 13:09:42 +0000558 []
559 >>> # In this case the loop part will effectively be ignored.
560 >>> LookupEmail('loop', alias, raise_on_error=False)
Simon Glassb0cd3412014-08-28 09:43:35 -0600561 Recursive email alias at 'other'
562 Recursive email alias at 'john'
563 Recursive email alias at 'mary'
Simon Glass12ea5f42013-03-26 13:09:42 +0000564 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass26132882012-01-14 15:12:45 +0000565 """
566 if not alias:
567 alias = settings.alias
568 lookup_name = lookup_name.strip()
569 if '@' in lookup_name: # Perhaps a real email address
570 return [lookup_name]
571
572 lookup_name = lookup_name.lower()
Simon Glass12ea5f42013-03-26 13:09:42 +0000573 col = terminal.Color()
Simon Glass26132882012-01-14 15:12:45 +0000574
Simon Glass12ea5f42013-03-26 13:09:42 +0000575 out_list = []
Simon Glass26132882012-01-14 15:12:45 +0000576 if level > 10:
Simon Glass12ea5f42013-03-26 13:09:42 +0000577 msg = "Recursive email alias at '%s'" % lookup_name
578 if raise_on_error:
Paul Burtonf14a1312016-09-27 16:03:51 +0100579 raise OSError(msg)
Simon Glass12ea5f42013-03-26 13:09:42 +0000580 else:
Paul Burtonc3931342016-09-27 16:03:50 +0100581 print(col.Color(col.RED, msg))
Simon Glass12ea5f42013-03-26 13:09:42 +0000582 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000583
Simon Glass26132882012-01-14 15:12:45 +0000584 if lookup_name:
585 if not lookup_name in alias:
Simon Glass12ea5f42013-03-26 13:09:42 +0000586 msg = "Alias '%s' not found" % lookup_name
587 if raise_on_error:
Paul Burtonf14a1312016-09-27 16:03:51 +0100588 raise ValueError(msg)
Simon Glass12ea5f42013-03-26 13:09:42 +0000589 else:
Paul Burtonc3931342016-09-27 16:03:50 +0100590 print(col.Color(col.RED, msg))
Simon Glass12ea5f42013-03-26 13:09:42 +0000591 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000592 for item in alias[lookup_name]:
Simon Glass12ea5f42013-03-26 13:09:42 +0000593 todo = LookupEmail(item, alias, raise_on_error, level + 1)
Simon Glass26132882012-01-14 15:12:45 +0000594 for new_item in todo:
595 if not new_item in out_list:
596 out_list.append(new_item)
597
Paul Burtonc3931342016-09-27 16:03:50 +0100598 #print("No match for alias '%s'" % lookup_name)
Simon Glass26132882012-01-14 15:12:45 +0000599 return out_list
600
601def GetTopLevel():
602 """Return name of top-level directory for this git repo.
603
604 Returns:
605 Full path to git top-level directory
606
607 This test makes sure that we are running tests in the right subdir
608
Doug Anderson51d73212012-11-26 15:21:40 +0000609 >>> os.path.realpath(os.path.dirname(__file__)) == \
610 os.path.join(GetTopLevel(), 'tools', 'patman')
Simon Glass26132882012-01-14 15:12:45 +0000611 True
612 """
613 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
614
615def GetAliasFile():
616 """Gets the name of the git alias file.
617
618 Returns:
619 Filename of git alias file, or None if none
620 """
Simon Glass519fad22012-12-15 10:42:05 +0000621 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
622 raise_on_error=False)
Simon Glass26132882012-01-14 15:12:45 +0000623 if fname:
624 fname = os.path.join(GetTopLevel(), fname.strip())
625 return fname
626
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000627def GetDefaultUserName():
628 """Gets the user.name from .gitconfig file.
629
630 Returns:
631 User name found in .gitconfig file, or None if none
632 """
633 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
634 return uname
635
636def GetDefaultUserEmail():
637 """Gets the user.email from the global .gitconfig file.
638
639 Returns:
640 User's email found in .gitconfig file, or None if none
641 """
642 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
643 return uemail
644
Wu, Josh9873b912015-04-15 10:25:18 +0800645def GetDefaultSubjectPrefix():
646 """Gets the format.subjectprefix from local .git/config file.
647
648 Returns:
649 Subject prefix found in local .git/config file, or None if none
650 """
651 sub_prefix = command.OutputOneLine('git', 'config', 'format.subjectprefix',
652 raise_on_error=False)
653
654 return sub_prefix
655
Simon Glass26132882012-01-14 15:12:45 +0000656def Setup():
657 """Set up git utils, by reading the alias files."""
Simon Glass26132882012-01-14 15:12:45 +0000658 # Check for a git alias file also
Simon Glass81bcca82014-08-28 09:43:45 -0600659 global use_no_decorate
660
Simon Glass26132882012-01-14 15:12:45 +0000661 alias_fname = GetAliasFile()
662 if alias_fname:
663 settings.ReadGitAliases(alias_fname)
Simon Glass6af913d2014-08-09 15:33:11 -0600664 cmd = LogCmd(None, count=0)
665 use_no_decorate = (command.RunPipe([cmd], raise_on_error=False)
666 .return_code == 0)
Simon Glass26132882012-01-14 15:12:45 +0000667
Simon Glass11aba512012-12-15 10:42:07 +0000668def GetHead():
669 """Get the hash of the current HEAD
670
671 Returns:
672 Hash of HEAD
673 """
674 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
675
Simon Glass26132882012-01-14 15:12:45 +0000676if __name__ == "__main__":
677 import doctest
678
679 doctest.testmod()