blob: 5e742102c21ddcfc1b48bfe263179220c53e451b [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass0d24de92012-01-14 15:12:45 +00002# Copyright (c) 2011 The Chromium OS Authors.
3#
Simon Glass0d24de92012-01-14 15:12:45 +00004
Simon Glass0d24de92012-01-14 15:12:45 +00005import os
Simon Glass0d24de92012-01-14 15:12:45 +00006import sys
Simon Glass0d24de92012-01-14 15:12:45 +00007
Simon Glassbf776672020-04-17 18:09:04 -06008from patman import command
Simon Glassbf776672020-04-17 18:09:04 -06009from patman import settings
10from patman import terminal
Simon Glass5f6a1c42012-12-15 10:42:07 +000011
Simon Glass0157b182022-01-29 14:14:11 -070012# True to use --no-decorate - we check this in setup()
Simon Glasse49f14a2014-08-09 15:33:11 -060013use_no_decorate = True
14
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -050015
Simon Glass0157b182022-01-29 14:14:11 -070016def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -050017 count=None):
Simon Glasscda2a612014-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 Gustschinab4a6ab2019-10-27 17:55:04 +010022 git_dir: Path to git repository (None to use default)
Simon Glasscda2a612014-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 Glass9447a6b2014-08-28 09:43:37 -060032 cmd += ['--no-pager', 'log', '--no-color']
Simon Glasscda2a612014-08-09 15:33:10 -060033 if oneline:
34 cmd.append('--oneline')
Simon Glasse49f14a2014-08-09 15:33:11 -060035 if use_no_decorate:
36 cmd.append('--no-decorate')
Simon Glass042a7322014-08-14 21:59:11 -060037 if reverse:
38 cmd.append('--reverse')
Simon Glasscda2a612014-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 Glassd4c85722016-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 Glasscda2a612014-08-09 15:33:10 -060048 return cmd
Simon Glass0d24de92012-01-14 15:12:45 +000049
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -050050
Simon Glass0157b182022-01-29 14:14:11 -070051def count_commits_to_branch(branch):
Simon Glass0d24de92012-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 Glass262130f2020-07-05 21:41:51 -060057 Args:
58 branch: Branch to count from (None for current branch)
59
Simon Glass0d24de92012-01-14 15:12:45 +000060 Return:
61 Number of patches that exist on top of the branch
62 """
Simon Glass262130f2020-07-05 21:41:51 -060063 if branch:
Simon Glass0157b182022-01-29 14:14:11 -070064 us, msg = get_upstream('.git', branch)
Simon Glass262130f2020-07-05 21:41:51 -060065 rev_range = '%s..%s' % (us, branch)
66 else:
67 rev_range = '@{upstream}..'
Simon Glass0157b182022-01-29 14:14:11 -070068 pipe = [log_cmd(rev_range, oneline=True)]
Simon Glassd9800692022-01-29 14:14:05 -070069 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -050070 oneline=True, raise_on_error=False)
Simon Glassbe051c02020-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 Glass0d24de92012-01-14 15:12:45 +000075 return patch_count
76
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -050077
Simon Glass0157b182022-01-29 14:14:11 -070078def name_revision(commit_hash):
Simon Glass2a9e2c62014-12-01 17:33:54 -070079 """Gets the revision name for a commit
80
81 Args:
82 commit_hash: Commit hash to look up
83
84 Return:
85 Name of revision, if any, else None
86 """
87 pipe = ['git', 'name-rev', commit_hash]
Simon Glassd9800692022-01-29 14:14:05 -070088 stdout = command.run_pipe([pipe], capture=True, oneline=True).stdout
Simon Glass2a9e2c62014-12-01 17:33:54 -070089
90 # We expect a commit, a space, then a revision name
91 name = stdout.split(' ')[1].strip()
92 return name
93
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -050094
Simon Glass0157b182022-01-29 14:14:11 -070095def guess_upstream(git_dir, branch):
Simon Glass2a9e2c62014-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 Glass0157b182022-01-29 14:14:11 -0700111 pipe = [log_cmd(branch, git_dir=git_dir, oneline=True, count=100)]
Simon Glassd9800692022-01-29 14:14:05 -0700112 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500113 raise_on_error=False)
Simon Glass2a9e2c62014-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 Glass0157b182022-01-29 14:14:11 -0700118 name = name_revision(commit_hash)
Simon Glass2a9e2c62014-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500125
Simon Glass0157b182022-01-29 14:14:11 -0700126def get_upstream(git_dir, branch):
Simon Glass5f6a1c42012-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 Glass2a9e2c62014-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 Glass5f6a1c42012-12-15 10:42:07 +0000137 """
Simon Glasscce717a2013-05-08 08:06:08 +0000138 try:
Simon Glassd9800692022-01-29 14:14:05 -0700139 remote = command.output_one_line('git', '--git-dir', git_dir, 'config',
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500140 'branch.%s.remote' % branch)
Simon Glassd9800692022-01-29 14:14:05 -0700141 merge = command.output_one_line('git', '--git-dir', git_dir, 'config',
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500142 'branch.%s.merge' % branch)
143 except Exception:
Simon Glass0157b182022-01-29 14:14:11 -0700144 upstream, msg = guess_upstream(git_dir, branch)
Simon Glass2a9e2c62014-12-01 17:33:54 -0700145 return upstream, msg
Simon Glasscce717a2013-05-08 08:06:08 +0000146
Simon Glass5f6a1c42012-12-15 10:42:07 +0000147 if remote == '.':
Simon Glass71edbe52015-01-29 11:35:16 -0700148 return merge, None
Simon Glass5f6a1c42012-12-15 10:42:07 +0000149 elif remote and merge:
150 leaf = merge.split('/')[-1]
Simon Glass2a9e2c62014-12-01 17:33:54 -0700151 return '%s/%s' % (remote, leaf), None
Simon Glass5f6a1c42012-12-15 10:42:07 +0000152 else:
Paul Burtonac3fde92016-09-27 16:03:51 +0100153 raise ValueError("Cannot determine upstream branch for branch "
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500154 "'%s' remote='%s', merge='%s'"
155 % (branch, remote, merge))
Simon Glass5f6a1c42012-12-15 10:42:07 +0000156
157
Simon Glass0157b182022-01-29 14:14:11 -0700158def get_range_in_branch(git_dir, branch, include_upstream=False):
Simon Glass5f6a1c42012-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 Glasscce717a2013-05-08 08:06:08 +0000166 access the commits. If the branch does not exist, returns None.
Simon Glass5f6a1c42012-12-15 10:42:07 +0000167 """
Simon Glass0157b182022-01-29 14:14:11 -0700168 upstream, msg = get_upstream(git_dir, branch)
Simon Glasscce717a2013-05-08 08:06:08 +0000169 if not upstream:
Simon Glass2a9e2c62014-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 Glass5f6a1c42012-12-15 10:42:07 +0000173
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500174
Simon Glass0157b182022-01-29 14:14:11 -0700175def count_commits_in_range(git_dir, range_expr):
Simon Glass5abab202014-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 Gustschinab4a6ab2019-10-27 17:55:04 +0100182 Number of patches that exist in the supplied range or None if none
Simon Glass5abab202014-12-01 17:33:57 -0700183 were found
184 """
Simon Glass0157b182022-01-29 14:14:11 -0700185 pipe = [log_cmd(range_expr, git_dir=git_dir, oneline=True)]
Simon Glassd9800692022-01-29 14:14:05 -0700186 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500187 raise_on_error=False)
Simon Glass5abab202014-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500193
Simon Glass0157b182022-01-29 14:14:11 -0700194def count_commits_in_branch(git_dir, branch, include_upstream=False):
Simon Glass5f6a1c42012-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 Glasscce717a2013-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 Glass5f6a1c42012-12-15 10:42:07 +0000203 """
Simon Glass0157b182022-01-29 14:14:11 -0700204 range_expr, msg = get_range_in_branch(git_dir, branch, include_upstream)
Simon Glasscce717a2013-05-08 08:06:08 +0000205 if not range_expr:
Simon Glass2a9e2c62014-12-01 17:33:54 -0700206 return None, msg
Simon Glass0157b182022-01-29 14:14:11 -0700207 return count_commits_in_range(git_dir, range_expr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000208
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500209
Simon Glass0157b182022-01-29 14:14:11 -0700210def count_commits(commit_range):
Simon Glass5f6a1c42012-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 Glass0157b182022-01-29 14:14:11 -0700218 pipe = [log_cmd(commit_range, oneline=True),
Simon Glass5f6a1c42012-12-15 10:42:07 +0000219 ['wc', '-l']]
Simon Glassd9800692022-01-29 14:14:05 -0700220 stdout = command.run_pipe(pipe, capture=True, oneline=True).stdout
Simon Glass5f6a1c42012-12-15 10:42:07 +0000221 patch_count = int(stdout)
222 return patch_count
223
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500224
Simon Glass0157b182022-01-29 14:14:11 -0700225def checkout(commit_hash, git_dir=None, work_tree=None, force=False):
Simon Glass5f6a1c42012-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 Glassd9800692022-01-29 14:14:05 -0700240 result = command.run_pipe([pipe], capture=True, raise_on_error=False,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500241 capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000242 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100243 raise OSError('git checkout (%s): %s' % (pipe, result.stderr))
Simon Glass5f6a1c42012-12-15 10:42:07 +0000244
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500245
Simon Glass0157b182022-01-29 14:14:11 -0700246def clone(git_dir, output_dir):
Simon Glass5f6a1c42012-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 Glassd9800692022-01-29 14:14:05 -0700253 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500254 capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000255 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100256 raise OSError('git clone: %s' % result.stderr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000257
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500258
Simon Glass0157b182022-01-29 14:14:11 -0700259def fetch(git_dir=None, work_tree=None):
Simon Glass5f6a1c42012-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 Glassd9800692022-01-29 14:14:05 -0700271 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000272 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100273 raise OSError('git fetch: %s' % result.stderr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000274
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500275
Simon Glass0157b182022-01-29 14:14:11 -0700276def check_worktree_is_available(git_dir):
Alper Nebi Yasak76de29f2020-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 Glassd9800692022-01-29 14:14:05 -0700286 result = command.run_pipe([pipe], capture=True, capture_stderr=True,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500287 raise_on_error=False)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300288 return result.return_code == 0
289
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500290
Simon Glass0157b182022-01-29 14:14:11 -0700291def add_worktree(git_dir, output_dir, commit_hash=None):
Alper Nebi Yasak76de29f2020-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 Glassd9800692022-01-29 14:14:05 -0700303 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500304 capture_stderr=True)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300305 if result.return_code != 0:
306 raise OSError('git worktree add: %s' % result.stderr)
307
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500308
Simon Glass0157b182022-01-29 14:14:11 -0700309def prune_worktrees(git_dir):
Alper Nebi Yasak76de29f2020-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 Glassd9800692022-01-29 14:14:05 -0700316 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300317 if result.return_code != 0:
318 raise OSError('git worktree prune: %s' % result.stderr)
319
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500320
321def create_patches(branch, start, count, ignore_binary, series, signoff=True):
Simon Glass0d24de92012-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 Glass262130f2020-07-05 21:41:51 -0600328 branch: Branch to create patches from (None for current branch)
Simon Glass0d24de92012-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 Glass7d5b04e2020-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 Glass0d24de92012-01-14 15:12:45 +0000333 Return:
Simon Glass7d5b04e2020-07-05 21:41:49 -0600334 Filename of cover letter (None if none)
Simon Glass0d24de92012-01-14 15:12:45 +0000335 List of filenames of patch files
336 """
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500337 cmd = ['git', 'format-patch', '-M']
Philipp Tomsichb3aff152020-11-24 18:14:52 +0100338 if signoff:
339 cmd.append('--signoff')
Bin Meng14aa35a2020-05-04 00:52:44 -0700340 if ignore_binary:
341 cmd.append('--no-binary')
Simon Glass0d24de92012-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 Glass262130f2020-07-05 21:41:51 -0600347 brname = branch or 'HEAD'
348 cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
Simon Glass0d24de92012-01-14 15:12:45 +0000349
Simon Glassd9800692022-01-29 14:14:05 -0700350 stdout = command.run_list(cmd)
Simon Glass0d24de92012-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500355 return files[0], files[1:]
Simon Glass0d24de92012-01-14 15:12:45 +0000356 else:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500357 return None, files
358
Simon Glass0d24de92012-01-14 15:12:45 +0000359
Simon Glass0157b182022-01-29 14:14:11 -0700360def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
Simon Glass0d24de92012-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 Glassa1318f72013-03-26 13:09:42 +0000373 alias: Alias dictionary
Simon Glass0fb560d2021-01-23 08:56:15 -0700374 warn_on_error: True to raise an error when an alias fails to match,
Simon Glassa1318f72013-03-26 13:09:42 +0000375 False to just print a message.
Simon Glass0d24de92012-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 Glass0157b182022-01-29 14:14:11 -0700386 >>> build_email_list(['john', 'mary'], None, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000387 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
Simon Glass0157b182022-01-29 14:14:11 -0700388 >>> build_email_list(['john', 'mary'], '--to', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000389 ['--to "j.bloggs@napier.co.nz"', \
390'--to "Mary Poppins <m.poppins@cloud.net>"']
Simon Glass0157b182022-01-29 14:14:11 -0700391 >>> build_email_list(['john', 'mary'], 'Cc', alias)
Simon Glass0d24de92012-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 Glass0157b182022-01-29 14:14:11 -0700397 raw += lookup_email(item, alias, warn_on_error=warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000398 result = []
399 for item in raw:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500400 if item not in result:
Simon Glass0d24de92012-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500406
Simon Glass0157b182022-01-29 14:14:11 -0700407def check_suppress_cc_config():
Nicolas Boichat94977562020-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500413 suppresscc = command.output_one_line(
414 'git', 'config', 'sendemail.suppresscc', raise_on_error=False)
Nicolas Boichat94977562020-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 Glass252ac582022-01-29 14:14:17 -0700420 print((col.build(col.RED, "error") +
Maxim Cournoyerf9e20e32022-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 Boichat94977562020-07-13 10:50:00 +0800429 return False
430
431 return True
432
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500433
Simon Glass0157b182022-01-29 14:14:11 -0700434def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500435 self_only=False, alias=None, in_reply_to=None, thread=False,
Maxim Cournoyer8c042fb2022-12-20 00:28:46 -0500436 smtp_server=None, get_maintainer_script=None):
Simon Glass0d24de92012-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 Glass0fb560d2021-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 Glass0d24de92012-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 Anderson6d819922013-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 Kulikowski27067a42016-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 Glassa60aedf2018-06-19 09:56:07 -0600452 smtp_server: SMTP server to use to send patches
Maxim Cournoyer8c042fb2022-12-20 00:28:46 -0500453 get_maintainer_script: File name of script to get maintainers emails
Simon Glass0d24de92012-01-14 15:12:45 +0000454
455 Returns:
456 Git command that was/would be run
457
Doug Andersona9700482012-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 Glass0d24de92012-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 Glass38a9d3b2020-06-07 06:45:47 -0600469 >>> series = {}
470 >>> series['to'] = ['fred']
471 >>> series['cc'] = ['mary']
Simon Glass0157b182022-01-29 14:14:11 -0700472 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glassa1318f72013-03-26 13:09:42 +0000473 False, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000474 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass46007672020-11-03 13:54:10 -0700475"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Simon Glass0157b182022-01-29 14:14:11 -0700476 >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \
Simon Glassa1318f72013-03-26 13:09:42 +0000477 alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000478 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass46007672020-11-03 13:54:10 -0700479"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
Simon Glass38a9d3b2020-06-07 06:45:47 -0600480 >>> series['cc'] = ['all']
Simon Glass0157b182022-01-29 14:14:11 -0700481 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glassa1318f72013-03-26 13:09:42 +0000482 True, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000483 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
Simon Glass46007672020-11-03 13:54:10 -0700484send --cc-cmd cc-fname" cover p1 p2'
Simon Glass0157b182022-01-29 14:14:11 -0700485 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glassa1318f72013-03-26 13:09:42 +0000486 False, alias)
Simon Glass0d24de92012-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 Glass46007672020-11-03 13:54:10 -0700489"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Doug Andersona9700482012-11-26 15:21:40 +0000490
491 # Restore argv[0] since we clobbered it.
492 >>> sys.argv[0] = _old_argv0
Simon Glass0d24de92012-01-14 15:12:45 +0000493 """
Simon Glass0157b182022-01-29 14:14:11 -0700494 to = build_email_list(series.get('to'), '--to', alias, warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000495 if not to:
Simon Glassd9800692022-01-29 14:14:05 -0700496 git_config_to = command.output('git', 'config', 'sendemail.to',
Simon Glass785f1542016-07-25 18:59:00 -0600497 raise_on_error=False)
Masahiro Yamadaee860c62014-07-18 14:23:20 +0900498 if not git_config_to:
Simon Glass5a1af1d2019-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 Yamadaee860c62014-07-18 14:23:20 +0900504 return
Simon Glass0157b182022-01-29 14:14:11 -0700505 cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))),
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500506 '--cc', alias, warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000507 if self_only:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500508 to = build_email_list([os.getenv('USER')], '--to',
509 alias, warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000510 cc = []
511 cmd = ['git', 'send-email', '--annotate']
Simon Glassa60aedf2018-06-19 09:56:07 -0600512 if smtp_server:
513 cmd.append('--smtp-server=%s' % smtp_server)
Doug Anderson6d819922013-03-17 10:31:04 +0000514 if in_reply_to:
Simon Glassfc0056e2020-11-08 20:36:18 -0700515 cmd.append('--in-reply-to="%s"' % in_reply_to)
Mateusz Kulikowski27067a42016-01-14 20:37:41 +0100516 if thread:
517 cmd.append('--thread')
Doug Anderson6d819922013-03-17 10:31:04 +0000518
Simon Glass0d24de92012-01-14 15:12:45 +0000519 cmd += to
520 cmd += cc
Simon Glass46007672020-11-03 13:54:10 -0700521 cmd += ['--cc-cmd', '"%s send --cc-cmd %s"' % (sys.argv[0], cc_fname)]
Simon Glass0d24de92012-01-14 15:12:45 +0000522 if cover_fname:
523 cmd.append(cover_fname)
524 cmd += args
Simon Glass2df3a012017-05-29 15:31:25 -0600525 cmdstr = ' '.join(cmd)
Simon Glass0d24de92012-01-14 15:12:45 +0000526 if not dry_run:
Simon Glass2df3a012017-05-29 15:31:25 -0600527 os.system(cmdstr)
528 return cmdstr
Simon Glass0d24de92012-01-14 15:12:45 +0000529
530
Simon Glass0157b182022-01-29 14:14:11 -0700531def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
Simon Glass0d24de92012-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 Glassa1318f72013-03-26 13:09:42 +0000538 alias: Dictionary containing aliases (None to use settings default)
Simon Glass0fb560d2021-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 Glass0d24de92012-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 Glass0157b182022-01-29 14:14:11 -0700558 >>> lookup_email('mary', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000559 ['m.poppins@cloud.net']
Simon Glass0157b182022-01-29 14:14:11 -0700560 >>> lookup_email('arthur.wellesley@howe.ro.uk', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000561 ['arthur.wellesley@howe.ro.uk']
Simon Glass0157b182022-01-29 14:14:11 -0700562 >>> lookup_email('boys', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000563 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
Simon Glass0157b182022-01-29 14:14:11 -0700564 >>> lookup_email('all', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000565 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass0157b182022-01-29 14:14:11 -0700566 >>> lookup_email('odd', alias)
Simon Glass0fb560d2021-01-23 08:56:15 -0700567 Alias 'odd' not found
568 []
Simon Glass0157b182022-01-29 14:14:11 -0700569 >>> lookup_email('loop', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000570 Traceback (most recent call last):
571 ...
572 OSError: Recursive email alias at 'other'
Simon Glass0157b182022-01-29 14:14:11 -0700573 >>> lookup_email('odd', alias, warn_on_error=False)
Simon Glassa1318f72013-03-26 13:09:42 +0000574 []
575 >>> # In this case the loop part will effectively be ignored.
Simon Glass0157b182022-01-29 14:14:11 -0700576 >>> lookup_email('loop', alias, warn_on_error=False)
Simon Glasse752edc2014-08-28 09:43:35 -0600577 Recursive email alias at 'other'
578 Recursive email alias at 'john'
579 Recursive email alias at 'mary'
Simon Glassa1318f72013-03-26 13:09:42 +0000580 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass0d24de92012-01-14 15:12:45 +0000581 """
582 if not alias:
583 alias = settings.alias
584 lookup_name = lookup_name.strip()
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500585 if '@' in lookup_name: # Perhaps a real email address
Simon Glass0d24de92012-01-14 15:12:45 +0000586 return [lookup_name]
587
588 lookup_name = lookup_name.lower()
Simon Glassa1318f72013-03-26 13:09:42 +0000589 col = terminal.Color()
Simon Glass0d24de92012-01-14 15:12:45 +0000590
591 out_list = []
Simon Glassa1318f72013-03-26 13:09:42 +0000592 if level > 10:
593 msg = "Recursive email alias at '%s'" % lookup_name
Simon Glass0fb560d2021-01-23 08:56:15 -0700594 if warn_on_error:
Paul Burtonac3fde92016-09-27 16:03:51 +0100595 raise OSError(msg)
Simon Glassa1318f72013-03-26 13:09:42 +0000596 else:
Simon Glass252ac582022-01-29 14:14:17 -0700597 print(col.build(col.RED, msg))
Simon Glassa1318f72013-03-26 13:09:42 +0000598 return out_list
599
Simon Glass0d24de92012-01-14 15:12:45 +0000600 if lookup_name:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500601 if lookup_name not in alias:
Simon Glassa1318f72013-03-26 13:09:42 +0000602 msg = "Alias '%s' not found" % lookup_name
Simon Glass0fb560d2021-01-23 08:56:15 -0700603 if warn_on_error:
Simon Glass252ac582022-01-29 14:14:17 -0700604 print(col.build(col.RED, msg))
Simon Glass0fb560d2021-01-23 08:56:15 -0700605 return out_list
Simon Glass0d24de92012-01-14 15:12:45 +0000606 for item in alias[lookup_name]:
Simon Glass0157b182022-01-29 14:14:11 -0700607 todo = lookup_email(item, alias, warn_on_error, level + 1)
Simon Glass0d24de92012-01-14 15:12:45 +0000608 for new_item in todo:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500609 if new_item not in out_list:
Simon Glass0d24de92012-01-14 15:12:45 +0000610 out_list.append(new_item)
611
Simon Glass0d24de92012-01-14 15:12:45 +0000612 return out_list
613
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500614
Simon Glass0157b182022-01-29 14:14:11 -0700615def get_top_level():
Simon Glass0d24de92012-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 Andersona9700482012-11-26 15:21:40 +0000623 >>> os.path.realpath(os.path.dirname(__file__)) == \
Simon Glass0157b182022-01-29 14:14:11 -0700624 os.path.join(get_top_level(), 'tools', 'patman')
Simon Glass0d24de92012-01-14 15:12:45 +0000625 True
626 """
Simon Glassd9800692022-01-29 14:14:05 -0700627 return command.output_one_line('git', 'rev-parse', '--show-toplevel')
Simon Glass0d24de92012-01-14 15:12:45 +0000628
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500629
Simon Glass0157b182022-01-29 14:14:11 -0700630def get_alias_file():
Simon Glass0d24de92012-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 Glassd9800692022-01-29 14:14:05 -0700636 fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile',
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500637 raise_on_error=False)
Brian Norrisdca79262022-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 Glass0157b182022-01-29 14:14:11 -0700645 return os.path.join(get_top_level(), fname)
Simon Glass0d24de92012-01-14 15:12:45 +0000646
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500647
Simon Glass0157b182022-01-29 14:14:11 -0700648def get_default_user_name():
Vikram Narayanan87d65552012-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 """
Simon Glassd9800692022-01-29 14:14:05 -0700654 uname = command.output_one_line('git', 'config', '--global', 'user.name')
Vikram Narayanan87d65552012-05-23 09:01:06 +0000655 return uname
656
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500657
Simon Glass0157b182022-01-29 14:14:11 -0700658def get_default_user_email():
Vikram Narayanan87d65552012-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 """
Simon Glassd9800692022-01-29 14:14:05 -0700664 uemail = command.output_one_line('git', 'config', '--global', 'user.email')
Vikram Narayanan87d65552012-05-23 09:01:06 +0000665 return uemail
666
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500667
Simon Glass0157b182022-01-29 14:14:11 -0700668def get_default_subject_prefix():
Wu, Josh3871cd82015-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500674 sub_prefix = command.output_one_line(
675 'git', 'config', 'format.subjectprefix', raise_on_error=False)
Wu, Josh3871cd82015-04-15 10:25:18 +0800676
677 return sub_prefix
678
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500679
Simon Glass0157b182022-01-29 14:14:11 -0700680def setup():
Simon Glass0d24de92012-01-14 15:12:45 +0000681 """Set up git utils, by reading the alias files."""
Simon Glass0d24de92012-01-14 15:12:45 +0000682 # Check for a git alias file also
Simon Glass0b703db2014-08-28 09:43:45 -0600683 global use_no_decorate
684
Simon Glass0157b182022-01-29 14:14:11 -0700685 alias_fname = get_alias_file()
Simon Glass0d24de92012-01-14 15:12:45 +0000686 if alias_fname:
687 settings.ReadGitAliases(alias_fname)
Simon Glass0157b182022-01-29 14:14:11 -0700688 cmd = log_cmd(None, count=0)
Simon Glassd9800692022-01-29 14:14:05 -0700689 use_no_decorate = (command.run_pipe([cmd], raise_on_error=False)
Simon Glasse49f14a2014-08-09 15:33:11 -0600690 .return_code == 0)
Simon Glass0d24de92012-01-14 15:12:45 +0000691
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500692
Simon Glass0157b182022-01-29 14:14:11 -0700693def get_head():
Simon Glass5f6a1c42012-12-15 10:42:07 +0000694 """Get the hash of the current HEAD
695
696 Returns:
697 Hash of HEAD
698 """
Simon Glassd9800692022-01-29 14:14:05 -0700699 return command.output_one_line('git', 'show', '-s', '--pretty=format:%H')
Simon Glass5f6a1c42012-12-15 10:42:07 +0000700
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500701
Simon Glass0d24de92012-01-14 15:12:45 +0000702if __name__ == "__main__":
703 import doctest
704
705 doctest.testmod()