blob: ceaf2ce1504c0dbb913964efda210168d646e406 [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 Glass0157b182022-01-29 14:14:11 -070015# True to use --no-decorate - we check this in setup()
Simon Glasse49f14a2014-08-09 15:33:11 -060016use_no_decorate = True
17
Simon Glass0157b182022-01-29 14:14:11 -070018def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
Simon Glasscda2a612014-08-09 15:33:10 -060019 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 Glass0157b182022-01-29 14:14:11 -070052def count_commits_to_branch(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:
Simon Glass0157b182022-01-29 14:14:11 -070065 us, msg = get_upstream('.git', branch)
Simon Glass262130f2020-07-05 21:41:51 -060066 rev_range = '%s..%s' % (us, branch)
67 else:
68 rev_range = '@{upstream}..'
Simon Glass0157b182022-01-29 14:14:11 -070069 pipe = [log_cmd(rev_range, oneline=True)]
Simon Glassd9800692022-01-29 14:14:05 -070070 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Simon Glassbe051c02020-10-29 21:46:34 -060071 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 Glass0d24de92012-01-14 15:12:45 +000076 return patch_count
77
Simon Glass0157b182022-01-29 14:14:11 -070078def name_revision(commit_hash):
Simon Glass2a9e2c62014-12-01 17:33:54 -070079 """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]
Simon Glassd9800692022-01-29 14:14:05 -070088 stdout = command.run_pipe([pipe], capture=True, oneline=True).stdout
Simon Glass2a9e2c62014-12-01 17:33:54 -070089
90 # We expect a commit, a space, then a revision name
91 name = stdout.split(' ')[1].strip()
92 return name
93
Simon Glass0157b182022-01-29 14:14:11 -070094def guess_upstream(git_dir, branch):
Simon Glass2a9e2c62014-12-01 17:33:54 -070095 """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 """
Simon Glass0157b182022-01-29 14:14:11 -0700110 pipe = [log_cmd(branch, git_dir=git_dir, oneline=True, count=100)]
Simon Glassd9800692022-01-29 14:14:05 -0700111 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Simon Glass2a9e2c62014-12-01 17:33:54 -0700112 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]
Simon Glass0157b182022-01-29 14:14:11 -0700117 name = name_revision(commit_hash)
Simon Glass2a9e2c62014-12-01 17:33:54 -0700118 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 Glass0157b182022-01-29 14:14:11 -0700124def get_upstream(git_dir, branch):
Simon Glass5f6a1c42012-12-15 10:42:07 +0000125 """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 Glass2a9e2c62014-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 Glass5f6a1c42012-12-15 10:42:07 +0000135 """
Simon Glasscce717a2013-05-08 08:06:08 +0000136 try:
Simon Glassd9800692022-01-29 14:14:05 -0700137 remote = command.output_one_line('git', '--git-dir', git_dir, 'config',
Simon Glasscce717a2013-05-08 08:06:08 +0000138 'branch.%s.remote' % branch)
Simon Glassd9800692022-01-29 14:14:05 -0700139 merge = command.output_one_line('git', '--git-dir', git_dir, 'config',
Simon Glasscce717a2013-05-08 08:06:08 +0000140 'branch.%s.merge' % branch)
141 except:
Simon Glass0157b182022-01-29 14:14:11 -0700142 upstream, msg = guess_upstream(git_dir, branch)
Simon Glass2a9e2c62014-12-01 17:33:54 -0700143 return upstream, msg
Simon Glasscce717a2013-05-08 08:06:08 +0000144
Simon Glass5f6a1c42012-12-15 10:42:07 +0000145 if remote == '.':
Simon Glass71edbe52015-01-29 11:35:16 -0700146 return merge, None
Simon Glass5f6a1c42012-12-15 10:42:07 +0000147 elif remote and merge:
148 leaf = merge.split('/')[-1]
Simon Glass2a9e2c62014-12-01 17:33:54 -0700149 return '%s/%s' % (remote, leaf), None
Simon Glass5f6a1c42012-12-15 10:42:07 +0000150 else:
Paul Burtonac3fde92016-09-27 16:03:51 +0100151 raise ValueError("Cannot determine upstream branch for branch "
Simon Glass5f6a1c42012-12-15 10:42:07 +0000152 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
153
154
Simon Glass0157b182022-01-29 14:14:11 -0700155def get_range_in_branch(git_dir, branch, include_upstream=False):
Simon Glass5f6a1c42012-12-15 10:42:07 +0000156 """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 Glasscce717a2013-05-08 08:06:08 +0000163 access the commits. If the branch does not exist, returns None.
Simon Glass5f6a1c42012-12-15 10:42:07 +0000164 """
Simon Glass0157b182022-01-29 14:14:11 -0700165 upstream, msg = get_upstream(git_dir, branch)
Simon Glasscce717a2013-05-08 08:06:08 +0000166 if not upstream:
Simon Glass2a9e2c62014-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 Glass5f6a1c42012-12-15 10:42:07 +0000170
Simon Glass0157b182022-01-29 14:14:11 -0700171def count_commits_in_range(git_dir, range_expr):
Simon Glass5abab202014-12-01 17:33:57 -0700172 """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 Gustschinab4a6ab2019-10-27 17:55:04 +0100178 Number of patches that exist in the supplied range or None if none
Simon Glass5abab202014-12-01 17:33:57 -0700179 were found
180 """
Simon Glass0157b182022-01-29 14:14:11 -0700181 pipe = [log_cmd(range_expr, git_dir=git_dir, oneline=True)]
Simon Glassd9800692022-01-29 14:14:05 -0700182 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Simon Glass5abab202014-12-01 17:33:57 -0700183 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 Glass0157b182022-01-29 14:14:11 -0700189def count_commits_in_branch(git_dir, branch, include_upstream=False):
Simon Glass5f6a1c42012-12-15 10:42:07 +0000190 """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 Glasscce717a2013-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 Glass5f6a1c42012-12-15 10:42:07 +0000198 """
Simon Glass0157b182022-01-29 14:14:11 -0700199 range_expr, msg = get_range_in_branch(git_dir, branch, include_upstream)
Simon Glasscce717a2013-05-08 08:06:08 +0000200 if not range_expr:
Simon Glass2a9e2c62014-12-01 17:33:54 -0700201 return None, msg
Simon Glass0157b182022-01-29 14:14:11 -0700202 return count_commits_in_range(git_dir, range_expr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000203
Simon Glass0157b182022-01-29 14:14:11 -0700204def count_commits(commit_range):
Simon Glass5f6a1c42012-12-15 10:42:07 +0000205 """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 Glass0157b182022-01-29 14:14:11 -0700212 pipe = [log_cmd(commit_range, oneline=True),
Simon Glass5f6a1c42012-12-15 10:42:07 +0000213 ['wc', '-l']]
Simon Glassd9800692022-01-29 14:14:05 -0700214 stdout = command.run_pipe(pipe, capture=True, oneline=True).stdout
Simon Glass5f6a1c42012-12-15 10:42:07 +0000215 patch_count = int(stdout)
216 return patch_count
217
Simon Glass0157b182022-01-29 14:14:11 -0700218def checkout(commit_hash, git_dir=None, work_tree=None, force=False):
Simon Glass5f6a1c42012-12-15 10:42:07 +0000219 """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 Glassd9800692022-01-29 14:14:05 -0700233 result = command.run_pipe([pipe], capture=True, raise_on_error=False,
Simon Glassddaf5c82014-09-05 19:00:09 -0600234 capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000235 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100236 raise OSError('git checkout (%s): %s' % (pipe, result.stderr))
Simon Glass5f6a1c42012-12-15 10:42:07 +0000237
Simon Glass0157b182022-01-29 14:14:11 -0700238def clone(git_dir, output_dir):
Simon Glass5f6a1c42012-12-15 10:42:07 +0000239 """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 Glassd9800692022-01-29 14:14:05 -0700245 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Simon Glassddaf5c82014-09-05 19:00:09 -0600246 capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000247 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100248 raise OSError('git clone: %s' % result.stderr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000249
Simon Glass0157b182022-01-29 14:14:11 -0700250def fetch(git_dir=None, work_tree=None):
Simon Glass5f6a1c42012-12-15 10:42:07 +0000251 """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 Glassd9800692022-01-29 14:14:05 -0700262 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000263 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100264 raise OSError('git fetch: %s' % result.stderr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000265
Simon Glass0157b182022-01-29 14:14:11 -0700266def check_worktree_is_available(git_dir):
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300267 """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']
Simon Glassd9800692022-01-29 14:14:05 -0700276 result = command.run_pipe([pipe], capture=True, capture_stderr=True,
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300277 raise_on_error=False)
278 return result.return_code == 0
279
Simon Glass0157b182022-01-29 14:14:11 -0700280def add_worktree(git_dir, output_dir, commit_hash=None):
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300281 """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)
Simon Glassd9800692022-01-29 14:14:05 -0700292 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300293 capture_stderr=True)
294 if result.return_code != 0:
295 raise OSError('git worktree add: %s' % result.stderr)
296
Simon Glass0157b182022-01-29 14:14:11 -0700297def prune_worktrees(git_dir):
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300298 """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']
Simon Glassd9800692022-01-29 14:14:05 -0700304 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300305 if result.return_code != 0:
306 raise OSError('git worktree prune: %s' % result.stderr)
307
Simon Glass0157b182022-01-29 14:14:11 -0700308def create_patches(branch, start, count, ignore_binary, series, signoff = True):
Simon Glass0d24de92012-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 Glass262130f2020-07-05 21:41:51 -0600315 branch: Branch to create patches from (None for current branch)
Simon Glass0d24de92012-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 Glass7d5b04e2020-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 Glass0d24de92012-01-14 15:12:45 +0000320 Return:
Simon Glass7d5b04e2020-07-05 21:41:49 -0600321 Filename of cover letter (None if none)
Simon Glass0d24de92012-01-14 15:12:45 +0000322 List of filenames of patch files
323 """
324 if series.get('version'):
325 version = '%s ' % series['version']
Philipp Tomsichb3aff152020-11-24 18:14:52 +0100326 cmd = ['git', 'format-patch', '-M' ]
327 if signoff:
328 cmd.append('--signoff')
Bin Meng14aa35a2020-05-04 00:52:44 -0700329 if ignore_binary:
330 cmd.append('--no-binary')
Simon Glass0d24de92012-01-14 15:12:45 +0000331 if series.get('cover'):
332 cmd.append('--cover-letter')
333 prefix = series.GetPatchPrefix()
334 if prefix:
335 cmd += ['--subject-prefix=%s' % prefix]
Simon Glass262130f2020-07-05 21:41:51 -0600336 brname = branch or 'HEAD'
337 cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
Simon Glass0d24de92012-01-14 15:12:45 +0000338
Simon Glassd9800692022-01-29 14:14:05 -0700339 stdout = command.run_list(cmd)
Simon Glass0d24de92012-01-14 15:12:45 +0000340 files = stdout.splitlines()
341
342 # We have an extra file if there is a cover letter
343 if series.get('cover'):
344 return files[0], files[1:]
345 else:
346 return None, files
347
Simon Glass0157b182022-01-29 14:14:11 -0700348def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
Simon Glass0d24de92012-01-14 15:12:45 +0000349 """Build a list of email addresses based on an input list.
350
351 Takes a list of email addresses and aliases, and turns this into a list
352 of only email address, by resolving any aliases that are present.
353
354 If the tag is given, then each email address is prepended with this
355 tag and a space. If the tag starts with a minus sign (indicating a
356 command line parameter) then the email address is quoted.
357
358 Args:
359 in_list: List of aliases/email addresses
360 tag: Text to put before each address
Simon Glassa1318f72013-03-26 13:09:42 +0000361 alias: Alias dictionary
Simon Glass0fb560d2021-01-23 08:56:15 -0700362 warn_on_error: True to raise an error when an alias fails to match,
Simon Glassa1318f72013-03-26 13:09:42 +0000363 False to just print a message.
Simon Glass0d24de92012-01-14 15:12:45 +0000364
365 Returns:
366 List of email addresses
367
368 >>> alias = {}
369 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
370 >>> alias['john'] = ['j.bloggs@napier.co.nz']
371 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
372 >>> alias['boys'] = ['fred', ' john']
373 >>> alias['all'] = ['fred ', 'john', ' mary ']
Simon Glass0157b182022-01-29 14:14:11 -0700374 >>> build_email_list(['john', 'mary'], None, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000375 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
Simon Glass0157b182022-01-29 14:14:11 -0700376 >>> build_email_list(['john', 'mary'], '--to', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000377 ['--to "j.bloggs@napier.co.nz"', \
378'--to "Mary Poppins <m.poppins@cloud.net>"']
Simon Glass0157b182022-01-29 14:14:11 -0700379 >>> build_email_list(['john', 'mary'], 'Cc', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000380 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
381 """
382 quote = '"' if tag and tag[0] == '-' else ''
383 raw = []
384 for item in in_list:
Simon Glass0157b182022-01-29 14:14:11 -0700385 raw += lookup_email(item, alias, warn_on_error=warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000386 result = []
387 for item in raw:
388 if not item in result:
389 result.append(item)
390 if tag:
391 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
392 return result
393
Simon Glass0157b182022-01-29 14:14:11 -0700394def check_suppress_cc_config():
Nicolas Boichat94977562020-07-13 10:50:00 +0800395 """Check if sendemail.suppresscc is configured correctly.
396
397 Returns:
398 True if the option is configured correctly, False otherwise.
399 """
Simon Glassd9800692022-01-29 14:14:05 -0700400 suppresscc = command.output_one_line('git', 'config', 'sendemail.suppresscc',
Nicolas Boichat94977562020-07-13 10:50:00 +0800401 raise_on_error=False)
402
403 # Other settings should be fine.
404 if suppresscc == 'all' or suppresscc == 'cccmd':
405 col = terminal.Color()
406
Simon Glass252ac582022-01-29 14:14:17 -0700407 print((col.build(col.RED, "error") +
Nicolas Boichat94977562020-07-13 10:50:00 +0800408 ": git config sendemail.suppresscc set to %s\n" % (suppresscc)) +
409 " patman needs --cc-cmd to be run to set the cc list.\n" +
410 " Please run:\n" +
411 " git config --unset sendemail.suppresscc\n" +
412 " Or read the man page:\n" +
413 " git send-email --help\n" +
414 " and set an option that runs --cc-cmd\n")
415 return False
416
417 return True
418
Simon Glass0157b182022-01-29 14:14:11 -0700419def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
Simon Glassa60aedf2018-06-19 09:56:07 -0600420 self_only=False, alias=None, in_reply_to=None, thread=False,
421 smtp_server=None):
Simon Glass0d24de92012-01-14 15:12:45 +0000422 """Email a patch series.
423
424 Args:
425 series: Series object containing destination info
426 cover_fname: filename of cover letter
427 args: list of filenames of patch files
428 dry_run: Just return the command that would be run
Simon Glass0fb560d2021-01-23 08:56:15 -0700429 warn_on_error: True to print a warning when an alias fails to match,
430 False to ignore it.
Simon Glass0d24de92012-01-14 15:12:45 +0000431 cc_fname: Filename of Cc file for per-commit Cc
432 self_only: True to just email to yourself as a test
Doug Anderson6d819922013-03-17 10:31:04 +0000433 in_reply_to: If set we'll pass this to git as --in-reply-to.
434 Should be a message ID that this is in reply to.
Mateusz Kulikowski27067a42016-01-14 20:37:41 +0100435 thread: True to add --thread to git send-email (make
436 all patches reply to cover-letter or first patch in series)
Simon Glassa60aedf2018-06-19 09:56:07 -0600437 smtp_server: SMTP server to use to send patches
Simon Glass0d24de92012-01-14 15:12:45 +0000438
439 Returns:
440 Git command that was/would be run
441
Doug Andersona9700482012-11-26 15:21:40 +0000442 # For the duration of this doctest pretend that we ran patman with ./patman
443 >>> _old_argv0 = sys.argv[0]
444 >>> sys.argv[0] = './patman'
445
Simon Glass0d24de92012-01-14 15:12:45 +0000446 >>> alias = {}
447 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
448 >>> alias['john'] = ['j.bloggs@napier.co.nz']
449 >>> alias['mary'] = ['m.poppins@cloud.net']
450 >>> alias['boys'] = ['fred', ' john']
451 >>> alias['all'] = ['fred ', 'john', ' mary ']
452 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
Simon Glass38a9d3b2020-06-07 06:45:47 -0600453 >>> series = {}
454 >>> series['to'] = ['fred']
455 >>> series['cc'] = ['mary']
Simon Glass0157b182022-01-29 14:14:11 -0700456 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glassa1318f72013-03-26 13:09:42 +0000457 False, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000458 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass46007672020-11-03 13:54:10 -0700459"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Simon Glass0157b182022-01-29 14:14:11 -0700460 >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \
Simon Glassa1318f72013-03-26 13:09:42 +0000461 alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000462 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass46007672020-11-03 13:54:10 -0700463"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
Simon Glass38a9d3b2020-06-07 06:45:47 -0600464 >>> series['cc'] = ['all']
Simon Glass0157b182022-01-29 14:14:11 -0700465 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glassa1318f72013-03-26 13:09:42 +0000466 True, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000467 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
Simon Glass46007672020-11-03 13:54:10 -0700468send --cc-cmd cc-fname" cover p1 p2'
Simon Glass0157b182022-01-29 14:14:11 -0700469 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glassa1318f72013-03-26 13:09:42 +0000470 False, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000471 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
472"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
Simon Glass46007672020-11-03 13:54:10 -0700473"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Doug Andersona9700482012-11-26 15:21:40 +0000474
475 # Restore argv[0] since we clobbered it.
476 >>> sys.argv[0] = _old_argv0
Simon Glass0d24de92012-01-14 15:12:45 +0000477 """
Simon Glass0157b182022-01-29 14:14:11 -0700478 to = build_email_list(series.get('to'), '--to', alias, warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000479 if not to:
Simon Glassd9800692022-01-29 14:14:05 -0700480 git_config_to = command.output('git', 'config', 'sendemail.to',
Simon Glass785f1542016-07-25 18:59:00 -0600481 raise_on_error=False)
Masahiro Yamadaee860c62014-07-18 14:23:20 +0900482 if not git_config_to:
Simon Glass5a1af1d2019-05-14 15:53:36 -0600483 print("No recipient.\n"
484 "Please add something like this to a commit\n"
485 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
486 "Or do something like this\n"
487 "git config sendemail.to u-boot@lists.denx.de")
Masahiro Yamadaee860c62014-07-18 14:23:20 +0900488 return
Simon Glass0157b182022-01-29 14:14:11 -0700489 cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))),
Simon Glass0fb560d2021-01-23 08:56:15 -0700490 '--cc', alias, warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000491 if self_only:
Simon Glass0157b182022-01-29 14:14:11 -0700492 to = build_email_list([os.getenv('USER')], '--to', alias, warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000493 cc = []
494 cmd = ['git', 'send-email', '--annotate']
Simon Glassa60aedf2018-06-19 09:56:07 -0600495 if smtp_server:
496 cmd.append('--smtp-server=%s' % smtp_server)
Doug Anderson6d819922013-03-17 10:31:04 +0000497 if in_reply_to:
Simon Glassfc0056e2020-11-08 20:36:18 -0700498 cmd.append('--in-reply-to="%s"' % in_reply_to)
Mateusz Kulikowski27067a42016-01-14 20:37:41 +0100499 if thread:
500 cmd.append('--thread')
Doug Anderson6d819922013-03-17 10:31:04 +0000501
Simon Glass0d24de92012-01-14 15:12:45 +0000502 cmd += to
503 cmd += cc
Simon Glass46007672020-11-03 13:54:10 -0700504 cmd += ['--cc-cmd', '"%s send --cc-cmd %s"' % (sys.argv[0], cc_fname)]
Simon Glass0d24de92012-01-14 15:12:45 +0000505 if cover_fname:
506 cmd.append(cover_fname)
507 cmd += args
Simon Glass2df3a012017-05-29 15:31:25 -0600508 cmdstr = ' '.join(cmd)
Simon Glass0d24de92012-01-14 15:12:45 +0000509 if not dry_run:
Simon Glass2df3a012017-05-29 15:31:25 -0600510 os.system(cmdstr)
511 return cmdstr
Simon Glass0d24de92012-01-14 15:12:45 +0000512
513
Simon Glass0157b182022-01-29 14:14:11 -0700514def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
Simon Glass0d24de92012-01-14 15:12:45 +0000515 """If an email address is an alias, look it up and return the full name
516
517 TODO: Why not just use git's own alias feature?
518
519 Args:
520 lookup_name: Alias or email address to look up
Simon Glassa1318f72013-03-26 13:09:42 +0000521 alias: Dictionary containing aliases (None to use settings default)
Simon Glass0fb560d2021-01-23 08:56:15 -0700522 warn_on_error: True to print a warning when an alias fails to match,
523 False to ignore it.
Simon Glass0d24de92012-01-14 15:12:45 +0000524
525 Returns:
526 tuple:
527 list containing a list of email addresses
528
529 Raises:
530 OSError if a recursive alias reference was found
531 ValueError if an alias was not found
532
533 >>> alias = {}
534 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
535 >>> alias['john'] = ['j.bloggs@napier.co.nz']
536 >>> alias['mary'] = ['m.poppins@cloud.net']
537 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
538 >>> alias['all'] = ['fred ', 'john', ' mary ']
539 >>> alias['loop'] = ['other', 'john', ' mary ']
540 >>> alias['other'] = ['loop', 'john', ' mary ']
Simon Glass0157b182022-01-29 14:14:11 -0700541 >>> lookup_email('mary', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000542 ['m.poppins@cloud.net']
Simon Glass0157b182022-01-29 14:14:11 -0700543 >>> lookup_email('arthur.wellesley@howe.ro.uk', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000544 ['arthur.wellesley@howe.ro.uk']
Simon Glass0157b182022-01-29 14:14:11 -0700545 >>> lookup_email('boys', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000546 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
Simon Glass0157b182022-01-29 14:14:11 -0700547 >>> lookup_email('all', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000548 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass0157b182022-01-29 14:14:11 -0700549 >>> lookup_email('odd', alias)
Simon Glass0fb560d2021-01-23 08:56:15 -0700550 Alias 'odd' not found
551 []
Simon Glass0157b182022-01-29 14:14:11 -0700552 >>> lookup_email('loop', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000553 Traceback (most recent call last):
554 ...
555 OSError: Recursive email alias at 'other'
Simon Glass0157b182022-01-29 14:14:11 -0700556 >>> lookup_email('odd', alias, warn_on_error=False)
Simon Glassa1318f72013-03-26 13:09:42 +0000557 []
558 >>> # In this case the loop part will effectively be ignored.
Simon Glass0157b182022-01-29 14:14:11 -0700559 >>> lookup_email('loop', alias, warn_on_error=False)
Simon Glasse752edc2014-08-28 09:43:35 -0600560 Recursive email alias at 'other'
561 Recursive email alias at 'john'
562 Recursive email alias at 'mary'
Simon Glassa1318f72013-03-26 13:09:42 +0000563 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass0d24de92012-01-14 15:12:45 +0000564 """
565 if not alias:
566 alias = settings.alias
567 lookup_name = lookup_name.strip()
568 if '@' in lookup_name: # Perhaps a real email address
569 return [lookup_name]
570
571 lookup_name = lookup_name.lower()
Simon Glassa1318f72013-03-26 13:09:42 +0000572 col = terminal.Color()
Simon Glass0d24de92012-01-14 15:12:45 +0000573
574 out_list = []
Simon Glassa1318f72013-03-26 13:09:42 +0000575 if level > 10:
576 msg = "Recursive email alias at '%s'" % lookup_name
Simon Glass0fb560d2021-01-23 08:56:15 -0700577 if warn_on_error:
Paul Burtonac3fde92016-09-27 16:03:51 +0100578 raise OSError(msg)
Simon Glassa1318f72013-03-26 13:09:42 +0000579 else:
Simon Glass252ac582022-01-29 14:14:17 -0700580 print(col.build(col.RED, msg))
Simon Glassa1318f72013-03-26 13:09:42 +0000581 return out_list
582
Simon Glass0d24de92012-01-14 15:12:45 +0000583 if lookup_name:
584 if not lookup_name in alias:
Simon Glassa1318f72013-03-26 13:09:42 +0000585 msg = "Alias '%s' not found" % lookup_name
Simon Glass0fb560d2021-01-23 08:56:15 -0700586 if warn_on_error:
Simon Glass252ac582022-01-29 14:14:17 -0700587 print(col.build(col.RED, msg))
Simon Glass0fb560d2021-01-23 08:56:15 -0700588 return out_list
Simon Glass0d24de92012-01-14 15:12:45 +0000589 for item in alias[lookup_name]:
Simon Glass0157b182022-01-29 14:14:11 -0700590 todo = lookup_email(item, alias, warn_on_error, level + 1)
Simon Glass0d24de92012-01-14 15:12:45 +0000591 for new_item in todo:
592 if not new_item in out_list:
593 out_list.append(new_item)
594
Simon Glass0d24de92012-01-14 15:12:45 +0000595 return out_list
596
Simon Glass0157b182022-01-29 14:14:11 -0700597def get_top_level():
Simon Glass0d24de92012-01-14 15:12:45 +0000598 """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__)) == \
Simon Glass0157b182022-01-29 14:14:11 -0700606 os.path.join(get_top_level(), 'tools', 'patman')
Simon Glass0d24de92012-01-14 15:12:45 +0000607 True
608 """
Simon Glassd9800692022-01-29 14:14:05 -0700609 return command.output_one_line('git', 'rev-parse', '--show-toplevel')
Simon Glass0d24de92012-01-14 15:12:45 +0000610
Simon Glass0157b182022-01-29 14:14:11 -0700611def get_alias_file():
Simon Glass0d24de92012-01-14 15:12:45 +0000612 """Gets the name of the git alias file.
613
614 Returns:
615 Filename of git alias file, or None if none
616 """
Simon Glassd9800692022-01-29 14:14:05 -0700617 fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile',
Simon Glassdc191502012-12-15 10:42:05 +0000618 raise_on_error=False)
Brian Norrisdca79262022-01-07 15:15:55 -0800619 if not fname:
620 return None
621
622 fname = os.path.expanduser(fname.strip())
623 if os.path.isabs(fname):
624 return fname
625
Simon Glass0157b182022-01-29 14:14:11 -0700626 return os.path.join(get_top_level(), fname)
Simon Glass0d24de92012-01-14 15:12:45 +0000627
Simon Glass0157b182022-01-29 14:14:11 -0700628def get_default_user_name():
Vikram Narayanan87d65552012-05-23 09:01:06 +0000629 """Gets the user.name from .gitconfig file.
630
631 Returns:
632 User name found in .gitconfig file, or None if none
633 """
Simon Glassd9800692022-01-29 14:14:05 -0700634 uname = command.output_one_line('git', 'config', '--global', 'user.name')
Vikram Narayanan87d65552012-05-23 09:01:06 +0000635 return uname
636
Simon Glass0157b182022-01-29 14:14:11 -0700637def get_default_user_email():
Vikram Narayanan87d65552012-05-23 09:01:06 +0000638 """Gets the user.email from the global .gitconfig file.
639
640 Returns:
641 User's email found in .gitconfig file, or None if none
642 """
Simon Glassd9800692022-01-29 14:14:05 -0700643 uemail = command.output_one_line('git', 'config', '--global', 'user.email')
Vikram Narayanan87d65552012-05-23 09:01:06 +0000644 return uemail
645
Simon Glass0157b182022-01-29 14:14:11 -0700646def get_default_subject_prefix():
Wu, Josh3871cd82015-04-15 10:25:18 +0800647 """Gets the format.subjectprefix from local .git/config file.
648
649 Returns:
650 Subject prefix found in local .git/config file, or None if none
651 """
Simon Glassd9800692022-01-29 14:14:05 -0700652 sub_prefix = command.output_one_line('git', 'config', 'format.subjectprefix',
Wu, Josh3871cd82015-04-15 10:25:18 +0800653 raise_on_error=False)
654
655 return sub_prefix
656
Simon Glass0157b182022-01-29 14:14:11 -0700657def setup():
Simon Glass0d24de92012-01-14 15:12:45 +0000658 """Set up git utils, by reading the alias files."""
Simon Glass0d24de92012-01-14 15:12:45 +0000659 # Check for a git alias file also
Simon Glass0b703db2014-08-28 09:43:45 -0600660 global use_no_decorate
661
Simon Glass0157b182022-01-29 14:14:11 -0700662 alias_fname = get_alias_file()
Simon Glass0d24de92012-01-14 15:12:45 +0000663 if alias_fname:
664 settings.ReadGitAliases(alias_fname)
Simon Glass0157b182022-01-29 14:14:11 -0700665 cmd = log_cmd(None, count=0)
Simon Glassd9800692022-01-29 14:14:05 -0700666 use_no_decorate = (command.run_pipe([cmd], raise_on_error=False)
Simon Glasse49f14a2014-08-09 15:33:11 -0600667 .return_code == 0)
Simon Glass0d24de92012-01-14 15:12:45 +0000668
Simon Glass0157b182022-01-29 14:14:11 -0700669def get_head():
Simon Glass5f6a1c42012-12-15 10:42:07 +0000670 """Get the hash of the current HEAD
671
672 Returns:
673 Hash of HEAD
674 """
Simon Glassd9800692022-01-29 14:14:05 -0700675 return command.output_one_line('git', 'show', '-s', '--pretty=format:%H')
Simon Glass5f6a1c42012-12-15 10:42:07 +0000676
Simon Glass0d24de92012-01-14 15:12:45 +0000677if __name__ == "__main__":
678 import doctest
679
680 doctest.testmod()