blob: 10ea5ff39f5c3ac65d2278e366201d4ddbed5f54 [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 settings
Simon Glass4583c002023-02-23 18:18:04 -07009from u_boot_pylib import command
10from u_boot_pylib 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:
Simon Glassa44cb1f2023-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 Glass5f6a1c42012-12-15 10:42:07 +0000153 else:
Paul Burtonac3fde92016-09-27 16:03:51 +0100154 raise ValueError("Cannot determine upstream branch for branch "
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500155 "'%s' remote='%s', merge='%s'"
156 % (branch, remote, merge))
Simon Glass5f6a1c42012-12-15 10:42:07 +0000157
158
Simon Glass0157b182022-01-29 14:14:11 -0700159def get_range_in_branch(git_dir, branch, include_upstream=False):
Simon Glass5f6a1c42012-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 Glasscce717a2013-05-08 08:06:08 +0000167 access the commits. If the branch does not exist, returns None.
Simon Glass5f6a1c42012-12-15 10:42:07 +0000168 """
Simon Glass0157b182022-01-29 14:14:11 -0700169 upstream, msg = get_upstream(git_dir, branch)
Simon Glasscce717a2013-05-08 08:06:08 +0000170 if not upstream:
Simon Glass2a9e2c62014-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 Glass5f6a1c42012-12-15 10:42:07 +0000174
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500175
Simon Glass0157b182022-01-29 14:14:11 -0700176def count_commits_in_range(git_dir, range_expr):
Simon Glass5abab202014-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 Gustschinab4a6ab2019-10-27 17:55:04 +0100183 Number of patches that exist in the supplied range or None if none
Simon Glass5abab202014-12-01 17:33:57 -0700184 were found
185 """
Simon Glass0157b182022-01-29 14:14:11 -0700186 pipe = [log_cmd(range_expr, git_dir=git_dir, oneline=True)]
Simon Glassd9800692022-01-29 14:14:05 -0700187 result = command.run_pipe(pipe, capture=True, capture_stderr=True,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500188 raise_on_error=False)
Simon Glass5abab202014-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500194
Simon Glass0157b182022-01-29 14:14:11 -0700195def count_commits_in_branch(git_dir, branch, include_upstream=False):
Simon Glass5f6a1c42012-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 Glasscce717a2013-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 Glass5f6a1c42012-12-15 10:42:07 +0000204 """
Simon Glass0157b182022-01-29 14:14:11 -0700205 range_expr, msg = get_range_in_branch(git_dir, branch, include_upstream)
Simon Glasscce717a2013-05-08 08:06:08 +0000206 if not range_expr:
Simon Glass2a9e2c62014-12-01 17:33:54 -0700207 return None, msg
Simon Glass0157b182022-01-29 14:14:11 -0700208 return count_commits_in_range(git_dir, range_expr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000209
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500210
Simon Glass0157b182022-01-29 14:14:11 -0700211def count_commits(commit_range):
Simon Glass5f6a1c42012-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 Glass0157b182022-01-29 14:14:11 -0700219 pipe = [log_cmd(commit_range, oneline=True),
Simon Glass5f6a1c42012-12-15 10:42:07 +0000220 ['wc', '-l']]
Simon Glassd9800692022-01-29 14:14:05 -0700221 stdout = command.run_pipe(pipe, capture=True, oneline=True).stdout
Simon Glass5f6a1c42012-12-15 10:42:07 +0000222 patch_count = int(stdout)
223 return patch_count
224
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500225
Simon Glass0157b182022-01-29 14:14:11 -0700226def checkout(commit_hash, git_dir=None, work_tree=None, force=False):
Simon Glass5f6a1c42012-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 Glassd9800692022-01-29 14:14:05 -0700241 result = command.run_pipe([pipe], capture=True, raise_on_error=False,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500242 capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000243 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100244 raise OSError('git checkout (%s): %s' % (pipe, result.stderr))
Simon Glass5f6a1c42012-12-15 10:42:07 +0000245
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500246
Simon Glass0157b182022-01-29 14:14:11 -0700247def clone(git_dir, output_dir):
Simon Glass5f6a1c42012-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 Glassd9800692022-01-29 14:14:05 -0700254 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500255 capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000256 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100257 raise OSError('git clone: %s' % result.stderr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000258
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500259
Simon Glass0157b182022-01-29 14:14:11 -0700260def fetch(git_dir=None, work_tree=None):
Simon Glass5f6a1c42012-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 Glassd9800692022-01-29 14:14:05 -0700272 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000273 if result.return_code != 0:
Paul Burtonac3fde92016-09-27 16:03:51 +0100274 raise OSError('git fetch: %s' % result.stderr)
Simon Glass5f6a1c42012-12-15 10:42:07 +0000275
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500276
Simon Glass0157b182022-01-29 14:14:11 -0700277def check_worktree_is_available(git_dir):
Alper Nebi Yasak76de29f2020-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 Glassd9800692022-01-29 14:14:05 -0700287 result = command.run_pipe([pipe], capture=True, capture_stderr=True,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500288 raise_on_error=False)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300289 return result.return_code == 0
290
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500291
Simon Glass0157b182022-01-29 14:14:11 -0700292def add_worktree(git_dir, output_dir, commit_hash=None):
Alper Nebi Yasak76de29f2020-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 Glassd9800692022-01-29 14:14:05 -0700304 result = command.run_pipe([pipe], capture=True, cwd=output_dir,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500305 capture_stderr=True)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300306 if result.return_code != 0:
307 raise OSError('git worktree add: %s' % result.stderr)
308
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500309
Simon Glass0157b182022-01-29 14:14:11 -0700310def prune_worktrees(git_dir):
Alper Nebi Yasak76de29f2020-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 Glassd9800692022-01-29 14:14:05 -0700317 result = command.run_pipe([pipe], capture=True, capture_stderr=True)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +0300318 if result.return_code != 0:
319 raise OSError('git worktree prune: %s' % result.stderr)
320
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500321
322def create_patches(branch, start, count, ignore_binary, series, signoff=True):
Simon Glass0d24de92012-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 Glass262130f2020-07-05 21:41:51 -0600329 branch: Branch to create patches from (None for current branch)
Simon Glass0d24de92012-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 Glass7d5b04e2020-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 Glass0d24de92012-01-14 15:12:45 +0000334 Return:
Simon Glass7d5b04e2020-07-05 21:41:49 -0600335 Filename of cover letter (None if none)
Simon Glass0d24de92012-01-14 15:12:45 +0000336 List of filenames of patch files
337 """
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500338 cmd = ['git', 'format-patch', '-M']
Philipp Tomsichb3aff152020-11-24 18:14:52 +0100339 if signoff:
340 cmd.append('--signoff')
Bin Meng14aa35a2020-05-04 00:52:44 -0700341 if ignore_binary:
342 cmd.append('--no-binary')
Simon Glass0d24de92012-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 Glass262130f2020-07-05 21:41:51 -0600348 brname = branch or 'HEAD'
349 cmd += ['%s~%d..%s~%d' % (brname, start + count, brname, start)]
Simon Glass0d24de92012-01-14 15:12:45 +0000350
Simon Glassd9800692022-01-29 14:14:05 -0700351 stdout = command.run_list(cmd)
Simon Glass0d24de92012-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500356 return files[0], files[1:]
Simon Glass0d24de92012-01-14 15:12:45 +0000357 else:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500358 return None, files
359
Simon Glass0d24de92012-01-14 15:12:45 +0000360
Simon Glass0157b182022-01-29 14:14:11 -0700361def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
Simon Glass0d24de92012-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 Glassa1318f72013-03-26 13:09:42 +0000374 alias: Alias dictionary
Simon Glass0fb560d2021-01-23 08:56:15 -0700375 warn_on_error: True to raise an error when an alias fails to match,
Simon Glassa1318f72013-03-26 13:09:42 +0000376 False to just print a message.
Simon Glass0d24de92012-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 Glass0157b182022-01-29 14:14:11 -0700387 >>> build_email_list(['john', 'mary'], None, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000388 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
Simon Glass0157b182022-01-29 14:14:11 -0700389 >>> build_email_list(['john', 'mary'], '--to', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000390 ['--to "j.bloggs@napier.co.nz"', \
391'--to "Mary Poppins <m.poppins@cloud.net>"']
Simon Glass0157b182022-01-29 14:14:11 -0700392 >>> build_email_list(['john', 'mary'], 'Cc', alias)
Simon Glass0d24de92012-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 Glass0157b182022-01-29 14:14:11 -0700398 raw += lookup_email(item, alias, warn_on_error=warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000399 result = []
400 for item in raw:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500401 if item not in result:
Simon Glass0d24de92012-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500407
Simon Glass0157b182022-01-29 14:14:11 -0700408def check_suppress_cc_config():
Nicolas Boichat94977562020-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500414 suppresscc = command.output_one_line(
415 'git', 'config', 'sendemail.suppresscc', raise_on_error=False)
Nicolas Boichat94977562020-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 Glass252ac582022-01-29 14:14:17 -0700421 print((col.build(col.RED, "error") +
Maxim Cournoyerf9e20e32022-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 Boichat94977562020-07-13 10:50:00 +0800430 return False
431
432 return True
433
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500434
Simon Glass0157b182022-01-29 14:14:11 -0700435def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500436 self_only=False, alias=None, in_reply_to=None, thread=False,
Maxim Cournoyer8c042fb2022-12-20 00:28:46 -0500437 smtp_server=None, get_maintainer_script=None):
Simon Glass0d24de92012-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 Glass0fb560d2021-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 Glass0d24de92012-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 Anderson6d819922013-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 Kulikowski27067a42016-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 Glassa60aedf2018-06-19 09:56:07 -0600453 smtp_server: SMTP server to use to send patches
Maxim Cournoyer8c042fb2022-12-20 00:28:46 -0500454 get_maintainer_script: File name of script to get maintainers emails
Simon Glass0d24de92012-01-14 15:12:45 +0000455
456 Returns:
457 Git command that was/would be run
458
Doug Andersona9700482012-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 Glass0d24de92012-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 Glass38a9d3b2020-06-07 06:45:47 -0600470 >>> series = {}
471 >>> series['to'] = ['fred']
472 >>> series['cc'] = ['mary']
Simon Glass0157b182022-01-29 14:14:11 -0700473 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glassa1318f72013-03-26 13:09:42 +0000474 False, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000475 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass46007672020-11-03 13:54:10 -0700476"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Simon Glass0157b182022-01-29 14:14:11 -0700477 >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \
Simon Glassa1318f72013-03-26 13:09:42 +0000478 alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000479 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass46007672020-11-03 13:54:10 -0700480"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
Simon Glass38a9d3b2020-06-07 06:45:47 -0600481 >>> series['cc'] = ['all']
Simon Glass0157b182022-01-29 14:14:11 -0700482 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glassa1318f72013-03-26 13:09:42 +0000483 True, alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000484 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
Simon Glass46007672020-11-03 13:54:10 -0700485send --cc-cmd cc-fname" cover p1 p2'
Simon Glass0157b182022-01-29 14:14:11 -0700486 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glassa1318f72013-03-26 13:09:42 +0000487 False, alias)
Simon Glass0d24de92012-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 Glass46007672020-11-03 13:54:10 -0700490"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Doug Andersona9700482012-11-26 15:21:40 +0000491
492 # Restore argv[0] since we clobbered it.
493 >>> sys.argv[0] = _old_argv0
Simon Glass0d24de92012-01-14 15:12:45 +0000494 """
Simon Glass0157b182022-01-29 14:14:11 -0700495 to = build_email_list(series.get('to'), '--to', alias, warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000496 if not to:
Simon Glassd9800692022-01-29 14:14:05 -0700497 git_config_to = command.output('git', 'config', 'sendemail.to',
Simon Glass785f1542016-07-25 18:59:00 -0600498 raise_on_error=False)
Masahiro Yamadaee860c62014-07-18 14:23:20 +0900499 if not git_config_to:
Simon Glass5a1af1d2019-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 Yamadaee860c62014-07-18 14:23:20 +0900505 return
Simon Glass0157b182022-01-29 14:14:11 -0700506 cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))),
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500507 '--cc', alias, warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000508 if self_only:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500509 to = build_email_list([os.getenv('USER')], '--to',
510 alias, warn_on_error)
Simon Glass0d24de92012-01-14 15:12:45 +0000511 cc = []
512 cmd = ['git', 'send-email', '--annotate']
Simon Glassa60aedf2018-06-19 09:56:07 -0600513 if smtp_server:
514 cmd.append('--smtp-server=%s' % smtp_server)
Doug Anderson6d819922013-03-17 10:31:04 +0000515 if in_reply_to:
Simon Glassfc0056e2020-11-08 20:36:18 -0700516 cmd.append('--in-reply-to="%s"' % in_reply_to)
Mateusz Kulikowski27067a42016-01-14 20:37:41 +0100517 if thread:
518 cmd.append('--thread')
Doug Anderson6d819922013-03-17 10:31:04 +0000519
Simon Glass0d24de92012-01-14 15:12:45 +0000520 cmd += to
521 cmd += cc
Simon Glass46007672020-11-03 13:54:10 -0700522 cmd += ['--cc-cmd', '"%s send --cc-cmd %s"' % (sys.argv[0], cc_fname)]
Simon Glass0d24de92012-01-14 15:12:45 +0000523 if cover_fname:
524 cmd.append(cover_fname)
525 cmd += args
Simon Glass2df3a012017-05-29 15:31:25 -0600526 cmdstr = ' '.join(cmd)
Simon Glass0d24de92012-01-14 15:12:45 +0000527 if not dry_run:
Simon Glass2df3a012017-05-29 15:31:25 -0600528 os.system(cmdstr)
529 return cmdstr
Simon Glass0d24de92012-01-14 15:12:45 +0000530
531
Simon Glass0157b182022-01-29 14:14:11 -0700532def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
Simon Glass0d24de92012-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 Glassa1318f72013-03-26 13:09:42 +0000539 alias: Dictionary containing aliases (None to use settings default)
Simon Glass0fb560d2021-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 Glass0d24de92012-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 Glass0157b182022-01-29 14:14:11 -0700559 >>> lookup_email('mary', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000560 ['m.poppins@cloud.net']
Simon Glass0157b182022-01-29 14:14:11 -0700561 >>> lookup_email('arthur.wellesley@howe.ro.uk', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000562 ['arthur.wellesley@howe.ro.uk']
Simon Glass0157b182022-01-29 14:14:11 -0700563 >>> lookup_email('boys', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000564 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
Simon Glass0157b182022-01-29 14:14:11 -0700565 >>> lookup_email('all', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000566 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass0157b182022-01-29 14:14:11 -0700567 >>> lookup_email('odd', alias)
Simon Glass0fb560d2021-01-23 08:56:15 -0700568 Alias 'odd' not found
569 []
Simon Glass0157b182022-01-29 14:14:11 -0700570 >>> lookup_email('loop', alias)
Simon Glass0d24de92012-01-14 15:12:45 +0000571 Traceback (most recent call last):
572 ...
573 OSError: Recursive email alias at 'other'
Simon Glass0157b182022-01-29 14:14:11 -0700574 >>> lookup_email('odd', alias, warn_on_error=False)
Simon Glassa1318f72013-03-26 13:09:42 +0000575 []
576 >>> # In this case the loop part will effectively be ignored.
Simon Glass0157b182022-01-29 14:14:11 -0700577 >>> lookup_email('loop', alias, warn_on_error=False)
Simon Glasse752edc2014-08-28 09:43:35 -0600578 Recursive email alias at 'other'
579 Recursive email alias at 'john'
580 Recursive email alias at 'mary'
Simon Glassa1318f72013-03-26 13:09:42 +0000581 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass0d24de92012-01-14 15:12:45 +0000582 """
583 if not alias:
584 alias = settings.alias
585 lookup_name = lookup_name.strip()
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500586 if '@' in lookup_name: # Perhaps a real email address
Simon Glass0d24de92012-01-14 15:12:45 +0000587 return [lookup_name]
588
589 lookup_name = lookup_name.lower()
Simon Glassa1318f72013-03-26 13:09:42 +0000590 col = terminal.Color()
Simon Glass0d24de92012-01-14 15:12:45 +0000591
592 out_list = []
Simon Glassa1318f72013-03-26 13:09:42 +0000593 if level > 10:
594 msg = "Recursive email alias at '%s'" % lookup_name
Simon Glass0fb560d2021-01-23 08:56:15 -0700595 if warn_on_error:
Paul Burtonac3fde92016-09-27 16:03:51 +0100596 raise OSError(msg)
Simon Glassa1318f72013-03-26 13:09:42 +0000597 else:
Simon Glass252ac582022-01-29 14:14:17 -0700598 print(col.build(col.RED, msg))
Simon Glassa1318f72013-03-26 13:09:42 +0000599 return out_list
600
Simon Glass0d24de92012-01-14 15:12:45 +0000601 if lookup_name:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500602 if lookup_name not in alias:
Simon Glassa1318f72013-03-26 13:09:42 +0000603 msg = "Alias '%s' not found" % lookup_name
Simon Glass0fb560d2021-01-23 08:56:15 -0700604 if warn_on_error:
Simon Glass252ac582022-01-29 14:14:17 -0700605 print(col.build(col.RED, msg))
Simon Glass0fb560d2021-01-23 08:56:15 -0700606 return out_list
Simon Glass0d24de92012-01-14 15:12:45 +0000607 for item in alias[lookup_name]:
Simon Glass0157b182022-01-29 14:14:11 -0700608 todo = lookup_email(item, alias, warn_on_error, level + 1)
Simon Glass0d24de92012-01-14 15:12:45 +0000609 for new_item in todo:
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500610 if new_item not in out_list:
Simon Glass0d24de92012-01-14 15:12:45 +0000611 out_list.append(new_item)
612
Simon Glass0d24de92012-01-14 15:12:45 +0000613 return out_list
614
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500615
Simon Glass0157b182022-01-29 14:14:11 -0700616def get_top_level():
Simon Glass0d24de92012-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 Andersona9700482012-11-26 15:21:40 +0000624 >>> os.path.realpath(os.path.dirname(__file__)) == \
Simon Glass0157b182022-01-29 14:14:11 -0700625 os.path.join(get_top_level(), 'tools', 'patman')
Simon Glass0d24de92012-01-14 15:12:45 +0000626 True
627 """
Simon Glassd9800692022-01-29 14:14:05 -0700628 return command.output_one_line('git', 'rev-parse', '--show-toplevel')
Simon Glass0d24de92012-01-14 15:12:45 +0000629
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500630
Simon Glass0157b182022-01-29 14:14:11 -0700631def get_alias_file():
Simon Glass0d24de92012-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 Glassd9800692022-01-29 14:14:05 -0700637 fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile',
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500638 raise_on_error=False)
Brian Norrisdca79262022-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 Glass0157b182022-01-29 14:14:11 -0700646 return os.path.join(get_top_level(), fname)
Simon Glass0d24de92012-01-14 15:12:45 +0000647
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500648
Simon Glass0157b182022-01-29 14:14:11 -0700649def get_default_user_name():
Vikram Narayanan87d65552012-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 Shaoce8cb762023-09-08 22:15:59 +0800655 uname = command.output_one_line('git', 'config', '--global', '--includes', 'user.name')
Vikram Narayanan87d65552012-05-23 09:01:06 +0000656 return uname
657
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500658
Simon Glass0157b182022-01-29 14:14:11 -0700659def get_default_user_email():
Vikram Narayanan87d65552012-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 Shaoce8cb762023-09-08 22:15:59 +0800665 uemail = command.output_one_line('git', 'config', '--global', '--includes', 'user.email')
Vikram Narayanan87d65552012-05-23 09:01:06 +0000666 return uemail
667
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500668
Simon Glass0157b182022-01-29 14:14:11 -0700669def get_default_subject_prefix():
Wu, Josh3871cd82015-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 Cournoyerf9e20e32022-12-19 17:32:38 -0500675 sub_prefix = command.output_one_line(
676 'git', 'config', 'format.subjectprefix', raise_on_error=False)
Wu, Josh3871cd82015-04-15 10:25:18 +0800677
678 return sub_prefix
679
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500680
Simon Glass0157b182022-01-29 14:14:11 -0700681def setup():
Simon Glass0d24de92012-01-14 15:12:45 +0000682 """Set up git utils, by reading the alias files."""
Simon Glass0d24de92012-01-14 15:12:45 +0000683 # Check for a git alias file also
Simon Glass0b703db2014-08-28 09:43:45 -0600684 global use_no_decorate
685
Simon Glass0157b182022-01-29 14:14:11 -0700686 alias_fname = get_alias_file()
Simon Glass0d24de92012-01-14 15:12:45 +0000687 if alias_fname:
688 settings.ReadGitAliases(alias_fname)
Simon Glass0157b182022-01-29 14:14:11 -0700689 cmd = log_cmd(None, count=0)
Simon Glassd9800692022-01-29 14:14:05 -0700690 use_no_decorate = (command.run_pipe([cmd], raise_on_error=False)
Simon Glasse49f14a2014-08-09 15:33:11 -0600691 .return_code == 0)
Simon Glass0d24de92012-01-14 15:12:45 +0000692
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500693
Simon Glass0157b182022-01-29 14:14:11 -0700694def get_head():
Simon Glass5f6a1c42012-12-15 10:42:07 +0000695 """Get the hash of the current HEAD
696
697 Returns:
698 Hash of HEAD
699 """
Simon Glassd9800692022-01-29 14:14:05 -0700700 return command.output_one_line('git', 'show', '-s', '--pretty=format:%H')
Simon Glass5f6a1c42012-12-15 10:42:07 +0000701
Maxim Cournoyerf9e20e32022-12-19 17:32:38 -0500702
Simon Glass0d24de92012-01-14 15:12:45 +0000703if __name__ == "__main__":
704 import doctest
705
706 doctest.testmod()