blob: 10ea5ff39f5c3ac65d2278e366201d4ddbed5f54 [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:
Simon Glass978c1fb2023-10-30 10:22:30 -0700150 # Drop the initial refs/heads from merge
151 leaf = merge.split('/', maxsplit=2)[2:]
152 return '%s/%s' % (remote, '/'.join(leaf)), None
Simon Glass11aba512012-12-15 10:42:07 +0000153 else:
Paul Burtonf14a1312016-09-27 16:03:51 +0100154 raise ValueError("Cannot determine upstream branch for branch "
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500155 "'%s' remote='%s', merge='%s'"
156 % (branch, remote, merge))
Simon Glass11aba512012-12-15 10:42:07 +0000157
158
Simon Glass761648b2022-01-29 14:14:11 -0700159def get_range_in_branch(git_dir, branch, include_upstream=False):
Simon Glass11aba512012-12-15 10:42:07 +0000160 """Returns an expression for the commits in the given branch.
161
162 Args:
163 git_dir: Directory containing git repo
164 branch: Name of branch
165 Return:
166 Expression in the form 'upstream..branch' which can be used to
Simon Glassd2e95382013-05-08 08:06:08 +0000167 access the commits. If the branch does not exist, returns None.
Simon Glass11aba512012-12-15 10:42:07 +0000168 """
Simon Glass761648b2022-01-29 14:14:11 -0700169 upstream, msg = get_upstream(git_dir, branch)
Simon Glassd2e95382013-05-08 08:06:08 +0000170 if not upstream:
Simon Glassf204ab12014-12-01 17:33:54 -0700171 return None, msg
172 rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
173 return rstr, msg
Simon Glass11aba512012-12-15 10:42:07 +0000174
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500175
Simon Glass761648b2022-01-29 14:14:11 -0700176def count_commits_in_range(git_dir, range_expr):
Simon Glass5eeef462014-12-01 17:33:57 -0700177 """Returns the number of commits in the given range.
178
179 Args:
180 git_dir: Directory containing git repo
181 range_expr: Range to check
182 Return:
Anatolij Gustschinf2bcb322019-10-27 17:55:04 +0100183 Number of patches that exist in the supplied range or None if none
Simon Glass5eeef462014-12-01 17:33:57 -0700184 were found
185 """
Simon Glass761648b2022-01-29 14:14:11 -0700186 pipe = [log_cmd(range_expr, git_dir=git_dir, oneline=True)]
Simon Glass840be732022-01-29 14:14:05 -0700187 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500188 raise_on_error=False)
Simon Glass5eeef462014-12-01 17:33:57 -0700189 if result.return_code:
190 return None, "Range '%s' not found or is invalid" % range_expr
191 patch_count = len(result.stdout.splitlines())
192 return patch_count, None
193
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500194
Simon Glass761648b2022-01-29 14:14:11 -0700195def count_commits_in_branch(git_dir, branch, include_upstream=False):
Simon Glass11aba512012-12-15 10:42:07 +0000196 """Returns the number of commits in the given branch.
197
198 Args:
199 git_dir: Directory containing git repo
200 branch: Name of branch
201 Return:
Simon Glassd2e95382013-05-08 08:06:08 +0000202 Number of patches that exist on top of the branch, or None if the
203 branch does not exist.
Simon Glass11aba512012-12-15 10:42:07 +0000204 """
Simon Glass761648b2022-01-29 14:14:11 -0700205 range_expr, msg = get_range_in_branch(git_dir, branch, include_upstream)
Simon Glassd2e95382013-05-08 08:06:08 +0000206 if not range_expr:
Simon Glassf204ab12014-12-01 17:33:54 -0700207 return None, msg
Simon Glass761648b2022-01-29 14:14:11 -0700208 return count_commits_in_range(git_dir, range_expr)
Simon Glass11aba512012-12-15 10:42:07 +0000209
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500210
Simon Glass761648b2022-01-29 14:14:11 -0700211def count_commits(commit_range):
Simon Glass11aba512012-12-15 10:42:07 +0000212 """Returns the number of commits in the given range.
213
214 Args:
215 commit_range: Range of commits to count (e.g. 'HEAD..base')
216 Return:
217 Number of patches that exist on top of the branch
218 """
Simon Glass761648b2022-01-29 14:14:11 -0700219 pipe = [log_cmd(commit_range, oneline=True),
Simon Glass11aba512012-12-15 10:42:07 +0000220 ['wc', '-l']]
Simon Glass840be732022-01-29 14:14:05 -0700221 stdout = command.run_pipe(pipe, capture=True, oneline=True).stdout
Simon Glass11aba512012-12-15 10:42:07 +0000222 patch_count = int(stdout)
223 return patch_count
224
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500225
Simon Glass761648b2022-01-29 14:14:11 -0700226def checkout(commit_hash, git_dir=None, work_tree=None, force=False):
Simon Glass11aba512012-12-15 10:42:07 +0000227 """Checkout the selected commit for this build
228
229 Args:
230 commit_hash: Commit hash to check out
231 """
232 pipe = ['git']
233 if git_dir:
234 pipe.extend(['--git-dir', git_dir])
235 if work_tree:
236 pipe.extend(['--work-tree', work_tree])
237 pipe.append('checkout')
238 if force:
239 pipe.append('-f')
240 pipe.append(commit_hash)
Simon Glass840be732022-01-29 14:14:05 -0700241 result = command.run_pipe([pipe], capture=True, raise_on_error=False,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500242 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000243 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100244 raise OSError('git checkout (%s): %s' % (pipe, result.stderr))
Simon Glass11aba512012-12-15 10:42:07 +0000245
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500246
Simon Glass761648b2022-01-29 14:14:11 -0700247def clone(git_dir, output_dir):
Simon Glass11aba512012-12-15 10:42:07 +0000248 """Checkout the selected commit for this build
249
250 Args:
251 commit_hash: Commit hash to check out
252 """
253 pipe = ['git', 'clone', git_dir, '.']
Simon Glass840be732022-01-29 14:14:05 -0700254 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500255 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000256 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100257 raise OSError('git clone: %s' % result.stderr)
Simon Glass11aba512012-12-15 10:42:07 +0000258
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500259
Simon Glass761648b2022-01-29 14:14:11 -0700260def fetch(git_dir=None, work_tree=None):
Simon Glass11aba512012-12-15 10:42:07 +0000261 """Fetch from the origin repo
262
263 Args:
264 commit_hash: Commit hash to check out
265 """
266 pipe = ['git']
267 if git_dir:
268 pipe.extend(['--git-dir', git_dir])
269 if work_tree:
270 pipe.extend(['--work-tree', work_tree])
271 pipe.append('fetch')
Simon Glass840be732022-01-29 14:14:05 -0700272 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000273 if result.return_code != 0:
Paul Burtonf14a1312016-09-27 16:03:51 +0100274 raise OSError('git fetch: %s' % result.stderr)
Simon Glass11aba512012-12-15 10:42:07 +0000275
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500276
Simon Glass761648b2022-01-29 14:14:11 -0700277def check_worktree_is_available(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300278 """Check if git-worktree functionality is available
279
280 Args:
281 git_dir: The repository to test in
282
283 Returns:
284 True if git-worktree commands will work, False otherwise.
285 """
286 pipe = ['git', '--git-dir', git_dir, 'worktree', 'list']
Simon Glass840be732022-01-29 14:14:05 -0700287 result = command.run_pipe([pipe], capture=True, capture_stderr=True,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500288 raise_on_error=False)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300289 return result.return_code == 0
290
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500291
Simon Glass761648b2022-01-29 14:14:11 -0700292def add_worktree(git_dir, output_dir, commit_hash=None):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300293 """Create and checkout a new git worktree for this build
294
295 Args:
296 git_dir: The repository to checkout the worktree from
297 output_dir: Path for the new worktree
298 commit_hash: Commit hash to checkout
299 """
300 # We need to pass --detach to avoid creating a new branch
301 pipe = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach']
302 if commit_hash:
303 pipe.append(commit_hash)
Simon Glass840be732022-01-29 14:14:05 -0700304 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500305 capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300306 if result.return_code != 0:
307 raise OSError('git worktree add: %s' % result.stderr)
308
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500309
Simon Glass761648b2022-01-29 14:14:11 -0700310def prune_worktrees(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300311 """Remove administrative files for deleted worktrees
312
313 Args:
314 git_dir: The repository whose deleted worktrees should be pruned
315 """
316 pipe = ['git', '--git-dir', git_dir, 'worktree', 'prune']
Simon Glass840be732022-01-29 14:14:05 -0700317 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300318 if result.return_code != 0:
319 raise OSError('git worktree prune: %s' % result.stderr)
320
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500321
322def create_patches(branch, start, count, ignore_binary, series, signoff=True):
Simon Glass26132882012-01-14 15:12:45 +0000323 """Create a series of patches from the top of the current branch.
324
325 The patch files are written to the current directory using
326 git format-patch.
327
328 Args:
Simon Glass2eb4da72020-07-05 21:41:51 -0600329 branch: Branch to create patches from (None for current branch)
Simon Glass26132882012-01-14 15:12:45 +0000330 start: Commit to start from: 0=HEAD, 1=next one, etc.
331 count: number of commits to include
Simon Glass24725af2020-07-05 21:41:49 -0600332 ignore_binary: Don't generate patches for binary files
333 series: Series object for this series (set of patches)
Simon Glass26132882012-01-14 15:12:45 +0000334 Return:
Simon Glass24725af2020-07-05 21:41:49 -0600335 Filename of cover letter (None if none)
Simon Glass26132882012-01-14 15:12:45 +0000336 List of filenames of patch files
337 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500338 cmd = ['git', 'format-patch', '-M']
Philipp Tomsich858531a2020-11-24 18:14:52 +0100339 if signoff:
340 cmd.append('--signoff')
Bin Menga04f1212020-05-04 00:52:44 -0700341 if ignore_binary:
342 cmd.append('--no-binary')
Simon Glass26132882012-01-14 15:12:45 +0000343 if series.get('cover'):
344 cmd.append('--cover-letter')
345 prefix = series.GetPatchPrefix()
346 if prefix:
347 cmd += ['--subject-prefix=%s' % prefix]
Simon Glass2eb4da72020-07-05 21:41:51 -0600348 brname = branch or 'HEAD'
349 cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
Simon Glass26132882012-01-14 15:12:45 +0000350
Simon Glass840be732022-01-29 14:14:05 -0700351 stdout = command.run_list(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000352 files = stdout.splitlines()
353
354 # We have an extra file if there is a cover letter
355 if series.get('cover'):
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500356 return files[0], files[1:]
Simon Glass26132882012-01-14 15:12:45 +0000357 else:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500358 return None, files
359
Simon Glass26132882012-01-14 15:12:45 +0000360
Simon Glass761648b2022-01-29 14:14:11 -0700361def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
Simon Glass26132882012-01-14 15:12:45 +0000362 """Build a list of email addresses based on an input list.
363
364 Takes a list of email addresses and aliases, and turns this into a list
365 of only email address, by resolving any aliases that are present.
366
367 If the tag is given, then each email address is prepended with this
368 tag and a space. If the tag starts with a minus sign (indicating a
369 command line parameter) then the email address is quoted.
370
371 Args:
372 in_list: List of aliases/email addresses
373 tag: Text to put before each address
Simon Glass12ea5f42013-03-26 13:09:42 +0000374 alias: Alias dictionary
Simon Glass1f975b92021-01-23 08:56:15 -0700375 warn_on_error: True to raise an error when an alias fails to match,
Simon Glass12ea5f42013-03-26 13:09:42 +0000376 False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000377
378 Returns:
379 List of email addresses
380
381 >>> alias = {}
382 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
383 >>> alias['john'] = ['j.bloggs@napier.co.nz']
384 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
385 >>> alias['boys'] = ['fred', ' john']
386 >>> alias['all'] = ['fred ', 'john', ' mary ']
Simon Glass761648b2022-01-29 14:14:11 -0700387 >>> build_email_list(['john', 'mary'], None, alias)
Simon Glass26132882012-01-14 15:12:45 +0000388 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
Simon Glass761648b2022-01-29 14:14:11 -0700389 >>> build_email_list(['john', 'mary'], '--to', alias)
Simon Glass26132882012-01-14 15:12:45 +0000390 ['--to "j.bloggs@napier.co.nz"', \
391'--to "Mary Poppins <m.poppins@cloud.net>"']
Simon Glass761648b2022-01-29 14:14:11 -0700392 >>> build_email_list(['john', 'mary'], 'Cc', alias)
Simon Glass26132882012-01-14 15:12:45 +0000393 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
394 """
395 quote = '"' if tag and tag[0] == '-' else ''
396 raw = []
397 for item in in_list:
Simon Glass761648b2022-01-29 14:14:11 -0700398 raw += lookup_email(item, alias, warn_on_error=warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000399 result = []
400 for item in raw:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500401 if item not in result:
Simon Glass26132882012-01-14 15:12:45 +0000402 result.append(item)
403 if tag:
404 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
405 return result
406
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500407
Simon Glass761648b2022-01-29 14:14:11 -0700408def check_suppress_cc_config():
Nicolas Boichat0da95742020-07-13 10:50:00 +0800409 """Check if sendemail.suppresscc is configured correctly.
410
411 Returns:
412 True if the option is configured correctly, False otherwise.
413 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500414 suppresscc = command.output_one_line(
415 'git', 'config', 'sendemail.suppresscc', raise_on_error=False)
Nicolas Boichat0da95742020-07-13 10:50:00 +0800416
417 # Other settings should be fine.
418 if suppresscc == 'all' or suppresscc == 'cccmd':
419 col = terminal.Color()
420
Simon Glassf45d3742022-01-29 14:14:17 -0700421 print((col.build(col.RED, "error") +
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500422 ": git config sendemail.suppresscc set to %s\n"
423 % (suppresscc)) +
424 " patman needs --cc-cmd to be run to set the cc list.\n" +
425 " Please run:\n" +
426 " git config --unset sendemail.suppresscc\n" +
427 " Or read the man page:\n" +
428 " git send-email --help\n" +
429 " and set an option that runs --cc-cmd\n")
Nicolas Boichat0da95742020-07-13 10:50:00 +0800430 return False
431
432 return True
433
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500434
Simon Glass761648b2022-01-29 14:14:11 -0700435def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500436 self_only=False, alias=None, in_reply_to=None, thread=False,
Maxim Cournoyer3ef23e92022-12-20 00:28:46 -0500437 smtp_server=None, get_maintainer_script=None):
Simon Glass26132882012-01-14 15:12:45 +0000438 """Email a patch series.
439
440 Args:
441 series: Series object containing destination info
442 cover_fname: filename of cover letter
443 args: list of filenames of patch files
444 dry_run: Just return the command that would be run
Simon Glass1f975b92021-01-23 08:56:15 -0700445 warn_on_error: True to print a warning when an alias fails to match,
446 False to ignore it.
Simon Glass26132882012-01-14 15:12:45 +0000447 cc_fname: Filename of Cc file for per-commit Cc
448 self_only: True to just email to yourself as a test
Doug Anderson06f27ac2013-03-17 10:31:04 +0000449 in_reply_to: If set we'll pass this to git as --in-reply-to.
450 Should be a message ID that this is in reply to.
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100451 thread: True to add --thread to git send-email (make
452 all patches reply to cover-letter or first patch in series)
Simon Glass8137e302018-06-19 09:56:07 -0600453 smtp_server: SMTP server to use to send patches
Maxim Cournoyer3ef23e92022-12-20 00:28:46 -0500454 get_maintainer_script: File name of script to get maintainers emails
Simon Glass26132882012-01-14 15:12:45 +0000455
456 Returns:
457 Git command that was/would be run
458
Doug Anderson51d73212012-11-26 15:21:40 +0000459 # For the duration of this doctest pretend that we ran patman with ./patman
460 >>> _old_argv0 = sys.argv[0]
461 >>> sys.argv[0] = './patman'
462
Simon Glass26132882012-01-14 15:12:45 +0000463 >>> alias = {}
464 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
465 >>> alias['john'] = ['j.bloggs@napier.co.nz']
466 >>> alias['mary'] = ['m.poppins@cloud.net']
467 >>> alias['boys'] = ['fred', ' john']
468 >>> alias['all'] = ['fred ', 'john', ' mary ']
469 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
Simon Glass794c2592020-06-07 06:45:47 -0600470 >>> series = {}
471 >>> series['to'] = ['fred']
472 >>> series['cc'] = ['mary']
Simon Glass761648b2022-01-29 14:14:11 -0700473 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000474 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000475 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700476"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700477 >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \
Simon Glass12ea5f42013-03-26 13:09:42 +0000478 alias)
Simon Glass26132882012-01-14 15:12:45 +0000479 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700480"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
Simon Glass794c2592020-06-07 06:45:47 -0600481 >>> series['cc'] = ['all']
Simon Glass761648b2022-01-29 14:14:11 -0700482 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000483 True, alias)
Simon Glass26132882012-01-14 15:12:45 +0000484 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
Simon Glass1ee91c12020-11-03 13:54:10 -0700485send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700486 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000487 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000488 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
489"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700490"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Doug Anderson51d73212012-11-26 15:21:40 +0000491
492 # Restore argv[0] since we clobbered it.
493 >>> sys.argv[0] = _old_argv0
Simon Glass26132882012-01-14 15:12:45 +0000494 """
Simon Glass761648b2022-01-29 14:14:11 -0700495 to = build_email_list(series.get('to'), '--to', alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000496 if not to:
Simon Glass840be732022-01-29 14:14:05 -0700497 git_config_to = command.output('git', 'config', 'sendemail.to',
Simon Glassc55e0562016-07-25 18:59:00 -0600498 raise_on_error=False)
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900499 if not git_config_to:
Simon Glass23b8a192019-05-14 15:53:36 -0600500 print("No recipient.\n"
501 "Please add something like this to a commit\n"
502 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
503 "Or do something like this\n"
504 "git config sendemail.to u-boot@lists.denx.de")
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900505 return
Simon Glass761648b2022-01-29 14:14:11 -0700506 cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))),
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500507 '--cc', alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000508 if self_only:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500509 to = build_email_list([os.getenv('USER')], '--to',
510 alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000511 cc = []
512 cmd = ['git', 'send-email', '--annotate']
Simon Glass8137e302018-06-19 09:56:07 -0600513 if smtp_server:
514 cmd.append('--smtp-server=%s' % smtp_server)
Doug Anderson06f27ac2013-03-17 10:31:04 +0000515 if in_reply_to:
Simon Glass9dfb3112020-11-08 20:36:18 -0700516 cmd.append('--in-reply-to="%s"' % in_reply_to)
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100517 if thread:
518 cmd.append('--thread')
Doug Anderson06f27ac2013-03-17 10:31:04 +0000519
Simon Glass26132882012-01-14 15:12:45 +0000520 cmd += to
521 cmd += cc
Simon Glass1ee91c12020-11-03 13:54:10 -0700522 cmd += ['--cc-cmd', '"%s send --cc-cmd %s"' % (sys.argv[0], cc_fname)]
Simon Glass26132882012-01-14 15:12:45 +0000523 if cover_fname:
524 cmd.append(cover_fname)
525 cmd += args
Simon Glass47e308e2017-05-29 15:31:25 -0600526 cmdstr = ' '.join(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000527 if not dry_run:
Simon Glass47e308e2017-05-29 15:31:25 -0600528 os.system(cmdstr)
529 return cmdstr
Simon Glass26132882012-01-14 15:12:45 +0000530
531
Simon Glass761648b2022-01-29 14:14:11 -0700532def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
Simon Glass26132882012-01-14 15:12:45 +0000533 """If an email address is an alias, look it up and return the full name
534
535 TODO: Why not just use git's own alias feature?
536
537 Args:
538 lookup_name: Alias or email address to look up
Simon Glass12ea5f42013-03-26 13:09:42 +0000539 alias: Dictionary containing aliases (None to use settings default)
Simon Glass1f975b92021-01-23 08:56:15 -0700540 warn_on_error: True to print a warning when an alias fails to match,
541 False to ignore it.
Simon Glass26132882012-01-14 15:12:45 +0000542
543 Returns:
544 tuple:
545 list containing a list of email addresses
546
547 Raises:
548 OSError if a recursive alias reference was found
549 ValueError if an alias was not found
550
551 >>> alias = {}
552 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
553 >>> alias['john'] = ['j.bloggs@napier.co.nz']
554 >>> alias['mary'] = ['m.poppins@cloud.net']
555 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
556 >>> alias['all'] = ['fred ', 'john', ' mary ']
557 >>> alias['loop'] = ['other', 'john', ' mary ']
558 >>> alias['other'] = ['loop', 'john', ' mary ']
Simon Glass761648b2022-01-29 14:14:11 -0700559 >>> lookup_email('mary', alias)
Simon Glass26132882012-01-14 15:12:45 +0000560 ['m.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700561 >>> lookup_email('arthur.wellesley@howe.ro.uk', alias)
Simon Glass26132882012-01-14 15:12:45 +0000562 ['arthur.wellesley@howe.ro.uk']
Simon Glass761648b2022-01-29 14:14:11 -0700563 >>> lookup_email('boys', alias)
Simon Glass26132882012-01-14 15:12:45 +0000564 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
Simon Glass761648b2022-01-29 14:14:11 -0700565 >>> lookup_email('all', alias)
Simon Glass26132882012-01-14 15:12:45 +0000566 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700567 >>> lookup_email('odd', alias)
Simon Glass1f975b92021-01-23 08:56:15 -0700568 Alias 'odd' not found
569 []
Simon Glass761648b2022-01-29 14:14:11 -0700570 >>> lookup_email('loop', alias)
Simon Glass26132882012-01-14 15:12:45 +0000571 Traceback (most recent call last):
572 ...
573 OSError: Recursive email alias at 'other'
Simon Glass761648b2022-01-29 14:14:11 -0700574 >>> lookup_email('odd', alias, warn_on_error=False)
Simon Glass12ea5f42013-03-26 13:09:42 +0000575 []
576 >>> # In this case the loop part will effectively be ignored.
Simon Glass761648b2022-01-29 14:14:11 -0700577 >>> lookup_email('loop', alias, warn_on_error=False)
Simon Glassb0cd3412014-08-28 09:43:35 -0600578 Recursive email alias at 'other'
579 Recursive email alias at 'john'
580 Recursive email alias at 'mary'
Simon Glass12ea5f42013-03-26 13:09:42 +0000581 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass26132882012-01-14 15:12:45 +0000582 """
583 if not alias:
584 alias = settings.alias
585 lookup_name = lookup_name.strip()
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500586 if '@' in lookup_name: # Perhaps a real email address
Simon Glass26132882012-01-14 15:12:45 +0000587 return [lookup_name]
588
589 lookup_name = lookup_name.lower()
Simon Glass12ea5f42013-03-26 13:09:42 +0000590 col = terminal.Color()
Simon Glass26132882012-01-14 15:12:45 +0000591
Simon Glass12ea5f42013-03-26 13:09:42 +0000592 out_list = []
Simon Glass26132882012-01-14 15:12:45 +0000593 if level > 10:
Simon Glass12ea5f42013-03-26 13:09:42 +0000594 msg = "Recursive email alias at '%s'" % lookup_name
Simon Glass1f975b92021-01-23 08:56:15 -0700595 if warn_on_error:
Paul Burtonf14a1312016-09-27 16:03:51 +0100596 raise OSError(msg)
Simon Glass12ea5f42013-03-26 13:09:42 +0000597 else:
Simon Glassf45d3742022-01-29 14:14:17 -0700598 print(col.build(col.RED, msg))
Simon Glass12ea5f42013-03-26 13:09:42 +0000599 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000600
Simon Glass26132882012-01-14 15:12:45 +0000601 if lookup_name:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500602 if lookup_name not in alias:
Simon Glass12ea5f42013-03-26 13:09:42 +0000603 msg = "Alias '%s' not found" % lookup_name
Simon Glass1f975b92021-01-23 08:56:15 -0700604 if warn_on_error:
Simon Glassf45d3742022-01-29 14:14:17 -0700605 print(col.build(col.RED, msg))
Simon Glass1f975b92021-01-23 08:56:15 -0700606 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000607 for item in alias[lookup_name]:
Simon Glass761648b2022-01-29 14:14:11 -0700608 todo = lookup_email(item, alias, warn_on_error, level + 1)
Simon Glass26132882012-01-14 15:12:45 +0000609 for new_item in todo:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500610 if new_item not in out_list:
Simon Glass26132882012-01-14 15:12:45 +0000611 out_list.append(new_item)
612
Simon Glass26132882012-01-14 15:12:45 +0000613 return out_list
614
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500615
Simon Glass761648b2022-01-29 14:14:11 -0700616def get_top_level():
Simon Glass26132882012-01-14 15:12:45 +0000617 """Return name of top-level directory for this git repo.
618
619 Returns:
620 Full path to git top-level directory
621
622 This test makes sure that we are running tests in the right subdir
623
Doug Anderson51d73212012-11-26 15:21:40 +0000624 >>> os.path.realpath(os.path.dirname(__file__)) == \
Simon Glass761648b2022-01-29 14:14:11 -0700625 os.path.join(get_top_level(), 'tools', 'patman')
Simon Glass26132882012-01-14 15:12:45 +0000626 True
627 """
Simon Glass840be732022-01-29 14:14:05 -0700628 return command.output_one_line('git', 'rev-parse', '--show-toplevel')
Simon Glass26132882012-01-14 15:12:45 +0000629
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500630
Simon Glass761648b2022-01-29 14:14:11 -0700631def get_alias_file():
Simon Glass26132882012-01-14 15:12:45 +0000632 """Gets the name of the git alias file.
633
634 Returns:
635 Filename of git alias file, or None if none
636 """
Simon Glass840be732022-01-29 14:14:05 -0700637 fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile',
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500638 raise_on_error=False)
Brian Norris94c775d2022-01-07 15:15:55 -0800639 if not fname:
640 return None
641
642 fname = os.path.expanduser(fname.strip())
643 if os.path.isabs(fname):
644 return fname
645
Simon Glass761648b2022-01-29 14:14:11 -0700646 return os.path.join(get_top_level(), fname)
Simon Glass26132882012-01-14 15:12:45 +0000647
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500648
Simon Glass761648b2022-01-29 14:14:11 -0700649def get_default_user_name():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000650 """Gets the user.name from .gitconfig file.
651
652 Returns:
653 User name found in .gitconfig file, or None if none
654 """
Fei Shao78712232023-09-08 22:15:59 +0800655 uname = command.output_one_line('git', 'config', '--global', '--includes', 'user.name')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000656 return uname
657
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500658
Simon Glass761648b2022-01-29 14:14:11 -0700659def get_default_user_email():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000660 """Gets the user.email from the global .gitconfig file.
661
662 Returns:
663 User's email found in .gitconfig file, or None if none
664 """
Fei Shao78712232023-09-08 22:15:59 +0800665 uemail = command.output_one_line('git', 'config', '--global', '--includes', 'user.email')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000666 return uemail
667
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500668
Simon Glass761648b2022-01-29 14:14:11 -0700669def get_default_subject_prefix():
Wu, Josh9873b912015-04-15 10:25:18 +0800670 """Gets the format.subjectprefix from local .git/config file.
671
672 Returns:
673 Subject prefix found in local .git/config file, or None if none
674 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500675 sub_prefix = command.output_one_line(
676 'git', 'config', 'format.subjectprefix', raise_on_error=False)
Wu, Josh9873b912015-04-15 10:25:18 +0800677
678 return sub_prefix
679
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500680
Simon Glass761648b2022-01-29 14:14:11 -0700681def setup():
Simon Glass26132882012-01-14 15:12:45 +0000682 """Set up git utils, by reading the alias files."""
Simon Glass26132882012-01-14 15:12:45 +0000683 # Check for a git alias file also
Simon Glass81bcca82014-08-28 09:43:45 -0600684 global use_no_decorate
685
Simon Glass761648b2022-01-29 14:14:11 -0700686 alias_fname = get_alias_file()
Simon Glass26132882012-01-14 15:12:45 +0000687 if alias_fname:
688 settings.ReadGitAliases(alias_fname)
Simon Glass761648b2022-01-29 14:14:11 -0700689 cmd = log_cmd(None, count=0)
Simon Glass840be732022-01-29 14:14:05 -0700690 use_no_decorate = (command.run_pipe([cmd], raise_on_error=False)
Simon Glass6af913d2014-08-09 15:33:11 -0600691 .return_code == 0)
Simon Glass26132882012-01-14 15:12:45 +0000692
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500693
Simon Glass761648b2022-01-29 14:14:11 -0700694def get_head():
Simon Glass11aba512012-12-15 10:42:07 +0000695 """Get the hash of the current HEAD
696
697 Returns:
698 Hash of HEAD
699 """
Simon Glass840be732022-01-29 14:14:05 -0700700 return command.output_one_line('git', 'show', '-s', '--pretty=format:%H')
Simon Glass11aba512012-12-15 10:42:07 +0000701
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500702
Simon Glass26132882012-01-14 15:12:45 +0000703if __name__ == "__main__":
704 import doctest
705
706 doctest.testmod()