blob: 192d8e69b32a9470d787f3ed0c42a31e577136fb [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}..'
69 pipe = [LogCmd(rev_range, oneline=True), ['wc', '-l']]
Simon Glass34e59432012-12-15 10:42:04 +000070 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
Simon Glass26132882012-01-14 15:12:45 +000071 patch_count = int(stdout)
72 return patch_count
73
Simon Glassf204ab12014-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 Glass11aba512012-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 Glassf204ab12014-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 Glass11aba512012-12-15 10:42:07 +0000131 """
Simon Glassd2e95382013-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 Glassf204ab12014-12-01 17:33:54 -0700138 upstream, msg = GuessUpstream(git_dir, branch)
139 return upstream, msg
Simon Glassd2e95382013-05-08 08:06:08 +0000140
Simon Glass11aba512012-12-15 10:42:07 +0000141 if remote == '.':
Simon Glass7e92f5c2015-01-29 11:35:16 -0700142 return merge, None
Simon Glass11aba512012-12-15 10:42:07 +0000143 elif remote and merge:
144 leaf = merge.split('/')[-1]
Simon Glassf204ab12014-12-01 17:33:54 -0700145 return '%s/%s' % (remote, leaf), None
Simon Glass11aba512012-12-15 10:42:07 +0000146 else:
Paul Burtonf14a1312016-09-27 16:03:51 +0100147 raise ValueError("Cannot determine upstream branch for branch "
Simon Glass11aba512012-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 Glassd2e95382013-05-08 08:06:08 +0000159 access the commits. If the branch does not exist, returns None.
Simon Glass11aba512012-12-15 10:42:07 +0000160 """
Simon Glassf204ab12014-12-01 17:33:54 -0700161 upstream, msg = GetUpstream(git_dir, branch)
Simon Glassd2e95382013-05-08 08:06:08 +0000162 if not upstream:
Simon Glassf204ab12014-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 Glass11aba512012-12-15 10:42:07 +0000166
Simon Glass5eeef462014-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 Gustschinf2bcb322019-10-27 17:55:04 +0100174 Number of patches that exist in the supplied range or None if none
Simon Glass5eeef462014-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 Glass11aba512012-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 Glassd2e95382013-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 Glass11aba512012-12-15 10:42:07 +0000194 """
Simon Glassf204ab12014-12-01 17:33:54 -0700195 range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream)
Simon Glassd2e95382013-05-08 08:06:08 +0000196 if not range_expr:
Simon Glassf204ab12014-12-01 17:33:54 -0700197 return None, msg
Simon Glass5eeef462014-12-01 17:33:57 -0700198 return CountCommitsInRange(git_dir, range_expr)
Simon Glass11aba512012-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 Glassb9dbcb42014-08-09 15:33:10 -0600208 pipe = [LogCmd(commit_range, oneline=True),
Simon Glass11aba512012-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 Glassf1bf6862014-09-05 19:00:09 -0600229 result = command.RunPipe([pipe], capture=True, raise_on_error=False,
230 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000231 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100232 raise OSError('git checkout (%s): %s' % (pipe, result.stderr))
Simon Glass11aba512012-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 Glassf1bf6862014-09-05 19:00:09 -0600241 result = command.RunPipe([pipe], capture=True, cwd=output_dir,
242 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000243 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100244 raise OSError('git clone: %s' % result.stderr)
Simon Glass11aba512012-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 Glassf1bf6862014-09-05 19:00:09 -0600258 result = command.RunPipe([pipe], capture=True, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000259 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100260 raise OSError('git fetch: %s' % result.stderr)
Simon Glass11aba512012-12-15 10:42:07 +0000261
Simon Glass2eb4da72020-07-05 21:41:51 -0600262def CreatePatches(branch, start, count, ignore_binary, series):
Simon Glass26132882012-01-14 15:12:45 +0000263 """Create a series of patches from the top of the current branch.
264
265 The patch files are written to the current directory using
266 git format-patch.
267
268 Args:
Simon Glass2eb4da72020-07-05 21:41:51 -0600269 branch: Branch to create patches from (None for current branch)
Simon Glass26132882012-01-14 15:12:45 +0000270 start: Commit to start from: 0=HEAD, 1=next one, etc.
271 count: number of commits to include
Simon Glass24725af2020-07-05 21:41:49 -0600272 ignore_binary: Don't generate patches for binary files
273 series: Series object for this series (set of patches)
Simon Glass26132882012-01-14 15:12:45 +0000274 Return:
Simon Glass24725af2020-07-05 21:41:49 -0600275 Filename of cover letter (None if none)
Simon Glass26132882012-01-14 15:12:45 +0000276 List of filenames of patch files
277 """
278 if series.get('version'):
279 version = '%s ' % series['version']
Masahiro Yamada41d176f2015-08-31 01:23:32 +0900280 cmd = ['git', 'format-patch', '-M', '--signoff']
Bin Menga04f1212020-05-04 00:52:44 -0700281 if ignore_binary:
282 cmd.append('--no-binary')
Simon Glass26132882012-01-14 15:12:45 +0000283 if series.get('cover'):
284 cmd.append('--cover-letter')
285 prefix = series.GetPatchPrefix()
286 if prefix:
287 cmd += ['--subject-prefix=%s' % prefix]
Simon Glass2eb4da72020-07-05 21:41:51 -0600288 brname = branch or 'HEAD'
289 cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
Simon Glass26132882012-01-14 15:12:45 +0000290
291 stdout = command.RunList(cmd)
292 files = stdout.splitlines()
293
294 # We have an extra file if there is a cover letter
295 if series.get('cover'):
296 return files[0], files[1:]
297 else:
298 return None, files
299
Simon Glass12ea5f42013-03-26 13:09:42 +0000300def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
Simon Glass26132882012-01-14 15:12:45 +0000301 """Build a list of email addresses based on an input list.
302
303 Takes a list of email addresses and aliases, and turns this into a list
304 of only email address, by resolving any aliases that are present.
305
306 If the tag is given, then each email address is prepended with this
307 tag and a space. If the tag starts with a minus sign (indicating a
308 command line parameter) then the email address is quoted.
309
310 Args:
311 in_list: List of aliases/email addresses
312 tag: Text to put before each address
Simon Glass12ea5f42013-03-26 13:09:42 +0000313 alias: Alias dictionary
314 raise_on_error: True to raise an error when an alias fails to match,
315 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000316
317 Returns:
318 List of email addresses
319
320 >>> alias = {}
321 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
322 >>> alias['john'] = ['j.bloggs@napier.co.nz']
323 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
324 >>> alias['boys'] = ['fred', ' john']
325 >>> alias['all'] = ['fred ', 'john', ' mary ']
326 >>> BuildEmailList(['john', 'mary'], None, alias)
327 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
328 >>> BuildEmailList(['john', 'mary'], '--to', alias)
329 ['--to "j.bloggs@napier.co.nz"', \
330'--to "Mary Poppins <m.poppins@cloud.net>"']
331 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
332 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
333 """
334 quote = '"' if tag and tag[0] == '-' else ''
335 raw = []
336 for item in in_list:
Simon Glass12ea5f42013-03-26 13:09:42 +0000337 raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000338 result = []
339 for item in raw:
Simon Glass8bb7a7a2019-05-14 15:53:50 -0600340 item = tools.FromUnicode(item)
Simon Glass26132882012-01-14 15:12:45 +0000341 if not item in result:
342 result.append(item)
343 if tag:
344 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
345 return result
346
Nicolas Boichat0da95742020-07-13 10:50:00 +0800347def CheckSuppressCCConfig():
348 """Check if sendemail.suppresscc is configured correctly.
349
350 Returns:
351 True if the option is configured correctly, False otherwise.
352 """
353 suppresscc = command.OutputOneLine('git', 'config', 'sendemail.suppresscc',
354 raise_on_error=False)
355
356 # Other settings should be fine.
357 if suppresscc == 'all' or suppresscc == 'cccmd':
358 col = terminal.Color()
359
360 print((col.Color(col.RED, "error") +
361 ": git config sendemail.suppresscc set to %s\n" % (suppresscc)) +
362 " patman needs --cc-cmd to be run to set the cc list.\n" +
363 " Please run:\n" +
364 " git config --unset sendemail.suppresscc\n" +
365 " Or read the man page:\n" +
366 " git send-email --help\n" +
367 " and set an option that runs --cc-cmd\n")
368 return False
369
370 return True
371
Simon Glass12ea5f42013-03-26 13:09:42 +0000372def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
Simon Glass8137e302018-06-19 09:56:07 -0600373 self_only=False, alias=None, in_reply_to=None, thread=False,
374 smtp_server=None):
Simon Glass26132882012-01-14 15:12:45 +0000375 """Email a patch series.
376
377 Args:
378 series: Series object containing destination info
379 cover_fname: filename of cover letter
380 args: list of filenames of patch files
381 dry_run: Just return the command that would be run
Simon Glass12ea5f42013-03-26 13:09:42 +0000382 raise_on_error: True to raise an error when an alias fails to match,
383 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000384 cc_fname: Filename of Cc file for per-commit Cc
385 self_only: True to just email to yourself as a test
Doug Anderson06f27ac2013-03-17 10:31:04 +0000386 in_reply_to: If set we'll pass this to git as --in-reply-to.
387 Should be a message ID that this is in reply to.
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100388 thread: True to add --thread to git send-email (make
389 all patches reply to cover-letter or first patch in series)
Simon Glass8137e302018-06-19 09:56:07 -0600390 smtp_server: SMTP server to use to send patches
Simon Glass26132882012-01-14 15:12:45 +0000391
392 Returns:
393 Git command that was/would be run
394
Doug Anderson51d73212012-11-26 15:21:40 +0000395 # For the duration of this doctest pretend that we ran patman with ./patman
396 >>> _old_argv0 = sys.argv[0]
397 >>> sys.argv[0] = './patman'
398
Simon Glass26132882012-01-14 15:12:45 +0000399 >>> alias = {}
400 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
401 >>> alias['john'] = ['j.bloggs@napier.co.nz']
402 >>> alias['mary'] = ['m.poppins@cloud.net']
403 >>> alias['boys'] = ['fred', ' john']
404 >>> alias['all'] = ['fred ', 'john', ' mary ']
405 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
Simon Glass794c2592020-06-07 06:45:47 -0600406 >>> series = {}
407 >>> series['to'] = ['fred']
408 >>> series['cc'] = ['mary']
Simon Glass12ea5f42013-03-26 13:09:42 +0000409 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
410 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000411 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
412"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
Simon Glass12ea5f42013-03-26 13:09:42 +0000413 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
414 alias)
Simon Glass26132882012-01-14 15:12:45 +0000415 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
416"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
Simon Glass794c2592020-06-07 06:45:47 -0600417 >>> series['cc'] = ['all']
Simon Glass12ea5f42013-03-26 13:09:42 +0000418 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
419 True, alias)
Simon Glass26132882012-01-14 15:12:45 +0000420 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
421--cc-cmd cc-fname" cover p1 p2'
Simon Glass12ea5f42013-03-26 13:09:42 +0000422 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
423 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000424 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
425"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
426"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
Doug Anderson51d73212012-11-26 15:21:40 +0000427
428 # Restore argv[0] since we clobbered it.
429 >>> sys.argv[0] = _old_argv0
Simon Glass26132882012-01-14 15:12:45 +0000430 """
Simon Glass12ea5f42013-03-26 13:09:42 +0000431 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000432 if not to:
Simon Glassc55e0562016-07-25 18:59:00 -0600433 git_config_to = command.Output('git', 'config', 'sendemail.to',
434 raise_on_error=False)
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900435 if not git_config_to:
Simon Glass23b8a192019-05-14 15:53:36 -0600436 print("No recipient.\n"
437 "Please add something like this to a commit\n"
438 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
439 "Or do something like this\n"
440 "git config sendemail.to u-boot@lists.denx.de")
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900441 return
Peter Tyserc6af8022015-01-26 11:42:21 -0600442 cc = BuildEmailList(list(set(series.get('cc')) - set(series.get('to'))),
443 '--cc', alias, raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000444 if self_only:
Simon Glass12ea5f42013-03-26 13:09:42 +0000445 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000446 cc = []
447 cmd = ['git', 'send-email', '--annotate']
Simon Glass8137e302018-06-19 09:56:07 -0600448 if smtp_server:
449 cmd.append('--smtp-server=%s' % smtp_server)
Doug Anderson06f27ac2013-03-17 10:31:04 +0000450 if in_reply_to:
Simon Glassb0976962019-05-14 15:53:54 -0600451 cmd.append('--in-reply-to="%s"' % tools.FromUnicode(in_reply_to))
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100452 if thread:
453 cmd.append('--thread')
Doug Anderson06f27ac2013-03-17 10:31:04 +0000454
Simon Glass26132882012-01-14 15:12:45 +0000455 cmd += to
456 cmd += cc
457 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
458 if cover_fname:
459 cmd.append(cover_fname)
460 cmd += args
Simon Glass47e308e2017-05-29 15:31:25 -0600461 cmdstr = ' '.join(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000462 if not dry_run:
Simon Glass47e308e2017-05-29 15:31:25 -0600463 os.system(cmdstr)
464 return cmdstr
Simon Glass26132882012-01-14 15:12:45 +0000465
466
Simon Glass12ea5f42013-03-26 13:09:42 +0000467def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
Simon Glass26132882012-01-14 15:12:45 +0000468 """If an email address is an alias, look it up and return the full name
469
470 TODO: Why not just use git's own alias feature?
471
472 Args:
473 lookup_name: Alias or email address to look up
Simon Glass12ea5f42013-03-26 13:09:42 +0000474 alias: Dictionary containing aliases (None to use settings default)
475 raise_on_error: True to raise an error when an alias fails to match,
476 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000477
478 Returns:
479 tuple:
480 list containing a list of email addresses
481
482 Raises:
483 OSError if a recursive alias reference was found
484 ValueError if an alias was not found
485
486 >>> alias = {}
487 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
488 >>> alias['john'] = ['j.bloggs@napier.co.nz']
489 >>> alias['mary'] = ['m.poppins@cloud.net']
490 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
491 >>> alias['all'] = ['fred ', 'john', ' mary ']
492 >>> alias['loop'] = ['other', 'john', ' mary ']
493 >>> alias['other'] = ['loop', 'john', ' mary ']
494 >>> LookupEmail('mary', alias)
495 ['m.poppins@cloud.net']
496 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
497 ['arthur.wellesley@howe.ro.uk']
498 >>> LookupEmail('boys', alias)
499 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
500 >>> LookupEmail('all', alias)
501 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
502 >>> LookupEmail('odd', alias)
503 Traceback (most recent call last):
504 ...
505 ValueError: Alias 'odd' not found
506 >>> LookupEmail('loop', alias)
507 Traceback (most recent call last):
508 ...
509 OSError: Recursive email alias at 'other'
Simon Glass12ea5f42013-03-26 13:09:42 +0000510 >>> LookupEmail('odd', alias, raise_on_error=False)
Simon Glassb0cd3412014-08-28 09:43:35 -0600511 Alias 'odd' not found
Simon Glass12ea5f42013-03-26 13:09:42 +0000512 []
513 >>> # In this case the loop part will effectively be ignored.
514 >>> LookupEmail('loop', alias, raise_on_error=False)
Simon Glassb0cd3412014-08-28 09:43:35 -0600515 Recursive email alias at 'other'
516 Recursive email alias at 'john'
517 Recursive email alias at 'mary'
Simon Glass12ea5f42013-03-26 13:09:42 +0000518 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass26132882012-01-14 15:12:45 +0000519 """
520 if not alias:
521 alias = settings.alias
522 lookup_name = lookup_name.strip()
523 if '@' in lookup_name: # Perhaps a real email address
524 return [lookup_name]
525
526 lookup_name = lookup_name.lower()
Simon Glass12ea5f42013-03-26 13:09:42 +0000527 col = terminal.Color()
Simon Glass26132882012-01-14 15:12:45 +0000528
Simon Glass12ea5f42013-03-26 13:09:42 +0000529 out_list = []
Simon Glass26132882012-01-14 15:12:45 +0000530 if level > 10:
Simon Glass12ea5f42013-03-26 13:09:42 +0000531 msg = "Recursive email alias at '%s'" % lookup_name
532 if raise_on_error:
Paul Burtonf14a1312016-09-27 16:03:51 +0100533 raise OSError(msg)
Simon Glass12ea5f42013-03-26 13:09:42 +0000534 else:
Paul Burtonc3931342016-09-27 16:03:50 +0100535 print(col.Color(col.RED, msg))
Simon Glass12ea5f42013-03-26 13:09:42 +0000536 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000537
Simon Glass26132882012-01-14 15:12:45 +0000538 if lookup_name:
539 if not lookup_name in alias:
Simon Glass12ea5f42013-03-26 13:09:42 +0000540 msg = "Alias '%s' not found" % lookup_name
541 if raise_on_error:
Paul Burtonf14a1312016-09-27 16:03:51 +0100542 raise ValueError(msg)
Simon Glass12ea5f42013-03-26 13:09:42 +0000543 else:
Paul Burtonc3931342016-09-27 16:03:50 +0100544 print(col.Color(col.RED, msg))
Simon Glass12ea5f42013-03-26 13:09:42 +0000545 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000546 for item in alias[lookup_name]:
Simon Glass12ea5f42013-03-26 13:09:42 +0000547 todo = LookupEmail(item, alias, raise_on_error, level + 1)
Simon Glass26132882012-01-14 15:12:45 +0000548 for new_item in todo:
549 if not new_item in out_list:
550 out_list.append(new_item)
551
Paul Burtonc3931342016-09-27 16:03:50 +0100552 #print("No match for alias '%s'" % lookup_name)
Simon Glass26132882012-01-14 15:12:45 +0000553 return out_list
554
555def GetTopLevel():
556 """Return name of top-level directory for this git repo.
557
558 Returns:
559 Full path to git top-level directory
560
561 This test makes sure that we are running tests in the right subdir
562
Doug Anderson51d73212012-11-26 15:21:40 +0000563 >>> os.path.realpath(os.path.dirname(__file__)) == \
564 os.path.join(GetTopLevel(), 'tools', 'patman')
Simon Glass26132882012-01-14 15:12:45 +0000565 True
566 """
567 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
568
569def GetAliasFile():
570 """Gets the name of the git alias file.
571
572 Returns:
573 Filename of git alias file, or None if none
574 """
Simon Glass519fad22012-12-15 10:42:05 +0000575 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
576 raise_on_error=False)
Simon Glass26132882012-01-14 15:12:45 +0000577 if fname:
578 fname = os.path.join(GetTopLevel(), fname.strip())
579 return fname
580
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000581def GetDefaultUserName():
582 """Gets the user.name from .gitconfig file.
583
584 Returns:
585 User name found in .gitconfig file, or None if none
586 """
587 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
588 return uname
589
590def GetDefaultUserEmail():
591 """Gets the user.email from the global .gitconfig file.
592
593 Returns:
594 User's email found in .gitconfig file, or None if none
595 """
596 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
597 return uemail
598
Wu, Josh9873b912015-04-15 10:25:18 +0800599def GetDefaultSubjectPrefix():
600 """Gets the format.subjectprefix from local .git/config file.
601
602 Returns:
603 Subject prefix found in local .git/config file, or None if none
604 """
605 sub_prefix = command.OutputOneLine('git', 'config', 'format.subjectprefix',
606 raise_on_error=False)
607
608 return sub_prefix
609
Simon Glass26132882012-01-14 15:12:45 +0000610def Setup():
611 """Set up git utils, by reading the alias files."""
Simon Glass26132882012-01-14 15:12:45 +0000612 # Check for a git alias file also
Simon Glass81bcca82014-08-28 09:43:45 -0600613 global use_no_decorate
614
Simon Glass26132882012-01-14 15:12:45 +0000615 alias_fname = GetAliasFile()
616 if alias_fname:
617 settings.ReadGitAliases(alias_fname)
Simon Glass6af913d2014-08-09 15:33:11 -0600618 cmd = LogCmd(None, count=0)
619 use_no_decorate = (command.RunPipe([cmd], raise_on_error=False)
620 .return_code == 0)
Simon Glass26132882012-01-14 15:12:45 +0000621
Simon Glass11aba512012-12-15 10:42:07 +0000622def GetHead():
623 """Get the hash of the current HEAD
624
625 Returns:
626 Hash of HEAD
627 """
628 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
629
Simon Glass26132882012-01-14 15:12:45 +0000630if __name__ == "__main__":
631 import doctest
632
633 doctest.testmod()