blob: 6d6a7eedecc67905bb621d7f5efc525a98d26a22 [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 os
Simon Glass26132882012-01-14 15:12:45 +00006import sys
Simon Glass26132882012-01-14 15:12:45 +00007
Simon Glassa997ea52020-04-17 18:09:04 -06008from patman import settings
Simon Glass131444f2023-02-23 18:18:04 -07009from u_boot_pylib import command
10from u_boot_pylib import terminal
Simon Glass11aba512012-12-15 10:42:07 +000011
Simon Glass761648b2022-01-29 14:14:11 -070012# True to use --no-decorate - we check this in setup()
Simon Glass6af913d2014-08-09 15:33:11 -060013use_no_decorate = True
14
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050015
Simon Glass761648b2022-01-29 14:14:11 -070016def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050017 count=None):
Simon Glassb9dbcb42014-08-09 15:33:10 -060018 """Create a command to perform a 'git log'
19
20 Args:
21 commit_range: Range expression to use for log, None for none
Anatolij Gustschinf2bcb322019-10-27 17:55:04 +010022 git_dir: Path to git repository (None to use default)
Simon Glassb9dbcb42014-08-09 15:33:10 -060023 oneline: True to use --oneline, else False
24 reverse: True to reverse the log (--reverse)
25 count: Number of commits to list, or None for no limit
26 Return:
27 List containing command and arguments to run
28 """
29 cmd = ['git']
30 if git_dir:
31 cmd += ['--git-dir', git_dir]
Simon Glass5f4e00d2014-08-28 09:43:37 -060032 cmd += ['--no-pager', 'log', '--no-color']
Simon Glassb9dbcb42014-08-09 15:33:10 -060033 if oneline:
34 cmd.append('--oneline')
Simon Glass6af913d2014-08-09 15:33:11 -060035 if use_no_decorate:
36 cmd.append('--no-decorate')
Simon Glass299b9092014-08-14 21:59:11 -060037 if reverse:
38 cmd.append('--reverse')
Simon Glassb9dbcb42014-08-09 15:33:10 -060039 if count is not None:
40 cmd.append('-n%d' % count)
41 if commit_range:
42 cmd.append(commit_range)
Simon Glass642e9a62016-03-12 18:50:31 -070043
44 # Add this in case we have a branch with the same name as a directory.
45 # This avoids messages like this, for example:
46 # fatal: ambiguous argument 'test': both revision and filename
47 cmd.append('--')
Simon Glassb9dbcb42014-08-09 15:33:10 -060048 return cmd
Simon Glass26132882012-01-14 15:12:45 +000049
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050050
Simon Glass761648b2022-01-29 14:14:11 -070051def count_commits_to_branch(branch):
Simon Glass26132882012-01-14 15:12:45 +000052 """Returns number of commits between HEAD and the tracking branch.
53
54 This looks back to the tracking branch and works out the number of commits
55 since then.
56
Simon Glass2eb4da72020-07-05 21:41:51 -060057 Args:
58 branch: Branch to count from (None for current branch)
59
Simon Glass26132882012-01-14 15:12:45 +000060 Return:
61 Number of patches that exist on top of the branch
62 """
Simon Glass2eb4da72020-07-05 21:41:51 -060063 if branch:
Simon Glass761648b2022-01-29 14:14:11 -070064 us, msg = get_upstream('.git', branch)
Simon Glass2eb4da72020-07-05 21:41:51 -060065 rev_range = '%s..%s' % (us, branch)
66 else:
67 rev_range = '@{upstream}..'
Simon Glass51f55182025-02-03 09:26:45 -070068 cmd = log_cmd(rev_range, oneline=True)
69 result = command.run_one(*cmd, capture=True, capture_stderr=True,
70 oneline=True, raise_on_error=False)
Simon Glass1c1f2072020-10-29 21:46:34 -060071 if result.return_code:
72 raise ValueError('Failed to determine upstream: %s' %
73 result.stderr.strip())
74 patch_count = len(result.stdout.splitlines())
Simon Glass26132882012-01-14 15:12:45 +000075 return patch_count
76
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050077
Simon Glass761648b2022-01-29 14:14:11 -070078def name_revision(commit_hash):
Simon Glassf204ab12014-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 """
Simon Glass51f55182025-02-03 09:26:45 -070087 stdout = command.output_one_line('git', 'name-rev', commit_hash)
Simon Glassf204ab12014-12-01 17:33:54 -070088
89 # We expect a commit, a space, then a revision name
90 name = stdout.split(' ')[1].strip()
91 return name
92
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050093
Simon Glass761648b2022-01-29 14:14:11 -070094def guess_upstream(git_dir, branch):
Simon Glassf204ab12014-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 Glass51f55182025-02-03 09:26:45 -0700110 cmd = log_cmd(branch, git_dir=git_dir, oneline=True, count=100)
111 result = command.run_one(*cmd, capture=True, capture_stderr=True,
112 raise_on_error=False)
Simon Glassf204ab12014-12-01 17:33:54 -0700113 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 Glass761648b2022-01-29 14:14:11 -0700117 name = name_revision(commit_hash)
Simon Glassf204ab12014-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
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500124
Simon Glass761648b2022-01-29 14:14:11 -0700125def get_upstream(git_dir, branch):
Simon Glass11aba512012-12-15 10:42:07 +0000126 """Returns the name of the upstream for a branch
127
128 Args:
129 git_dir: Git directory containing repo
130 branch: Name of branch
131
132 Returns:
Simon Glassf204ab12014-12-01 17:33:54 -0700133 Tuple:
134 Name of upstream branch (e.g. 'upstream/master') or None if none
135 Warning/error message, or None if none
Simon Glass11aba512012-12-15 10:42:07 +0000136 """
Simon Glassd2e95382013-05-08 08:06:08 +0000137 try:
Simon Glass840be732022-01-29 14:14:05 -0700138 remote = command.output_one_line('git', '--git-dir', git_dir, 'config',
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500139 'branch.%s.remote' % branch)
Simon Glass840be732022-01-29 14:14:05 -0700140 merge = command.output_one_line('git', '--git-dir', git_dir, 'config',
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500141 'branch.%s.merge' % branch)
Simon Glass82327692025-02-03 09:26:43 -0700142 except command.CommandExc:
Simon Glass761648b2022-01-29 14:14:11 -0700143 upstream, msg = guess_upstream(git_dir, branch)
Simon Glassf204ab12014-12-01 17:33:54 -0700144 return upstream, msg
Simon Glassd2e95382013-05-08 08:06:08 +0000145
Simon Glass11aba512012-12-15 10:42:07 +0000146 if remote == '.':
Simon Glass7e92f5c2015-01-29 11:35:16 -0700147 return merge, None
Simon Glass11aba512012-12-15 10:42:07 +0000148 elif remote and merge:
Simon Glass978c1fb2023-10-30 10:22:30 -0700149 # Drop the initial refs/heads from merge
150 leaf = merge.split('/', maxsplit=2)[2:]
151 return '%s/%s' % (remote, '/'.join(leaf)), None
Simon Glass11aba512012-12-15 10:42:07 +0000152 else:
Paul Burtonf14a1312016-09-27 16:03:51 +0100153 raise ValueError("Cannot determine upstream branch for branch "
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500154 "'%s' remote='%s', merge='%s'"
155 % (branch, remote, merge))
Simon Glass11aba512012-12-15 10:42:07 +0000156
157
Simon Glass761648b2022-01-29 14:14:11 -0700158def get_range_in_branch(git_dir, branch, include_upstream=False):
Simon Glass11aba512012-12-15 10:42:07 +0000159 """Returns an expression for the commits in the given branch.
160
161 Args:
162 git_dir: Directory containing git repo
163 branch: Name of branch
164 Return:
165 Expression in the form 'upstream..branch' which can be used to
Simon Glassd2e95382013-05-08 08:06:08 +0000166 access the commits. If the branch does not exist, returns None.
Simon Glass11aba512012-12-15 10:42:07 +0000167 """
Simon Glass761648b2022-01-29 14:14:11 -0700168 upstream, msg = get_upstream(git_dir, branch)
Simon Glassd2e95382013-05-08 08:06:08 +0000169 if not upstream:
Simon Glassf204ab12014-12-01 17:33:54 -0700170 return None, msg
171 rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
172 return rstr, msg
Simon Glass11aba512012-12-15 10:42:07 +0000173
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500174
Simon Glass761648b2022-01-29 14:14:11 -0700175def count_commits_in_range(git_dir, range_expr):
Simon Glass5eeef462014-12-01 17:33:57 -0700176 """Returns the number of commits in the given range.
177
178 Args:
179 git_dir: Directory containing git repo
180 range_expr: Range to check
181 Return:
Anatolij Gustschinf2bcb322019-10-27 17:55:04 +0100182 Number of patches that exist in the supplied range or None if none
Simon Glass5eeef462014-12-01 17:33:57 -0700183 were found
184 """
Simon Glass51f55182025-02-03 09:26:45 -0700185 cmd = log_cmd(range_expr, git_dir=git_dir, oneline=True)
186 result = command.run_one(*cmd, capture=True, capture_stderr=True,
187 raise_on_error=False)
Simon Glass5eeef462014-12-01 17:33:57 -0700188 if result.return_code:
189 return None, "Range '%s' not found or is invalid" % range_expr
190 patch_count = len(result.stdout.splitlines())
191 return patch_count, None
192
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500193
Simon Glass761648b2022-01-29 14:14:11 -0700194def count_commits_in_branch(git_dir, branch, include_upstream=False):
Simon Glass11aba512012-12-15 10:42:07 +0000195 """Returns the number of commits in the given branch.
196
197 Args:
198 git_dir: Directory containing git repo
199 branch: Name of branch
200 Return:
Simon Glassd2e95382013-05-08 08:06:08 +0000201 Number of patches that exist on top of the branch, or None if the
202 branch does not exist.
Simon Glass11aba512012-12-15 10:42:07 +0000203 """
Simon Glass761648b2022-01-29 14:14:11 -0700204 range_expr, msg = get_range_in_branch(git_dir, branch, include_upstream)
Simon Glassd2e95382013-05-08 08:06:08 +0000205 if not range_expr:
Simon Glassf204ab12014-12-01 17:33:54 -0700206 return None, msg
Simon Glass761648b2022-01-29 14:14:11 -0700207 return count_commits_in_range(git_dir, range_expr)
Simon Glass11aba512012-12-15 10:42:07 +0000208
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500209
Simon Glass761648b2022-01-29 14:14:11 -0700210def count_commits(commit_range):
Simon Glass11aba512012-12-15 10:42:07 +0000211 """Returns the number of commits in the given range.
212
213 Args:
214 commit_range: Range of commits to count (e.g. 'HEAD..base')
215 Return:
216 Number of patches that exist on top of the branch
217 """
Simon Glass761648b2022-01-29 14:14:11 -0700218 pipe = [log_cmd(commit_range, oneline=True),
Simon Glass11aba512012-12-15 10:42:07 +0000219 ['wc', '-l']]
Simon Glass840be732022-01-29 14:14:05 -0700220 stdout = command.run_pipe(pipe, capture=True, oneline=True).stdout
Simon Glass11aba512012-12-15 10:42:07 +0000221 patch_count = int(stdout)
222 return patch_count
223
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500224
Simon Glass761648b2022-01-29 14:14:11 -0700225def checkout(commit_hash, git_dir=None, work_tree=None, force=False):
Simon Glass11aba512012-12-15 10:42:07 +0000226 """Checkout the selected commit for this build
227
228 Args:
229 commit_hash: Commit hash to check out
230 """
231 pipe = ['git']
232 if git_dir:
233 pipe.extend(['--git-dir', git_dir])
234 if work_tree:
235 pipe.extend(['--work-tree', work_tree])
236 pipe.append('checkout')
237 if force:
238 pipe.append('-f')
239 pipe.append(commit_hash)
Simon Glass840be732022-01-29 14:14:05 -0700240 result = command.run_pipe([pipe], capture=True, raise_on_error=False,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500241 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000242 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100243 raise OSError('git checkout (%s): %s' % (pipe, result.stderr))
Simon Glass11aba512012-12-15 10:42:07 +0000244
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500245
Simon Glass761648b2022-01-29 14:14:11 -0700246def clone(git_dir, output_dir):
Simon Glass11aba512012-12-15 10:42:07 +0000247 """Checkout the selected commit for this build
248
249 Args:
250 commit_hash: Commit hash to check out
251 """
Simon Glass51f55182025-02-03 09:26:45 -0700252 result = command.run_one('git', 'clone', git_dir, '.', capture=True,
253 cwd=output_dir, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000254 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100255 raise OSError('git clone: %s' % result.stderr)
Simon Glass11aba512012-12-15 10:42:07 +0000256
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500257
Simon Glass761648b2022-01-29 14:14:11 -0700258def fetch(git_dir=None, work_tree=None):
Simon Glass11aba512012-12-15 10:42:07 +0000259 """Fetch from the origin repo
260
261 Args:
262 commit_hash: Commit hash to check out
263 """
Simon Glass51f55182025-02-03 09:26:45 -0700264 cmd = ['git']
Simon Glass11aba512012-12-15 10:42:07 +0000265 if git_dir:
Simon Glass51f55182025-02-03 09:26:45 -0700266 cmd.extend(['--git-dir', git_dir])
Simon Glass11aba512012-12-15 10:42:07 +0000267 if work_tree:
Simon Glass51f55182025-02-03 09:26:45 -0700268 cmd.extend(['--work-tree', work_tree])
269 cmd.append('fetch')
270 result = command.run_one(*cmd, capture=True, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000271 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100272 raise OSError('git fetch: %s' % result.stderr)
Simon Glass11aba512012-12-15 10:42:07 +0000273
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500274
Simon Glass761648b2022-01-29 14:14:11 -0700275def check_worktree_is_available(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300276 """Check if git-worktree functionality is available
277
278 Args:
279 git_dir: The repository to test in
280
281 Returns:
282 True if git-worktree commands will work, False otherwise.
283 """
Simon Glass51f55182025-02-03 09:26:45 -0700284 result = command.run_one('git', '--git-dir', git_dir, 'worktree', 'list',
285 capture=True, capture_stderr=True,
286 raise_on_error=False)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300287 return result.return_code == 0
288
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500289
Simon Glass761648b2022-01-29 14:14:11 -0700290def add_worktree(git_dir, output_dir, commit_hash=None):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300291 """Create and checkout a new git worktree for this build
292
293 Args:
294 git_dir: The repository to checkout the worktree from
295 output_dir: Path for the new worktree
296 commit_hash: Commit hash to checkout
297 """
298 # We need to pass --detach to avoid creating a new branch
Simon Glass51f55182025-02-03 09:26:45 -0700299 cmd = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach']
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300300 if commit_hash:
Simon Glass51f55182025-02-03 09:26:45 -0700301 cmd.append(commit_hash)
302 result = command.run_one(*cmd, capture=True, cwd=output_dir,
303 capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300304 if result.return_code != 0:
305 raise OSError('git worktree add: %s' % result.stderr)
306
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500307
Simon Glass761648b2022-01-29 14:14:11 -0700308def prune_worktrees(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300309 """Remove administrative files for deleted worktrees
310
311 Args:
312 git_dir: The repository whose deleted worktrees should be pruned
313 """
Simon Glass51f55182025-02-03 09:26:45 -0700314 result = command.run_one('git', '--git-dir', git_dir, 'worktree', 'prune',
315 capture=True, capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300316 if result.return_code != 0:
317 raise OSError('git worktree prune: %s' % result.stderr)
318
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500319
320def create_patches(branch, start, count, ignore_binary, series, signoff=True):
Simon Glass26132882012-01-14 15:12:45 +0000321 """Create a series of patches from the top of the current branch.
322
323 The patch files are written to the current directory using
324 git format-patch.
325
326 Args:
Simon Glass2eb4da72020-07-05 21:41:51 -0600327 branch: Branch to create patches from (None for current branch)
Simon Glass26132882012-01-14 15:12:45 +0000328 start: Commit to start from: 0=HEAD, 1=next one, etc.
329 count: number of commits to include
Simon Glass24725af2020-07-05 21:41:49 -0600330 ignore_binary: Don't generate patches for binary files
331 series: Series object for this series (set of patches)
Simon Glass26132882012-01-14 15:12:45 +0000332 Return:
Simon Glass24725af2020-07-05 21:41:49 -0600333 Filename of cover letter (None if none)
Simon Glass26132882012-01-14 15:12:45 +0000334 List of filenames of patch files
335 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500336 cmd = ['git', 'format-patch', '-M']
Philipp Tomsich858531a2020-11-24 18:14:52 +0100337 if signoff:
338 cmd.append('--signoff')
Bin Menga04f1212020-05-04 00:52:44 -0700339 if ignore_binary:
340 cmd.append('--no-binary')
Simon Glass26132882012-01-14 15:12:45 +0000341 if series.get('cover'):
342 cmd.append('--cover-letter')
343 prefix = series.GetPatchPrefix()
344 if prefix:
345 cmd += ['--subject-prefix=%s' % prefix]
Simon Glass2eb4da72020-07-05 21:41:51 -0600346 brname = branch or 'HEAD'
347 cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
Simon Glass26132882012-01-14 15:12:45 +0000348
Simon Glass840be732022-01-29 14:14:05 -0700349 stdout = command.run_list(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000350 files = stdout.splitlines()
351
352 # We have an extra file if there is a cover letter
353 if series.get('cover'):
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500354 return files[0], files[1:]
Simon Glass26132882012-01-14 15:12:45 +0000355 else:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500356 return None, files
357
Simon Glass26132882012-01-14 15:12:45 +0000358
Simon Glass761648b2022-01-29 14:14:11 -0700359def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
Simon Glass26132882012-01-14 15:12:45 +0000360 """Build a list of email addresses based on an input list.
361
362 Takes a list of email addresses and aliases, and turns this into a list
363 of only email address, by resolving any aliases that are present.
364
365 If the tag is given, then each email address is prepended with this
366 tag and a space. If the tag starts with a minus sign (indicating a
367 command line parameter) then the email address is quoted.
368
369 Args:
370 in_list: List of aliases/email addresses
371 tag: Text to put before each address
Simon Glass12ea5f42013-03-26 13:09:42 +0000372 alias: Alias dictionary
Simon Glass1f975b92021-01-23 08:56:15 -0700373 warn_on_error: True to raise an error when an alias fails to match,
Simon Glass12ea5f42013-03-26 13:09:42 +0000374 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000375
376 Returns:
377 List of email addresses
378
379 >>> alias = {}
380 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
381 >>> alias['john'] = ['j.bloggs@napier.co.nz']
382 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
383 >>> alias['boys'] = ['fred', ' john']
384 >>> alias['all'] = ['fred ', 'john', ' mary ']
Simon Glass761648b2022-01-29 14:14:11 -0700385 >>> build_email_list(['john', 'mary'], None, alias)
Simon Glass26132882012-01-14 15:12:45 +0000386 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
Simon Glass761648b2022-01-29 14:14:11 -0700387 >>> build_email_list(['john', 'mary'], '--to', alias)
Simon Glass26132882012-01-14 15:12:45 +0000388 ['--to "j.bloggs@napier.co.nz"', \
389'--to "Mary Poppins <m.poppins@cloud.net>"']
Simon Glass761648b2022-01-29 14:14:11 -0700390 >>> build_email_list(['john', 'mary'], 'Cc', alias)
Simon Glass26132882012-01-14 15:12:45 +0000391 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
392 """
393 quote = '"' if tag and tag[0] == '-' else ''
394 raw = []
395 for item in in_list:
Simon Glass761648b2022-01-29 14:14:11 -0700396 raw += lookup_email(item, alias, warn_on_error=warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000397 result = []
398 for item in raw:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500399 if item not in result:
Simon Glass26132882012-01-14 15:12:45 +0000400 result.append(item)
401 if tag:
402 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
403 return result
404
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500405
Simon Glass761648b2022-01-29 14:14:11 -0700406def check_suppress_cc_config():
Nicolas Boichat0da95742020-07-13 10:50:00 +0800407 """Check if sendemail.suppresscc is configured correctly.
408
409 Returns:
410 True if the option is configured correctly, False otherwise.
411 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500412 suppresscc = command.output_one_line(
413 'git', 'config', 'sendemail.suppresscc', raise_on_error=False)
Nicolas Boichat0da95742020-07-13 10:50:00 +0800414
415 # Other settings should be fine.
416 if suppresscc == 'all' or suppresscc == 'cccmd':
417 col = terminal.Color()
418
Simon Glassf45d3742022-01-29 14:14:17 -0700419 print((col.build(col.RED, "error") +
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500420 ": git config sendemail.suppresscc set to %s\n"
421 % (suppresscc)) +
422 " patman needs --cc-cmd to be run to set the cc list.\n" +
423 " Please run:\n" +
424 " git config --unset sendemail.suppresscc\n" +
425 " Or read the man page:\n" +
426 " git send-email --help\n" +
427 " and set an option that runs --cc-cmd\n")
Nicolas Boichat0da95742020-07-13 10:50:00 +0800428 return False
429
430 return True
431
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500432
Simon Glass761648b2022-01-29 14:14:11 -0700433def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500434 self_only=False, alias=None, in_reply_to=None, thread=False,
Maxim Cournoyer3ef23e92022-12-20 00:28:46 -0500435 smtp_server=None, get_maintainer_script=None):
Simon Glass26132882012-01-14 15:12:45 +0000436 """Email a patch series.
437
438 Args:
439 series: Series object containing destination info
440 cover_fname: filename of cover letter
441 args: list of filenames of patch files
442 dry_run: Just return the command that would be run
Simon Glass1f975b92021-01-23 08:56:15 -0700443 warn_on_error: True to print a warning when an alias fails to match,
444 False to ignore it.
Simon Glass26132882012-01-14 15:12:45 +0000445 cc_fname: Filename of Cc file for per-commit Cc
446 self_only: True to just email to yourself as a test
Doug Anderson06f27ac2013-03-17 10:31:04 +0000447 in_reply_to: If set we'll pass this to git as --in-reply-to.
448 Should be a message ID that this is in reply to.
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100449 thread: True to add --thread to git send-email (make
450 all patches reply to cover-letter or first patch in series)
Simon Glass8137e302018-06-19 09:56:07 -0600451 smtp_server: SMTP server to use to send patches
Maxim Cournoyer3ef23e92022-12-20 00:28:46 -0500452 get_maintainer_script: File name of script to get maintainers emails
Simon Glass26132882012-01-14 15:12:45 +0000453
454 Returns:
455 Git command that was/would be run
456
Doug Anderson51d73212012-11-26 15:21:40 +0000457 # For the duration of this doctest pretend that we ran patman with ./patman
458 >>> _old_argv0 = sys.argv[0]
459 >>> sys.argv[0] = './patman'
460
Simon Glass26132882012-01-14 15:12:45 +0000461 >>> alias = {}
462 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
463 >>> alias['john'] = ['j.bloggs@napier.co.nz']
464 >>> alias['mary'] = ['m.poppins@cloud.net']
465 >>> alias['boys'] = ['fred', ' john']
466 >>> alias['all'] = ['fred ', 'john', ' mary ']
467 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
Simon Glass794c2592020-06-07 06:45:47 -0600468 >>> series = {}
469 >>> series['to'] = ['fred']
470 >>> series['cc'] = ['mary']
Simon Glass761648b2022-01-29 14:14:11 -0700471 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000472 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000473 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700474"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700475 >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \
Simon Glass12ea5f42013-03-26 13:09:42 +0000476 alias)
Simon Glass26132882012-01-14 15:12:45 +0000477 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700478"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
Simon Glass794c2592020-06-07 06:45:47 -0600479 >>> series['cc'] = ['all']
Simon Glass761648b2022-01-29 14:14:11 -0700480 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000481 True, alias)
Simon Glass26132882012-01-14 15:12:45 +0000482 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
Simon Glass1ee91c12020-11-03 13:54:10 -0700483send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700484 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000485 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000486 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
487"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700488"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Doug Anderson51d73212012-11-26 15:21:40 +0000489
490 # Restore argv[0] since we clobbered it.
491 >>> sys.argv[0] = _old_argv0
Simon Glass26132882012-01-14 15:12:45 +0000492 """
Simon Glass761648b2022-01-29 14:14:11 -0700493 to = build_email_list(series.get('to'), '--to', alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000494 if not to:
Simon Glass840be732022-01-29 14:14:05 -0700495 git_config_to = command.output('git', 'config', 'sendemail.to',
Simon Glassc55e0562016-07-25 18:59:00 -0600496 raise_on_error=False)
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900497 if not git_config_to:
Simon Glass23b8a192019-05-14 15:53:36 -0600498 print("No recipient.\n"
499 "Please add something like this to a commit\n"
500 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
501 "Or do something like this\n"
502 "git config sendemail.to u-boot@lists.denx.de")
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900503 return
Simon Glass761648b2022-01-29 14:14:11 -0700504 cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))),
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500505 '--cc', alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000506 if self_only:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500507 to = build_email_list([os.getenv('USER')], '--to',
508 alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000509 cc = []
510 cmd = ['git', 'send-email', '--annotate']
Simon Glass8137e302018-06-19 09:56:07 -0600511 if smtp_server:
512 cmd.append('--smtp-server=%s' % smtp_server)
Doug Anderson06f27ac2013-03-17 10:31:04 +0000513 if in_reply_to:
Simon Glass9dfb3112020-11-08 20:36:18 -0700514 cmd.append('--in-reply-to="%s"' % in_reply_to)
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100515 if thread:
516 cmd.append('--thread')
Doug Anderson06f27ac2013-03-17 10:31:04 +0000517
Simon Glass26132882012-01-14 15:12:45 +0000518 cmd += to
519 cmd += cc
Simon Glass1ee91c12020-11-03 13:54:10 -0700520 cmd += ['--cc-cmd', '"%s send --cc-cmd %s"' % (sys.argv[0], cc_fname)]
Simon Glass26132882012-01-14 15:12:45 +0000521 if cover_fname:
522 cmd.append(cover_fname)
523 cmd += args
Simon Glass47e308e2017-05-29 15:31:25 -0600524 cmdstr = ' '.join(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000525 if not dry_run:
Simon Glass47e308e2017-05-29 15:31:25 -0600526 os.system(cmdstr)
527 return cmdstr
Simon Glass26132882012-01-14 15:12:45 +0000528
529
Simon Glass761648b2022-01-29 14:14:11 -0700530def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
Simon Glass26132882012-01-14 15:12:45 +0000531 """If an email address is an alias, look it up and return the full name
532
533 TODO: Why not just use git's own alias feature?
534
535 Args:
536 lookup_name: Alias or email address to look up
Simon Glass12ea5f42013-03-26 13:09:42 +0000537 alias: Dictionary containing aliases (None to use settings default)
Simon Glass1f975b92021-01-23 08:56:15 -0700538 warn_on_error: True to print a warning when an alias fails to match,
539 False to ignore it.
Simon Glass26132882012-01-14 15:12:45 +0000540
541 Returns:
542 tuple:
543 list containing a list of email addresses
544
545 Raises:
546 OSError if a recursive alias reference was found
547 ValueError if an alias was not found
548
549 >>> alias = {}
550 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
551 >>> alias['john'] = ['j.bloggs@napier.co.nz']
552 >>> alias['mary'] = ['m.poppins@cloud.net']
553 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
554 >>> alias['all'] = ['fred ', 'john', ' mary ']
555 >>> alias['loop'] = ['other', 'john', ' mary ']
556 >>> alias['other'] = ['loop', 'john', ' mary ']
Simon Glass761648b2022-01-29 14:14:11 -0700557 >>> lookup_email('mary', alias)
Simon Glass26132882012-01-14 15:12:45 +0000558 ['m.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700559 >>> lookup_email('arthur.wellesley@howe.ro.uk', alias)
Simon Glass26132882012-01-14 15:12:45 +0000560 ['arthur.wellesley@howe.ro.uk']
Simon Glass761648b2022-01-29 14:14:11 -0700561 >>> lookup_email('boys', alias)
Simon Glass26132882012-01-14 15:12:45 +0000562 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
Simon Glass761648b2022-01-29 14:14:11 -0700563 >>> lookup_email('all', alias)
Simon Glass26132882012-01-14 15:12:45 +0000564 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700565 >>> lookup_email('odd', alias)
Simon Glass1f975b92021-01-23 08:56:15 -0700566 Alias 'odd' not found
567 []
Simon Glass761648b2022-01-29 14:14:11 -0700568 >>> lookup_email('loop', alias)
Simon Glass26132882012-01-14 15:12:45 +0000569 Traceback (most recent call last):
570 ...
571 OSError: Recursive email alias at 'other'
Simon Glass761648b2022-01-29 14:14:11 -0700572 >>> lookup_email('odd', alias, warn_on_error=False)
Simon Glass12ea5f42013-03-26 13:09:42 +0000573 []
574 >>> # In this case the loop part will effectively be ignored.
Simon Glass761648b2022-01-29 14:14:11 -0700575 >>> lookup_email('loop', alias, warn_on_error=False)
Simon Glassb0cd3412014-08-28 09:43:35 -0600576 Recursive email alias at 'other'
577 Recursive email alias at 'john'
578 Recursive email alias at 'mary'
Simon Glass12ea5f42013-03-26 13:09:42 +0000579 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass26132882012-01-14 15:12:45 +0000580 """
581 if not alias:
582 alias = settings.alias
583 lookup_name = lookup_name.strip()
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500584 if '@' in lookup_name: # Perhaps a real email address
Simon Glass26132882012-01-14 15:12:45 +0000585 return [lookup_name]
586
587 lookup_name = lookup_name.lower()
Simon Glass12ea5f42013-03-26 13:09:42 +0000588 col = terminal.Color()
Simon Glass26132882012-01-14 15:12:45 +0000589
Simon Glass12ea5f42013-03-26 13:09:42 +0000590 out_list = []
Simon Glass26132882012-01-14 15:12:45 +0000591 if level > 10:
Simon Glass12ea5f42013-03-26 13:09:42 +0000592 msg = "Recursive email alias at '%s'" % lookup_name
Simon Glass1f975b92021-01-23 08:56:15 -0700593 if warn_on_error:
Paul Burtonf14a1312016-09-27 16:03:51 +0100594 raise OSError(msg)
Simon Glass12ea5f42013-03-26 13:09:42 +0000595 else:
Simon Glassf45d3742022-01-29 14:14:17 -0700596 print(col.build(col.RED, msg))
Simon Glass12ea5f42013-03-26 13:09:42 +0000597 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000598
Simon Glass26132882012-01-14 15:12:45 +0000599 if lookup_name:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500600 if lookup_name not in alias:
Simon Glass12ea5f42013-03-26 13:09:42 +0000601 msg = "Alias '%s' not found" % lookup_name
Simon Glass1f975b92021-01-23 08:56:15 -0700602 if warn_on_error:
Simon Glassf45d3742022-01-29 14:14:17 -0700603 print(col.build(col.RED, msg))
Simon Glass1f975b92021-01-23 08:56:15 -0700604 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000605 for item in alias[lookup_name]:
Simon Glass761648b2022-01-29 14:14:11 -0700606 todo = lookup_email(item, alias, warn_on_error, level + 1)
Simon Glass26132882012-01-14 15:12:45 +0000607 for new_item in todo:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500608 if new_item not in out_list:
Simon Glass26132882012-01-14 15:12:45 +0000609 out_list.append(new_item)
610
Simon Glass26132882012-01-14 15:12:45 +0000611 return out_list
612
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500613
Simon Glass761648b2022-01-29 14:14:11 -0700614def get_top_level():
Simon Glass26132882012-01-14 15:12:45 +0000615 """Return name of top-level directory for this git repo.
616
617 Returns:
618 Full path to git top-level directory
619
620 This test makes sure that we are running tests in the right subdir
621
Doug Anderson51d73212012-11-26 15:21:40 +0000622 >>> os.path.realpath(os.path.dirname(__file__)) == \
Simon Glass761648b2022-01-29 14:14:11 -0700623 os.path.join(get_top_level(), 'tools', 'patman')
Simon Glass26132882012-01-14 15:12:45 +0000624 True
625 """
Simon Glass840be732022-01-29 14:14:05 -0700626 return command.output_one_line('git', 'rev-parse', '--show-toplevel')
Simon Glass26132882012-01-14 15:12:45 +0000627
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500628
Simon Glass761648b2022-01-29 14:14:11 -0700629def get_alias_file():
Simon Glass26132882012-01-14 15:12:45 +0000630 """Gets the name of the git alias file.
631
632 Returns:
633 Filename of git alias file, or None if none
634 """
Simon Glass840be732022-01-29 14:14:05 -0700635 fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile',
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500636 raise_on_error=False)
Brian Norris94c775d2022-01-07 15:15:55 -0800637 if not fname:
638 return None
639
640 fname = os.path.expanduser(fname.strip())
641 if os.path.isabs(fname):
642 return fname
643
Simon Glass761648b2022-01-29 14:14:11 -0700644 return os.path.join(get_top_level(), fname)
Simon Glass26132882012-01-14 15:12:45 +0000645
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500646
Simon Glass761648b2022-01-29 14:14:11 -0700647def get_default_user_name():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000648 """Gets the user.name from .gitconfig file.
649
650 Returns:
651 User name found in .gitconfig file, or None if none
652 """
Fei Shao78712232023-09-08 22:15:59 +0800653 uname = command.output_one_line('git', 'config', '--global', '--includes', 'user.name')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000654 return uname
655
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500656
Simon Glass761648b2022-01-29 14:14:11 -0700657def get_default_user_email():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000658 """Gets the user.email from the global .gitconfig file.
659
660 Returns:
661 User's email found in .gitconfig file, or None if none
662 """
Fei Shao78712232023-09-08 22:15:59 +0800663 uemail = command.output_one_line('git', 'config', '--global', '--includes', 'user.email')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000664 return uemail
665
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500666
Simon Glass761648b2022-01-29 14:14:11 -0700667def get_default_subject_prefix():
Wu, Josh9873b912015-04-15 10:25:18 +0800668 """Gets the format.subjectprefix from local .git/config file.
669
670 Returns:
671 Subject prefix found in local .git/config file, or None if none
672 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500673 sub_prefix = command.output_one_line(
674 'git', 'config', 'format.subjectprefix', raise_on_error=False)
Wu, Josh9873b912015-04-15 10:25:18 +0800675
676 return sub_prefix
677
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500678
Simon Glass761648b2022-01-29 14:14:11 -0700679def setup():
Simon Glass26132882012-01-14 15:12:45 +0000680 """Set up git utils, by reading the alias files."""
Simon Glass26132882012-01-14 15:12:45 +0000681 # Check for a git alias file also
Simon Glass81bcca82014-08-28 09:43:45 -0600682 global use_no_decorate
683
Simon Glass761648b2022-01-29 14:14:11 -0700684 alias_fname = get_alias_file()
Simon Glass26132882012-01-14 15:12:45 +0000685 if alias_fname:
686 settings.ReadGitAliases(alias_fname)
Simon Glass761648b2022-01-29 14:14:11 -0700687 cmd = log_cmd(None, count=0)
Simon Glass51f55182025-02-03 09:26:45 -0700688 use_no_decorate = (command.run_one(*cmd, raise_on_error=False)
Simon Glass6af913d2014-08-09 15:33:11 -0600689 .return_code == 0)
Simon Glass26132882012-01-14 15:12:45 +0000690
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500691
Simon Glass761648b2022-01-29 14:14:11 -0700692def get_head():
Simon Glass11aba512012-12-15 10:42:07 +0000693 """Get the hash of the current HEAD
694
695 Returns:
696 Hash of HEAD
697 """
Simon Glass840be732022-01-29 14:14:05 -0700698 return command.output_one_line('git', 'show', '-s', '--pretty=format:%H')
Simon Glass11aba512012-12-15 10:42:07 +0000699
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500700
Simon Glass26132882012-01-14 15:12:45 +0000701if __name__ == "__main__":
702 import doctest
703
704 doctest.testmod()