blob: 74c6e944948675ff7a060320c19e1e32477ec639 [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 command
Simon Glassa997ea52020-04-17 18:09:04 -06009from patman import settings
10from patman 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,
436 smtp_server=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
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 """
Simon Glass840be732022-01-29 14:14:05 -0700653 uname = command.output_one_line('git', 'config', '--global', '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 """
Simon Glass840be732022-01-29 14:14:05 -0700663 uemail = command.output_one_line('git', 'config', '--global', '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 Glass840be732022-01-29 14:14:05 -0700688 use_no_decorate = (command.run_pipe([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()