blob: b0a12f2e8c0648d8dd3488a5556f007856197b24 [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 Glass761648b2022-01-29 14:14:11 -070068 pipe = [log_cmd(rev_range, oneline=True)]
Simon Glass840be732022-01-29 14:14:05 -070069 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050070 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 """
87 pipe = ['git', 'name-rev', commit_hash]
Simon Glass840be732022-01-29 14:14:05 -070088 stdout = command.run_pipe([pipe], capture=True, oneline=True).stdout
Simon Glassf204ab12014-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
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050094
Simon Glass761648b2022-01-29 14:14:11 -070095def guess_upstream(git_dir, branch):
Simon Glassf204ab12014-12-01 17:33:54 -070096 """Tries to guess the upstream for a branch
97
98 This lists out top commits on a branch and tries to find a suitable
99 upstream. It does this by looking for the first commit where
100 'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
101
102 Args:
103 git_dir: Git directory containing repo
104 branch: Name of branch
105
106 Returns:
107 Tuple:
108 Name of upstream branch (e.g. 'upstream/master') or None if none
109 Warning/error message, or None if none
110 """
Simon Glass761648b2022-01-29 14:14:11 -0700111 pipe = [log_cmd(branch, git_dir=git_dir, oneline=True, count=100)]
Simon Glass840be732022-01-29 14:14:05 -0700112 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500113 raise_on_error=False)
Simon Glassf204ab12014-12-01 17:33:54 -0700114 if result.return_code:
115 return None, "Branch '%s' not found" % branch
116 for line in result.stdout.splitlines()[1:]:
117 commit_hash = line.split(' ')[0]
Simon Glass761648b2022-01-29 14:14:11 -0700118 name = name_revision(commit_hash)
Simon Glassf204ab12014-12-01 17:33:54 -0700119 if '~' not in name and '^' not in name:
120 if name.startswith('remotes/'):
121 name = name[8:]
122 return name, "Guessing upstream as '%s'" % name
123 return None, "Cannot find a suitable upstream for branch '%s'" % branch
124
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500125
Simon Glass761648b2022-01-29 14:14:11 -0700126def get_upstream(git_dir, branch):
Simon Glass11aba512012-12-15 10:42:07 +0000127 """Returns the name of the upstream for a branch
128
129 Args:
130 git_dir: Git directory containing repo
131 branch: Name of branch
132
133 Returns:
Simon Glassf204ab12014-12-01 17:33:54 -0700134 Tuple:
135 Name of upstream branch (e.g. 'upstream/master') or None if none
136 Warning/error message, or None if none
Simon Glass11aba512012-12-15 10:42:07 +0000137 """
Simon Glassd2e95382013-05-08 08:06:08 +0000138 try:
Simon Glass840be732022-01-29 14:14:05 -0700139 remote = command.output_one_line('git', '--git-dir', git_dir, 'config',
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500140 'branch.%s.remote' % branch)
Simon Glass840be732022-01-29 14:14:05 -0700141 merge = command.output_one_line('git', '--git-dir', git_dir, 'config',
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500142 'branch.%s.merge' % branch)
143 except Exception:
Simon Glass761648b2022-01-29 14:14:11 -0700144 upstream, msg = guess_upstream(git_dir, branch)
Simon Glassf204ab12014-12-01 17:33:54 -0700145 return upstream, msg
Simon Glassd2e95382013-05-08 08:06:08 +0000146
Simon Glass11aba512012-12-15 10:42:07 +0000147 if remote == '.':
Simon Glass7e92f5c2015-01-29 11:35:16 -0700148 return merge, None
Simon Glass11aba512012-12-15 10:42:07 +0000149 elif remote and merge:
150 leaf = merge.split('/')[-1]
Simon Glassf204ab12014-12-01 17:33:54 -0700151 return '%s/%s' % (remote, 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 Glass761648b2022-01-29 14:14:11 -0700185 pipe = [log_cmd(range_expr, git_dir=git_dir, oneline=True)]
Simon Glass840be732022-01-29 14:14:05 -0700186 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500187 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 """
252 pipe = ['git', 'clone', git_dir, '.']
Simon Glass840be732022-01-29 14:14:05 -0700253 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500254 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000255 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100256 raise OSError('git clone: %s' % result.stderr)
Simon Glass11aba512012-12-15 10:42:07 +0000257
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500258
Simon Glass761648b2022-01-29 14:14:11 -0700259def fetch(git_dir=None, work_tree=None):
Simon Glass11aba512012-12-15 10:42:07 +0000260 """Fetch from the origin repo
261
262 Args:
263 commit_hash: Commit hash to check out
264 """
265 pipe = ['git']
266 if git_dir:
267 pipe.extend(['--git-dir', git_dir])
268 if work_tree:
269 pipe.extend(['--work-tree', work_tree])
270 pipe.append('fetch')
Simon Glass840be732022-01-29 14:14:05 -0700271 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000272 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100273 raise OSError('git fetch: %s' % result.stderr)
Simon Glass11aba512012-12-15 10:42:07 +0000274
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500275
Simon Glass761648b2022-01-29 14:14:11 -0700276def check_worktree_is_available(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300277 """Check if git-worktree functionality is available
278
279 Args:
280 git_dir: The repository to test in
281
282 Returns:
283 True if git-worktree commands will work, False otherwise.
284 """
285 pipe = ['git', '--git-dir', git_dir, 'worktree', 'list']
Simon Glass840be732022-01-29 14:14:05 -0700286 result = command.run_pipe([pipe], capture=True, capture_stderr=True,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500287 raise_on_error=False)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300288 return result.return_code == 0
289
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500290
Simon Glass761648b2022-01-29 14:14:11 -0700291def add_worktree(git_dir, output_dir, commit_hash=None):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300292 """Create and checkout a new git worktree for this build
293
294 Args:
295 git_dir: The repository to checkout the worktree from
296 output_dir: Path for the new worktree
297 commit_hash: Commit hash to checkout
298 """
299 # We need to pass --detach to avoid creating a new branch
300 pipe = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach']
301 if commit_hash:
302 pipe.append(commit_hash)
Simon Glass840be732022-01-29 14:14:05 -0700303 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500304 capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300305 if result.return_code != 0:
306 raise OSError('git worktree add: %s' % result.stderr)
307
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500308
Simon Glass761648b2022-01-29 14:14:11 -0700309def prune_worktrees(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300310 """Remove administrative files for deleted worktrees
311
312 Args:
313 git_dir: The repository whose deleted worktrees should be pruned
314 """
315 pipe = ['git', '--git-dir', git_dir, 'worktree', 'prune']
Simon Glass840be732022-01-29 14:14:05 -0700316 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300317 if result.return_code != 0:
318 raise OSError('git worktree prune: %s' % result.stderr)
319
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500320
321def create_patches(branch, start, count, ignore_binary, series, signoff=True):
Simon Glass26132882012-01-14 15:12:45 +0000322 """Create a series of patches from the top of the current branch.
323
324 The patch files are written to the current directory using
325 git format-patch.
326
327 Args:
Simon Glass2eb4da72020-07-05 21:41:51 -0600328 branch: Branch to create patches from (None for current branch)
Simon Glass26132882012-01-14 15:12:45 +0000329 start: Commit to start from: 0=HEAD, 1=next one, etc.
330 count: number of commits to include
Simon Glass24725af2020-07-05 21:41:49 -0600331 ignore_binary: Don't generate patches for binary files
332 series: Series object for this series (set of patches)
Simon Glass26132882012-01-14 15:12:45 +0000333 Return:
Simon Glass24725af2020-07-05 21:41:49 -0600334 Filename of cover letter (None if none)
Simon Glass26132882012-01-14 15:12:45 +0000335 List of filenames of patch files
336 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500337 cmd = ['git', 'format-patch', '-M']
Philipp Tomsich858531a2020-11-24 18:14:52 +0100338 if signoff:
339 cmd.append('--signoff')
Bin Menga04f1212020-05-04 00:52:44 -0700340 if ignore_binary:
341 cmd.append('--no-binary')
Simon Glass26132882012-01-14 15:12:45 +0000342 if series.get('cover'):
343 cmd.append('--cover-letter')
344 prefix = series.GetPatchPrefix()
345 if prefix:
346 cmd += ['--subject-prefix=%s' % prefix]
Simon Glass2eb4da72020-07-05 21:41:51 -0600347 brname = branch or 'HEAD'
348 cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
Simon Glass26132882012-01-14 15:12:45 +0000349
Simon Glass840be732022-01-29 14:14:05 -0700350 stdout = command.run_list(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000351 files = stdout.splitlines()
352
353 # We have an extra file if there is a cover letter
354 if series.get('cover'):
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500355 return files[0], files[1:]
Simon Glass26132882012-01-14 15:12:45 +0000356 else:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500357 return None, files
358
Simon Glass26132882012-01-14 15:12:45 +0000359
Simon Glass761648b2022-01-29 14:14:11 -0700360def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
Simon Glass26132882012-01-14 15:12:45 +0000361 """Build a list of email addresses based on an input list.
362
363 Takes a list of email addresses and aliases, and turns this into a list
364 of only email address, by resolving any aliases that are present.
365
366 If the tag is given, then each email address is prepended with this
367 tag and a space. If the tag starts with a minus sign (indicating a
368 command line parameter) then the email address is quoted.
369
370 Args:
371 in_list: List of aliases/email addresses
372 tag: Text to put before each address
Simon Glass12ea5f42013-03-26 13:09:42 +0000373 alias: Alias dictionary
Simon Glass1f975b92021-01-23 08:56:15 -0700374 warn_on_error: True to raise an error when an alias fails to match,
Simon Glass12ea5f42013-03-26 13:09:42 +0000375 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000376
377 Returns:
378 List of email addresses
379
380 >>> alias = {}
381 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
382 >>> alias['john'] = ['j.bloggs@napier.co.nz']
383 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
384 >>> alias['boys'] = ['fred', ' john']
385 >>> alias['all'] = ['fred ', 'john', ' mary ']
Simon Glass761648b2022-01-29 14:14:11 -0700386 >>> build_email_list(['john', 'mary'], None, alias)
Simon Glass26132882012-01-14 15:12:45 +0000387 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
Simon Glass761648b2022-01-29 14:14:11 -0700388 >>> build_email_list(['john', 'mary'], '--to', alias)
Simon Glass26132882012-01-14 15:12:45 +0000389 ['--to "j.bloggs@napier.co.nz"', \
390'--to "Mary Poppins <m.poppins@cloud.net>"']
Simon Glass761648b2022-01-29 14:14:11 -0700391 >>> build_email_list(['john', 'mary'], 'Cc', alias)
Simon Glass26132882012-01-14 15:12:45 +0000392 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
393 """
394 quote = '"' if tag and tag[0] == '-' else ''
395 raw = []
396 for item in in_list:
Simon Glass761648b2022-01-29 14:14:11 -0700397 raw += lookup_email(item, alias, warn_on_error=warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000398 result = []
399 for item in raw:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500400 if item not in result:
Simon Glass26132882012-01-14 15:12:45 +0000401 result.append(item)
402 if tag:
403 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
404 return result
405
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500406
Simon Glass761648b2022-01-29 14:14:11 -0700407def check_suppress_cc_config():
Nicolas Boichat0da95742020-07-13 10:50:00 +0800408 """Check if sendemail.suppresscc is configured correctly.
409
410 Returns:
411 True if the option is configured correctly, False otherwise.
412 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500413 suppresscc = command.output_one_line(
414 'git', 'config', 'sendemail.suppresscc', raise_on_error=False)
Nicolas Boichat0da95742020-07-13 10:50:00 +0800415
416 # Other settings should be fine.
417 if suppresscc == 'all' or suppresscc == 'cccmd':
418 col = terminal.Color()
419
Simon Glassf45d3742022-01-29 14:14:17 -0700420 print((col.build(col.RED, "error") +
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500421 ": git config sendemail.suppresscc set to %s\n"
422 % (suppresscc)) +
423 " patman needs --cc-cmd to be run to set the cc list.\n" +
424 " Please run:\n" +
425 " git config --unset sendemail.suppresscc\n" +
426 " Or read the man page:\n" +
427 " git send-email --help\n" +
428 " and set an option that runs --cc-cmd\n")
Nicolas Boichat0da95742020-07-13 10:50:00 +0800429 return False
430
431 return True
432
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500433
Simon Glass761648b2022-01-29 14:14:11 -0700434def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500435 self_only=False, alias=None, in_reply_to=None, thread=False,
Maxim Cournoyer3ef23e92022-12-20 00:28:46 -0500436 smtp_server=None, get_maintainer_script=None):
Simon Glass26132882012-01-14 15:12:45 +0000437 """Email a patch series.
438
439 Args:
440 series: Series object containing destination info
441 cover_fname: filename of cover letter
442 args: list of filenames of patch files
443 dry_run: Just return the command that would be run
Simon Glass1f975b92021-01-23 08:56:15 -0700444 warn_on_error: True to print a warning when an alias fails to match,
445 False to ignore it.
Simon Glass26132882012-01-14 15:12:45 +0000446 cc_fname: Filename of Cc file for per-commit Cc
447 self_only: True to just email to yourself as a test
Doug Anderson06f27ac2013-03-17 10:31:04 +0000448 in_reply_to: If set we'll pass this to git as --in-reply-to.
449 Should be a message ID that this is in reply to.
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100450 thread: True to add --thread to git send-email (make
451 all patches reply to cover-letter or first patch in series)
Simon Glass8137e302018-06-19 09:56:07 -0600452 smtp_server: SMTP server to use to send patches
Maxim Cournoyer3ef23e92022-12-20 00:28:46 -0500453 get_maintainer_script: File name of script to get maintainers emails
Simon Glass26132882012-01-14 15:12:45 +0000454
455 Returns:
456 Git command that was/would be run
457
Doug Anderson51d73212012-11-26 15:21:40 +0000458 # For the duration of this doctest pretend that we ran patman with ./patman
459 >>> _old_argv0 = sys.argv[0]
460 >>> sys.argv[0] = './patman'
461
Simon Glass26132882012-01-14 15:12:45 +0000462 >>> alias = {}
463 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
464 >>> alias['john'] = ['j.bloggs@napier.co.nz']
465 >>> alias['mary'] = ['m.poppins@cloud.net']
466 >>> alias['boys'] = ['fred', ' john']
467 >>> alias['all'] = ['fred ', 'john', ' mary ']
468 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
Simon Glass794c2592020-06-07 06:45:47 -0600469 >>> series = {}
470 >>> series['to'] = ['fred']
471 >>> series['cc'] = ['mary']
Simon Glass761648b2022-01-29 14:14:11 -0700472 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000473 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000474 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700475"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700476 >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \
Simon Glass12ea5f42013-03-26 13:09:42 +0000477 alias)
Simon Glass26132882012-01-14 15:12:45 +0000478 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700479"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
Simon Glass794c2592020-06-07 06:45:47 -0600480 >>> series['cc'] = ['all']
Simon Glass761648b2022-01-29 14:14:11 -0700481 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000482 True, alias)
Simon Glass26132882012-01-14 15:12:45 +0000483 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
Simon Glass1ee91c12020-11-03 13:54:10 -0700484send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700485 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000486 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000487 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
488"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700489"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Doug Anderson51d73212012-11-26 15:21:40 +0000490
491 # Restore argv[0] since we clobbered it.
492 >>> sys.argv[0] = _old_argv0
Simon Glass26132882012-01-14 15:12:45 +0000493 """
Simon Glass761648b2022-01-29 14:14:11 -0700494 to = build_email_list(series.get('to'), '--to', alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000495 if not to:
Simon Glass840be732022-01-29 14:14:05 -0700496 git_config_to = command.output('git', 'config', 'sendemail.to',
Simon Glassc55e0562016-07-25 18:59:00 -0600497 raise_on_error=False)
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900498 if not git_config_to:
Simon Glass23b8a192019-05-14 15:53:36 -0600499 print("No recipient.\n"
500 "Please add something like this to a commit\n"
501 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
502 "Or do something like this\n"
503 "git config sendemail.to u-boot@lists.denx.de")
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900504 return
Simon Glass761648b2022-01-29 14:14:11 -0700505 cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))),
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500506 '--cc', alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000507 if self_only:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500508 to = build_email_list([os.getenv('USER')], '--to',
509 alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000510 cc = []
511 cmd = ['git', 'send-email', '--annotate']
Simon Glass8137e302018-06-19 09:56:07 -0600512 if smtp_server:
513 cmd.append('--smtp-server=%s' % smtp_server)
Doug Anderson06f27ac2013-03-17 10:31:04 +0000514 if in_reply_to:
Simon Glass9dfb3112020-11-08 20:36:18 -0700515 cmd.append('--in-reply-to="%s"' % in_reply_to)
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100516 if thread:
517 cmd.append('--thread')
Doug Anderson06f27ac2013-03-17 10:31:04 +0000518
Simon Glass26132882012-01-14 15:12:45 +0000519 cmd += to
520 cmd += cc
Simon Glass1ee91c12020-11-03 13:54:10 -0700521 cmd += ['--cc-cmd', '"%s send --cc-cmd %s"' % (sys.argv[0], cc_fname)]
Simon Glass26132882012-01-14 15:12:45 +0000522 if cover_fname:
523 cmd.append(cover_fname)
524 cmd += args
Simon Glass47e308e2017-05-29 15:31:25 -0600525 cmdstr = ' '.join(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000526 if not dry_run:
Simon Glass47e308e2017-05-29 15:31:25 -0600527 os.system(cmdstr)
528 return cmdstr
Simon Glass26132882012-01-14 15:12:45 +0000529
530
Simon Glass761648b2022-01-29 14:14:11 -0700531def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
Simon Glass26132882012-01-14 15:12:45 +0000532 """If an email address is an alias, look it up and return the full name
533
534 TODO: Why not just use git's own alias feature?
535
536 Args:
537 lookup_name: Alias or email address to look up
Simon Glass12ea5f42013-03-26 13:09:42 +0000538 alias: Dictionary containing aliases (None to use settings default)
Simon Glass1f975b92021-01-23 08:56:15 -0700539 warn_on_error: True to print a warning when an alias fails to match,
540 False to ignore it.
Simon Glass26132882012-01-14 15:12:45 +0000541
542 Returns:
543 tuple:
544 list containing a list of email addresses
545
546 Raises:
547 OSError if a recursive alias reference was found
548 ValueError if an alias was not found
549
550 >>> alias = {}
551 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
552 >>> alias['john'] = ['j.bloggs@napier.co.nz']
553 >>> alias['mary'] = ['m.poppins@cloud.net']
554 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
555 >>> alias['all'] = ['fred ', 'john', ' mary ']
556 >>> alias['loop'] = ['other', 'john', ' mary ']
557 >>> alias['other'] = ['loop', 'john', ' mary ']
Simon Glass761648b2022-01-29 14:14:11 -0700558 >>> lookup_email('mary', alias)
Simon Glass26132882012-01-14 15:12:45 +0000559 ['m.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700560 >>> lookup_email('arthur.wellesley@howe.ro.uk', alias)
Simon Glass26132882012-01-14 15:12:45 +0000561 ['arthur.wellesley@howe.ro.uk']
Simon Glass761648b2022-01-29 14:14:11 -0700562 >>> lookup_email('boys', alias)
Simon Glass26132882012-01-14 15:12:45 +0000563 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
Simon Glass761648b2022-01-29 14:14:11 -0700564 >>> lookup_email('all', alias)
Simon Glass26132882012-01-14 15:12:45 +0000565 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700566 >>> lookup_email('odd', alias)
Simon Glass1f975b92021-01-23 08:56:15 -0700567 Alias 'odd' not found
568 []
Simon Glass761648b2022-01-29 14:14:11 -0700569 >>> lookup_email('loop', alias)
Simon Glass26132882012-01-14 15:12:45 +0000570 Traceback (most recent call last):
571 ...
572 OSError: Recursive email alias at 'other'
Simon Glass761648b2022-01-29 14:14:11 -0700573 >>> lookup_email('odd', alias, warn_on_error=False)
Simon Glass12ea5f42013-03-26 13:09:42 +0000574 []
575 >>> # In this case the loop part will effectively be ignored.
Simon Glass761648b2022-01-29 14:14:11 -0700576 >>> lookup_email('loop', alias, warn_on_error=False)
Simon Glassb0cd3412014-08-28 09:43:35 -0600577 Recursive email alias at 'other'
578 Recursive email alias at 'john'
579 Recursive email alias at 'mary'
Simon Glass12ea5f42013-03-26 13:09:42 +0000580 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass26132882012-01-14 15:12:45 +0000581 """
582 if not alias:
583 alias = settings.alias
584 lookup_name = lookup_name.strip()
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500585 if '@' in lookup_name: # Perhaps a real email address
Simon Glass26132882012-01-14 15:12:45 +0000586 return [lookup_name]
587
588 lookup_name = lookup_name.lower()
Simon Glass12ea5f42013-03-26 13:09:42 +0000589 col = terminal.Color()
Simon Glass26132882012-01-14 15:12:45 +0000590
Simon Glass12ea5f42013-03-26 13:09:42 +0000591 out_list = []
Simon Glass26132882012-01-14 15:12:45 +0000592 if level > 10:
Simon Glass12ea5f42013-03-26 13:09:42 +0000593 msg = "Recursive email alias at '%s'" % lookup_name
Simon Glass1f975b92021-01-23 08:56:15 -0700594 if warn_on_error:
Paul Burtonf14a1312016-09-27 16:03:51 +0100595 raise OSError(msg)
Simon Glass12ea5f42013-03-26 13:09:42 +0000596 else:
Simon Glassf45d3742022-01-29 14:14:17 -0700597 print(col.build(col.RED, msg))
Simon Glass12ea5f42013-03-26 13:09:42 +0000598 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000599
Simon Glass26132882012-01-14 15:12:45 +0000600 if lookup_name:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500601 if lookup_name not in alias:
Simon Glass12ea5f42013-03-26 13:09:42 +0000602 msg = "Alias '%s' not found" % lookup_name
Simon Glass1f975b92021-01-23 08:56:15 -0700603 if warn_on_error:
Simon Glassf45d3742022-01-29 14:14:17 -0700604 print(col.build(col.RED, msg))
Simon Glass1f975b92021-01-23 08:56:15 -0700605 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000606 for item in alias[lookup_name]:
Simon Glass761648b2022-01-29 14:14:11 -0700607 todo = lookup_email(item, alias, warn_on_error, level + 1)
Simon Glass26132882012-01-14 15:12:45 +0000608 for new_item in todo:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500609 if new_item not in out_list:
Simon Glass26132882012-01-14 15:12:45 +0000610 out_list.append(new_item)
611
Simon Glass26132882012-01-14 15:12:45 +0000612 return out_list
613
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500614
Simon Glass761648b2022-01-29 14:14:11 -0700615def get_top_level():
Simon Glass26132882012-01-14 15:12:45 +0000616 """Return name of top-level directory for this git repo.
617
618 Returns:
619 Full path to git top-level directory
620
621 This test makes sure that we are running tests in the right subdir
622
Doug Anderson51d73212012-11-26 15:21:40 +0000623 >>> os.path.realpath(os.path.dirname(__file__)) == \
Simon Glass761648b2022-01-29 14:14:11 -0700624 os.path.join(get_top_level(), 'tools', 'patman')
Simon Glass26132882012-01-14 15:12:45 +0000625 True
626 """
Simon Glass840be732022-01-29 14:14:05 -0700627 return command.output_one_line('git', 'rev-parse', '--show-toplevel')
Simon Glass26132882012-01-14 15:12:45 +0000628
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500629
Simon Glass761648b2022-01-29 14:14:11 -0700630def get_alias_file():
Simon Glass26132882012-01-14 15:12:45 +0000631 """Gets the name of the git alias file.
632
633 Returns:
634 Filename of git alias file, or None if none
635 """
Simon Glass840be732022-01-29 14:14:05 -0700636 fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile',
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500637 raise_on_error=False)
Brian Norris94c775d2022-01-07 15:15:55 -0800638 if not fname:
639 return None
640
641 fname = os.path.expanduser(fname.strip())
642 if os.path.isabs(fname):
643 return fname
644
Simon Glass761648b2022-01-29 14:14:11 -0700645 return os.path.join(get_top_level(), fname)
Simon Glass26132882012-01-14 15:12:45 +0000646
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500647
Simon Glass761648b2022-01-29 14:14:11 -0700648def get_default_user_name():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000649 """Gets the user.name from .gitconfig file.
650
651 Returns:
652 User name found in .gitconfig file, or None if none
653 """
Fei Shao78712232023-09-08 22:15:59 +0800654 uname = command.output_one_line('git', 'config', '--global', '--includes', 'user.name')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000655 return uname
656
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500657
Simon Glass761648b2022-01-29 14:14:11 -0700658def get_default_user_email():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000659 """Gets the user.email from the global .gitconfig file.
660
661 Returns:
662 User's email found in .gitconfig file, or None if none
663 """
Fei Shao78712232023-09-08 22:15:59 +0800664 uemail = command.output_one_line('git', 'config', '--global', '--includes', 'user.email')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000665 return uemail
666
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500667
Simon Glass761648b2022-01-29 14:14:11 -0700668def get_default_subject_prefix():
Wu, Josh9873b912015-04-15 10:25:18 +0800669 """Gets the format.subjectprefix from local .git/config file.
670
671 Returns:
672 Subject prefix found in local .git/config file, or None if none
673 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500674 sub_prefix = command.output_one_line(
675 'git', 'config', 'format.subjectprefix', raise_on_error=False)
Wu, Josh9873b912015-04-15 10:25:18 +0800676
677 return sub_prefix
678
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500679
Simon Glass761648b2022-01-29 14:14:11 -0700680def setup():
Simon Glass26132882012-01-14 15:12:45 +0000681 """Set up git utils, by reading the alias files."""
Simon Glass26132882012-01-14 15:12:45 +0000682 # Check for a git alias file also
Simon Glass81bcca82014-08-28 09:43:45 -0600683 global use_no_decorate
684
Simon Glass761648b2022-01-29 14:14:11 -0700685 alias_fname = get_alias_file()
Simon Glass26132882012-01-14 15:12:45 +0000686 if alias_fname:
687 settings.ReadGitAliases(alias_fname)
Simon Glass761648b2022-01-29 14:14:11 -0700688 cmd = log_cmd(None, count=0)
Simon Glass840be732022-01-29 14:14:05 -0700689 use_no_decorate = (command.run_pipe([cmd], raise_on_error=False)
Simon Glass6af913d2014-08-09 15:33:11 -0600690 .return_code == 0)
Simon Glass26132882012-01-14 15:12:45 +0000691
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500692
Simon Glass761648b2022-01-29 14:14:11 -0700693def get_head():
Simon Glass11aba512012-12-15 10:42:07 +0000694 """Get the hash of the current HEAD
695
696 Returns:
697 Hash of HEAD
698 """
Simon Glass840be732022-01-29 14:14:05 -0700699 return command.output_one_line('git', 'show', '-s', '--pretty=format:%H')
Simon Glass11aba512012-12-15 10:42:07 +0000700
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500701
Simon Glass26132882012-01-14 15:12:45 +0000702if __name__ == "__main__":
703 import doctest
704
705 doctest.testmod()