blob: 5e3e98ac9a6a26611962c2dcdef8261675697d2c [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass26132882012-01-14 15:12:45 +00002# Copyright (c) 2011 The Chromium OS Authors.
3#
Simon Glass26132882012-01-14 15:12:45 +00004
Simon Glass26132882012-01-14 15:12:45 +00005import os
Simon Glass26132882012-01-14 15:12:45 +00006import sys
Simon Glass26132882012-01-14 15:12:45 +00007
Simon Glassa997ea52020-04-17 18:09:04 -06008from patman import settings
Simon Glass131444f2023-02-23 18:18:04 -07009from u_boot_pylib import command
10from u_boot_pylib import terminal
Simon Glass11aba512012-12-15 10:42:07 +000011
Simon Glass761648b2022-01-29 14:14:11 -070012# True to use --no-decorate - we check this in setup()
Simon Glass7ade94e2025-03-16 08:00:18 +000013USE_NO_DECORATE = True
Simon Glass6af913d2014-08-09 15:33:11 -060014
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:
Simon Glass7ade94e2025-03-16 08:00:18 +000021 commit_range (str): Range expression to use for log, None for none
22 git_dir (str): Path to git repository (None to use default)
23 oneline (bool): True to use --oneline, else False
24 reverse (bool): True to reverse the log (--reverse)
25 count (int or None): Number of commits to list, or None for no limit
Simon Glassb9dbcb42014-08-09 15:33:10 -060026 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 Glass7ade94e2025-03-16 08:00:18 +000035 if USE_NO_DECORATE:
Simon Glass6af913d2014-08-09 15:33:11 -060036 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:
Simon Glass7ade94e2025-03-16 08:00:18 +000040 cmd.append(f'-n{count}')
Simon Glassb9dbcb42014-08-09 15:33:10 -060041 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:
Simon Glass7ade94e2025-03-16 08:00:18 +000058 branch (str or None): Branch to count from (None for current branch)
Simon Glass2eb4da72020-07-05 21:41:51 -060059
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 Glass7ade94e2025-03-16 08:00:18 +000064 us, _ = get_upstream('.git', branch)
65 rev_range = f'{us}..{branch}'
Simon Glass2eb4da72020-07-05 21:41:51 -060066 else:
67 rev_range = '@{upstream}..'
Simon Glass51f55182025-02-03 09:26:45 -070068 cmd = log_cmd(rev_range, oneline=True)
69 result = command.run_one(*cmd, capture=True, capture_stderr=True,
70 oneline=True, raise_on_error=False)
Simon Glass1c1f2072020-10-29 21:46:34 -060071 if result.return_code:
Simon Glass7ade94e2025-03-16 08:00:18 +000072 raise ValueError(
73 f'Failed to determine upstream: {result.stderr.strip()}')
Simon Glass1c1f2072020-10-29 21:46:34 -060074 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:
Simon Glass7ade94e2025-03-16 08:00:18 +000082 commit_hash (str): Commit hash to look up
Simon Glassf204ab12014-12-01 17:33:54 -070083
84 Return:
85 Name of revision, if any, else None
86 """
Simon Glass51f55182025-02-03 09:26:45 -070087 stdout = command.output_one_line('git', 'name-rev', commit_hash)
Simon Glassf204ab12014-12-01 17:33:54 -070088
89 # We expect a commit, a space, then a revision name
90 name = stdout.split(' ')[1].strip()
91 return name
92
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050093
Simon Glass761648b2022-01-29 14:14:11 -070094def guess_upstream(git_dir, branch):
Simon Glassf204ab12014-12-01 17:33:54 -070095 """Tries to guess the upstream for a branch
96
97 This lists out top commits on a branch and tries to find a suitable
98 upstream. It does this by looking for the first commit where
99 'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
100
101 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000102 git_dir (str): Git directory containing repo
103 branch (str): Name of branch
Simon Glassf204ab12014-12-01 17:33:54 -0700104
105 Returns:
106 Tuple:
107 Name of upstream branch (e.g. 'upstream/master') or None if none
108 Warning/error message, or None if none
109 """
Simon Glass51f55182025-02-03 09:26:45 -0700110 cmd = log_cmd(branch, git_dir=git_dir, oneline=True, count=100)
111 result = command.run_one(*cmd, capture=True, capture_stderr=True,
112 raise_on_error=False)
Simon Glassf204ab12014-12-01 17:33:54 -0700113 if result.return_code:
Simon Glass7ade94e2025-03-16 08:00:18 +0000114 return None, f"Branch '{branch}' not found"
Simon Glassf204ab12014-12-01 17:33:54 -0700115 for line in result.stdout.splitlines()[1:]:
116 commit_hash = line.split(' ')[0]
Simon Glass761648b2022-01-29 14:14:11 -0700117 name = name_revision(commit_hash)
Simon Glassf204ab12014-12-01 17:33:54 -0700118 if '~' not in name and '^' not in name:
119 if name.startswith('remotes/'):
120 name = name[8:]
Simon Glass7ade94e2025-03-16 08:00:18 +0000121 return name, f"Guessing upstream as '{name}'"
122 return None, f"Cannot find a suitable upstream for branch '{branch}'"
Simon Glassf204ab12014-12-01 17:33:54 -0700123
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500124
Simon Glass761648b2022-01-29 14:14:11 -0700125def get_upstream(git_dir, branch):
Simon Glass11aba512012-12-15 10:42:07 +0000126 """Returns the name of the upstream for a branch
127
128 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000129 git_dir (str): Git directory containing repo
130 branch (str): Name of branch
Simon Glass11aba512012-12-15 10:42:07 +0000131
132 Returns:
Simon Glassf204ab12014-12-01 17:33:54 -0700133 Tuple:
134 Name of upstream branch (e.g. 'upstream/master') or None if none
135 Warning/error message, or None if none
Simon Glass11aba512012-12-15 10:42:07 +0000136 """
Simon Glassd2e95382013-05-08 08:06:08 +0000137 try:
Simon Glass840be732022-01-29 14:14:05 -0700138 remote = command.output_one_line('git', '--git-dir', git_dir, 'config',
Simon Glass7ade94e2025-03-16 08:00:18 +0000139 f'branch.{branch}.remote')
Simon Glass840be732022-01-29 14:14:05 -0700140 merge = command.output_one_line('git', '--git-dir', git_dir, 'config',
Simon Glass7ade94e2025-03-16 08:00:18 +0000141 f'branch.{branch}.merge')
Simon Glass82327692025-02-03 09:26:43 -0700142 except command.CommandExc:
Simon Glass761648b2022-01-29 14:14:11 -0700143 upstream, msg = guess_upstream(git_dir, branch)
Simon Glassf204ab12014-12-01 17:33:54 -0700144 return upstream, msg
Simon Glassd2e95382013-05-08 08:06:08 +0000145
Simon Glass11aba512012-12-15 10:42:07 +0000146 if remote == '.':
Simon Glass7e92f5c2015-01-29 11:35:16 -0700147 return merge, None
Simon Glass7ade94e2025-03-16 08:00:18 +0000148 if remote and merge:
Simon Glass978c1fb2023-10-30 10:22:30 -0700149 # Drop the initial refs/heads from merge
150 leaf = merge.split('/', maxsplit=2)[2:]
Simon Glass7ade94e2025-03-16 08:00:18 +0000151 return f'{remote}/{"/".join(leaf)}', None
152 raise ValueError("Cannot determine upstream branch for branch "
153 f"'{branch}' remote='{remote}', merge='{merge}'")
Simon Glass11aba512012-12-15 10:42:07 +0000154
155
Simon Glass761648b2022-01-29 14:14:11 -0700156def get_range_in_branch(git_dir, branch, include_upstream=False):
Simon Glass11aba512012-12-15 10:42:07 +0000157 """Returns an expression for the commits in the given branch.
158
159 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000160 git_dir (str): Directory containing git repo
161 branch (str): Name of branch
162 include_upstream (bool): Include the upstream commit as well
Simon Glass11aba512012-12-15 10:42:07 +0000163 Return:
164 Expression in the form 'upstream..branch' which can be used to
Simon Glassd2e95382013-05-08 08:06:08 +0000165 access the commits. If the branch does not exist, returns None.
Simon Glass11aba512012-12-15 10:42:07 +0000166 """
Simon Glass761648b2022-01-29 14:14:11 -0700167 upstream, msg = get_upstream(git_dir, branch)
Simon Glassd2e95382013-05-08 08:06:08 +0000168 if not upstream:
Simon Glassf204ab12014-12-01 17:33:54 -0700169 return None, msg
Simon Glass7ade94e2025-03-16 08:00:18 +0000170 rstr = f"{upstream}{'~' if include_upstream else ''}..{branch}"
Simon Glassf204ab12014-12-01 17:33:54 -0700171 return rstr, msg
Simon Glass11aba512012-12-15 10:42:07 +0000172
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500173
Simon Glass761648b2022-01-29 14:14:11 -0700174def count_commits_in_range(git_dir, range_expr):
Simon Glass5eeef462014-12-01 17:33:57 -0700175 """Returns the number of commits in the given range.
176
177 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000178 git_dir (str): Directory containing git repo
179 range_expr (str): Range to check
Simon Glass5eeef462014-12-01 17:33:57 -0700180 Return:
Anatolij Gustschinf2bcb322019-10-27 17:55:04 +0100181 Number of patches that exist in the supplied range or None if none
Simon Glass5eeef462014-12-01 17:33:57 -0700182 were found
183 """
Simon Glass51f55182025-02-03 09:26:45 -0700184 cmd = log_cmd(range_expr, git_dir=git_dir, oneline=True)
185 result = command.run_one(*cmd, capture=True, capture_stderr=True,
186 raise_on_error=False)
Simon Glass5eeef462014-12-01 17:33:57 -0700187 if result.return_code:
Simon Glass7ade94e2025-03-16 08:00:18 +0000188 return None, f"Range '{range_expr}' not found or is invalid"
Simon Glass5eeef462014-12-01 17:33:57 -0700189 patch_count = len(result.stdout.splitlines())
190 return patch_count, None
191
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500192
Simon Glass761648b2022-01-29 14:14:11 -0700193def count_commits_in_branch(git_dir, branch, include_upstream=False):
Simon Glass11aba512012-12-15 10:42:07 +0000194 """Returns the number of commits in the given branch.
195
196 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000197 git_dir (str): Directory containing git repo
198 branch (str): Name of branch
199 include_upstream (bool): Include the upstream commit as well
Simon Glass11aba512012-12-15 10:42:07 +0000200 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:
Simon Glass7ade94e2025-03-16 08:00:18 +0000214 commit_range (str): Range of commits to count (e.g. 'HEAD..base')
Simon Glass11aba512012-12-15 10:42:07 +0000215 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:
Simon Glass7ade94e2025-03-16 08:00:18 +0000229 commit_hash (str): Commit hash to check out
230 git_dir (str): Directory containing git repo, or None for current dir
231 work_tree (str): Git worktree to use, or None if none
232 force (bool): True to force the checkout (git checkout -f)
Simon Glass11aba512012-12-15 10:42:07 +0000233 """
234 pipe = ['git']
235 if git_dir:
236 pipe.extend(['--git-dir', git_dir])
237 if work_tree:
238 pipe.extend(['--work-tree', work_tree])
239 pipe.append('checkout')
240 if force:
241 pipe.append('-f')
242 pipe.append(commit_hash)
Simon Glass840be732022-01-29 14:14:05 -0700243 result = command.run_pipe([pipe], capture=True, raise_on_error=False,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500244 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000245 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000246 raise OSError(f'git checkout ({pipe}): {result.stderr}')
Simon Glass11aba512012-12-15 10:42:07 +0000247
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500248
Simon Glass7ade94e2025-03-16 08:00:18 +0000249def clone(repo, output_dir):
250 """Clone a repo
Simon Glass11aba512012-12-15 10:42:07 +0000251
252 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000253 repo (str): Repo to clone (e.g. web address)
254 output_dir (str): Directory to close into
Simon Glass11aba512012-12-15 10:42:07 +0000255 """
Simon Glass7ade94e2025-03-16 08:00:18 +0000256 result = command.run_one('git', 'clone', repo, '.', capture=True,
Simon Glass51f55182025-02-03 09:26:45 -0700257 cwd=output_dir, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000258 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000259 raise OSError(f'git clone: {result.stderr}')
Simon Glass11aba512012-12-15 10:42:07 +0000260
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500261
Simon Glass761648b2022-01-29 14:14:11 -0700262def fetch(git_dir=None, work_tree=None):
Simon Glass11aba512012-12-15 10:42:07 +0000263 """Fetch from the origin repo
264
265 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000266 git_dir (str): Directory containing git repo, or None for current dir
267 work_tree (str or None): Git worktree to use, or None if none
Simon Glass11aba512012-12-15 10:42:07 +0000268 """
Simon Glass51f55182025-02-03 09:26:45 -0700269 cmd = ['git']
Simon Glass11aba512012-12-15 10:42:07 +0000270 if git_dir:
Simon Glass51f55182025-02-03 09:26:45 -0700271 cmd.extend(['--git-dir', git_dir])
Simon Glass11aba512012-12-15 10:42:07 +0000272 if work_tree:
Simon Glass51f55182025-02-03 09:26:45 -0700273 cmd.extend(['--work-tree', work_tree])
274 cmd.append('fetch')
275 result = command.run_one(*cmd, capture=True, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000276 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000277 raise OSError(f'git fetch: {result.stderr}')
Simon Glass11aba512012-12-15 10:42:07 +0000278
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500279
Simon Glass761648b2022-01-29 14:14:11 -0700280def check_worktree_is_available(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300281 """Check if git-worktree functionality is available
282
283 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000284 git_dir (str): The repository to test in
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300285
286 Returns:
287 True if git-worktree commands will work, False otherwise.
288 """
Simon Glass51f55182025-02-03 09:26:45 -0700289 result = command.run_one('git', '--git-dir', git_dir, 'worktree', 'list',
290 capture=True, capture_stderr=True,
291 raise_on_error=False)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300292 return result.return_code == 0
293
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500294
Simon Glass761648b2022-01-29 14:14:11 -0700295def add_worktree(git_dir, output_dir, commit_hash=None):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300296 """Create and checkout a new git worktree for this build
297
298 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000299 git_dir (str): The repository to checkout the worktree from
300 output_dir (str): Path for the new worktree
301 commit_hash (str): Commit hash to checkout
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300302 """
303 # We need to pass --detach to avoid creating a new branch
Simon Glass51f55182025-02-03 09:26:45 -0700304 cmd = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach']
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300305 if commit_hash:
Simon Glass51f55182025-02-03 09:26:45 -0700306 cmd.append(commit_hash)
307 result = command.run_one(*cmd, capture=True, cwd=output_dir,
308 capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300309 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000310 raise OSError(f'git worktree add: {result.stderr}')
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300311
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500312
Simon Glass761648b2022-01-29 14:14:11 -0700313def prune_worktrees(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300314 """Remove administrative files for deleted worktrees
315
316 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000317 git_dir (str): The repository whose deleted worktrees should be pruned
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300318 """
Simon Glass51f55182025-02-03 09:26:45 -0700319 result = command.run_one('git', '--git-dir', git_dir, 'worktree', 'prune',
320 capture=True, capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300321 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000322 raise OSError(f'git worktree prune: {result.stderr}')
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300323
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500324
325def create_patches(branch, start, count, ignore_binary, series, signoff=True):
Simon Glass26132882012-01-14 15:12:45 +0000326 """Create a series of patches from the top of the current branch.
327
328 The patch files are written to the current directory using
329 git format-patch.
330
331 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000332 branch (str): Branch to create patches from (None for current branch)
333 start (int): Commit to start from: 0=HEAD, 1=next one, etc.
334 count (int): number of commits to include
335 ignore_binary (bool): Don't generate patches for binary files
336 series (Series): Series object for this series (set of patches)
337 signoff (bool): True to add signoff lines automatically
Simon Glass26132882012-01-14 15:12:45 +0000338 Return:
Simon Glass24725af2020-07-05 21:41:49 -0600339 Filename of cover letter (None if none)
Simon Glass26132882012-01-14 15:12:45 +0000340 List of filenames of patch files
341 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500342 cmd = ['git', 'format-patch', '-M']
Philipp Tomsich858531a2020-11-24 18:14:52 +0100343 if signoff:
344 cmd.append('--signoff')
Bin Menga04f1212020-05-04 00:52:44 -0700345 if ignore_binary:
346 cmd.append('--no-binary')
Simon Glass26132882012-01-14 15:12:45 +0000347 if series.get('cover'):
348 cmd.append('--cover-letter')
349 prefix = series.GetPatchPrefix()
350 if prefix:
Simon Glass7ade94e2025-03-16 08:00:18 +0000351 cmd += [f'--subject-prefix={prefix}']
Simon Glass2eb4da72020-07-05 21:41:51 -0600352 brname = branch or 'HEAD'
Simon Glass7ade94e2025-03-16 08:00:18 +0000353 cmd += [f'{brname}~{start + count}..{brname}~{start}']
Simon Glass26132882012-01-14 15:12:45 +0000354
Simon Glass840be732022-01-29 14:14:05 -0700355 stdout = command.run_list(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000356 files = stdout.splitlines()
357
358 # We have an extra file if there is a cover letter
359 if series.get('cover'):
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500360 return files[0], files[1:]
Simon Glass7ade94e2025-03-16 08:00:18 +0000361 return None, files
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500362
Simon Glass26132882012-01-14 15:12:45 +0000363
Simon Glass761648b2022-01-29 14:14:11 -0700364def build_email_list(in_list, tag=None, alias=None, warn_on_error=True):
Simon Glass26132882012-01-14 15:12:45 +0000365 """Build a list of email addresses based on an input list.
366
367 Takes a list of email addresses and aliases, and turns this into a list
368 of only email address, by resolving any aliases that are present.
369
370 If the tag is given, then each email address is prepended with this
371 tag and a space. If the tag starts with a minus sign (indicating a
372 command line parameter) then the email address is quoted.
373
374 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000375 in_list (list of str): List of aliases/email addresses
376 tag (str): Text to put before each address
377 alias (dict): Alias dictionary:
378 key: alias
379 value: list of aliases or email addresses
380 warn_on_error (bool): True to raise an error when an alias fails to
381 match, False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000382
383 Returns:
384 List of email addresses
385
386 >>> alias = {}
387 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
388 >>> alias['john'] = ['j.bloggs@napier.co.nz']
389 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
390 >>> alias['boys'] = ['fred', ' john']
391 >>> alias['all'] = ['fred ', 'john', ' mary ']
Simon Glass761648b2022-01-29 14:14:11 -0700392 >>> build_email_list(['john', 'mary'], None, alias)
Simon Glass26132882012-01-14 15:12:45 +0000393 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
Simon Glass761648b2022-01-29 14:14:11 -0700394 >>> build_email_list(['john', 'mary'], '--to', alias)
Simon Glass26132882012-01-14 15:12:45 +0000395 ['--to "j.bloggs@napier.co.nz"', \
396'--to "Mary Poppins <m.poppins@cloud.net>"']
Simon Glass761648b2022-01-29 14:14:11 -0700397 >>> build_email_list(['john', 'mary'], 'Cc', alias)
Simon Glass26132882012-01-14 15:12:45 +0000398 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
399 """
400 quote = '"' if tag and tag[0] == '-' else ''
401 raw = []
402 for item in in_list:
Simon Glass761648b2022-01-29 14:14:11 -0700403 raw += lookup_email(item, alias, warn_on_error=warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000404 result = []
405 for item in raw:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500406 if item not in result:
Simon Glass26132882012-01-14 15:12:45 +0000407 result.append(item)
408 if tag:
Simon Glass7ade94e2025-03-16 08:00:18 +0000409 return [f'{tag} {quote}{email}{quote}' for email in result]
Simon Glass26132882012-01-14 15:12:45 +0000410 return result
411
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500412
Simon Glass761648b2022-01-29 14:14:11 -0700413def check_suppress_cc_config():
Nicolas Boichat0da95742020-07-13 10:50:00 +0800414 """Check if sendemail.suppresscc is configured correctly.
415
416 Returns:
Simon Glass7ade94e2025-03-16 08:00:18 +0000417 bool: True if the option is configured correctly, False otherwise.
Nicolas Boichat0da95742020-07-13 10:50:00 +0800418 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500419 suppresscc = command.output_one_line(
420 'git', 'config', 'sendemail.suppresscc', raise_on_error=False)
Nicolas Boichat0da95742020-07-13 10:50:00 +0800421
422 # Other settings should be fine.
Simon Glass7ade94e2025-03-16 08:00:18 +0000423 if suppresscc in ('all', 'cccmd'):
Nicolas Boichat0da95742020-07-13 10:50:00 +0800424 col = terminal.Color()
425
Simon Glass7ade94e2025-03-16 08:00:18 +0000426 print(col.build(col.RED, 'error') +
427 f': git config sendemail.suppresscc set to {suppresscc}\n' +
428 ' patman needs --cc-cmd to be run to set the cc list.\n' +
429 ' Please run:\n' +
430 ' git config --unset sendemail.suppresscc\n' +
431 ' Or read the man page:\n' +
432 ' git send-email --help\n' +
433 ' and set an option that runs --cc-cmd\n')
Nicolas Boichat0da95742020-07-13 10:50:00 +0800434 return False
435
436 return True
437
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500438
Simon Glass761648b2022-01-29 14:14:11 -0700439def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500440 self_only=False, alias=None, in_reply_to=None, thread=False,
Simon Glass7ade94e2025-03-16 08:00:18 +0000441 smtp_server=None):
Simon Glass26132882012-01-14 15:12:45 +0000442 """Email a patch series.
443
444 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000445 series (Series): Series object containing destination info
446 cover_fname (str or None): filename of cover letter
447 args (list of str): list of filenames of patch files
448 dry_run (bool): Just return the command that would be run
449 warn_on_error (bool): True to print a warning when an alias fails to
450 match, False to ignore it.
451 cc_fname (str): Filename of Cc file for per-commit Cc
452 self_only (bool): True to just email to yourself as a test
453 alias (dict or None): Alias dictionary: (None to use settings default)
454 key: alias
455 value: list of aliases or email addresses
456 in_reply_to (str or None): If set we'll pass this to git as
457 --in-reply-to - should be a message ID that this is in reply to.
458 thread (bool): True to add --thread to git send-email (make
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100459 all patches reply to cover-letter or first patch in series)
Simon Glass7ade94e2025-03-16 08:00:18 +0000460 smtp_server (str or None): SMTP server to use to send patches
Simon Glass26132882012-01-14 15:12:45 +0000461
462 Returns:
463 Git command that was/would be run
464
Doug Anderson51d73212012-11-26 15:21:40 +0000465 # For the duration of this doctest pretend that we ran patman with ./patman
466 >>> _old_argv0 = sys.argv[0]
467 >>> sys.argv[0] = './patman'
468
Simon Glass26132882012-01-14 15:12:45 +0000469 >>> alias = {}
470 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
471 >>> alias['john'] = ['j.bloggs@napier.co.nz']
472 >>> alias['mary'] = ['m.poppins@cloud.net']
473 >>> alias['boys'] = ['fred', ' john']
474 >>> alias['all'] = ['fred ', 'john', ' mary ']
475 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
Simon Glass794c2592020-06-07 06:45:47 -0600476 >>> series = {}
477 >>> series['to'] = ['fred']
478 >>> series['cc'] = ['mary']
Simon Glass761648b2022-01-29 14:14:11 -0700479 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000480 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000481 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700482"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700483 >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \
Simon Glass12ea5f42013-03-26 13:09:42 +0000484 alias)
Simon Glass26132882012-01-14 15:12:45 +0000485 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700486"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
Simon Glass794c2592020-06-07 06:45:47 -0600487 >>> series['cc'] = ['all']
Simon Glass761648b2022-01-29 14:14:11 -0700488 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000489 True, alias)
Simon Glass26132882012-01-14 15:12:45 +0000490 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
Simon Glass1ee91c12020-11-03 13:54:10 -0700491send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700492 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000493 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000494 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
495"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700496"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Doug Anderson51d73212012-11-26 15:21:40 +0000497
498 # Restore argv[0] since we clobbered it.
499 >>> sys.argv[0] = _old_argv0
Simon Glass26132882012-01-14 15:12:45 +0000500 """
Simon Glass761648b2022-01-29 14:14:11 -0700501 to = build_email_list(series.get('to'), '--to', alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000502 if not to:
Simon Glass840be732022-01-29 14:14:05 -0700503 git_config_to = command.output('git', 'config', 'sendemail.to',
Simon Glassc55e0562016-07-25 18:59:00 -0600504 raise_on_error=False)
Masahiro Yamadad91f5b92014-07-18 14:23:20 +0900505 if not git_config_to:
Simon Glass23b8a192019-05-14 15:53:36 -0600506 print("No recipient.\n"
507 "Please add something like this to a commit\n"
508 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
509 "Or do something like this\n"
510 "git config sendemail.to u-boot@lists.denx.de")
Simon Glass7ade94e2025-03-16 08:00:18 +0000511 return None
Simon Glass761648b2022-01-29 14:14:11 -0700512 cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))),
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500513 '--cc', alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000514 if self_only:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500515 to = build_email_list([os.getenv('USER')], '--to',
516 alias, warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000517 cc = []
518 cmd = ['git', 'send-email', '--annotate']
Simon Glass8137e302018-06-19 09:56:07 -0600519 if smtp_server:
Simon Glass7ade94e2025-03-16 08:00:18 +0000520 cmd.append(f'--smtp-server={smtp_server}')
Doug Anderson06f27ac2013-03-17 10:31:04 +0000521 if in_reply_to:
Simon Glass7ade94e2025-03-16 08:00:18 +0000522 cmd.append(f'--in-reply-to="{in_reply_to}"')
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100523 if thread:
524 cmd.append('--thread')
Doug Anderson06f27ac2013-03-17 10:31:04 +0000525
Simon Glass26132882012-01-14 15:12:45 +0000526 cmd += to
527 cmd += cc
Simon Glass7ade94e2025-03-16 08:00:18 +0000528 cmd += ['--cc-cmd', f'"{sys.argv[0]} send --cc-cmd {cc_fname}"']
Simon Glass26132882012-01-14 15:12:45 +0000529 if cover_fname:
530 cmd.append(cover_fname)
531 cmd += args
Simon Glass47e308e2017-05-29 15:31:25 -0600532 cmdstr = ' '.join(cmd)
Simon Glass26132882012-01-14 15:12:45 +0000533 if not dry_run:
Simon Glass47e308e2017-05-29 15:31:25 -0600534 os.system(cmdstr)
535 return cmdstr
Simon Glass26132882012-01-14 15:12:45 +0000536
537
Simon Glass761648b2022-01-29 14:14:11 -0700538def lookup_email(lookup_name, alias=None, warn_on_error=True, level=0):
Simon Glass26132882012-01-14 15:12:45 +0000539 """If an email address is an alias, look it up and return the full name
540
541 TODO: Why not just use git's own alias feature?
542
543 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000544 lookup_name (str): Alias or email address to look up
545 alias (dict or None): Alias dictionary: (None to use settings default)
546 key: alias
547 value: list of aliases or email addresses
548 warn_on_error (bool): True to print a warning when an alias fails to
549 match, False to ignore it.
550 level (int): Depth of alias stack, used to detect recusion/loops
Simon Glass26132882012-01-14 15:12:45 +0000551
552 Returns:
553 tuple:
554 list containing a list of email addresses
555
556 Raises:
557 OSError if a recursive alias reference was found
558 ValueError if an alias was not found
559
560 >>> alias = {}
561 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
562 >>> alias['john'] = ['j.bloggs@napier.co.nz']
563 >>> alias['mary'] = ['m.poppins@cloud.net']
564 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
565 >>> alias['all'] = ['fred ', 'john', ' mary ']
566 >>> alias['loop'] = ['other', 'john', ' mary ']
567 >>> alias['other'] = ['loop', 'john', ' mary ']
Simon Glass761648b2022-01-29 14:14:11 -0700568 >>> lookup_email('mary', alias)
Simon Glass26132882012-01-14 15:12:45 +0000569 ['m.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700570 >>> lookup_email('arthur.wellesley@howe.ro.uk', alias)
Simon Glass26132882012-01-14 15:12:45 +0000571 ['arthur.wellesley@howe.ro.uk']
Simon Glass761648b2022-01-29 14:14:11 -0700572 >>> lookup_email('boys', alias)
Simon Glass26132882012-01-14 15:12:45 +0000573 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
Simon Glass761648b2022-01-29 14:14:11 -0700574 >>> lookup_email('all', alias)
Simon Glass26132882012-01-14 15:12:45 +0000575 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700576 >>> lookup_email('odd', alias)
Simon Glass1f975b92021-01-23 08:56:15 -0700577 Alias 'odd' not found
578 []
Simon Glass761648b2022-01-29 14:14:11 -0700579 >>> lookup_email('loop', alias)
Simon Glass26132882012-01-14 15:12:45 +0000580 Traceback (most recent call last):
581 ...
582 OSError: Recursive email alias at 'other'
Simon Glass761648b2022-01-29 14:14:11 -0700583 >>> lookup_email('odd', alias, warn_on_error=False)
Simon Glass12ea5f42013-03-26 13:09:42 +0000584 []
585 >>> # In this case the loop part will effectively be ignored.
Simon Glass761648b2022-01-29 14:14:11 -0700586 >>> lookup_email('loop', alias, warn_on_error=False)
Simon Glassb0cd3412014-08-28 09:43:35 -0600587 Recursive email alias at 'other'
588 Recursive email alias at 'john'
589 Recursive email alias at 'mary'
Simon Glass12ea5f42013-03-26 13:09:42 +0000590 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass26132882012-01-14 15:12:45 +0000591 """
592 if not alias:
593 alias = settings.alias
594 lookup_name = lookup_name.strip()
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500595 if '@' in lookup_name: # Perhaps a real email address
Simon Glass26132882012-01-14 15:12:45 +0000596 return [lookup_name]
597
598 lookup_name = lookup_name.lower()
Simon Glass12ea5f42013-03-26 13:09:42 +0000599 col = terminal.Color()
Simon Glass26132882012-01-14 15:12:45 +0000600
Simon Glass12ea5f42013-03-26 13:09:42 +0000601 out_list = []
Simon Glass26132882012-01-14 15:12:45 +0000602 if level > 10:
Simon Glass7ade94e2025-03-16 08:00:18 +0000603 msg = f"Recursive email alias at '{lookup_name}'"
Simon Glass1f975b92021-01-23 08:56:15 -0700604 if warn_on_error:
Paul Burtonf14a1312016-09-27 16:03:51 +0100605 raise OSError(msg)
Simon Glass7ade94e2025-03-16 08:00:18 +0000606 print(col.build(col.RED, msg))
607 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000608
Simon Glass26132882012-01-14 15:12:45 +0000609 if lookup_name:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500610 if lookup_name not in alias:
Simon Glass7ade94e2025-03-16 08:00:18 +0000611 msg = f"Alias '{lookup_name}' not found"
Simon Glass1f975b92021-01-23 08:56:15 -0700612 if warn_on_error:
Simon Glassf45d3742022-01-29 14:14:17 -0700613 print(col.build(col.RED, msg))
Simon Glass1f975b92021-01-23 08:56:15 -0700614 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000615 for item in alias[lookup_name]:
Simon Glass761648b2022-01-29 14:14:11 -0700616 todo = lookup_email(item, alias, warn_on_error, level + 1)
Simon Glass26132882012-01-14 15:12:45 +0000617 for new_item in todo:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500618 if new_item not in out_list:
Simon Glass26132882012-01-14 15:12:45 +0000619 out_list.append(new_item)
620
Simon Glass26132882012-01-14 15:12:45 +0000621 return out_list
622
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500623
Simon Glass761648b2022-01-29 14:14:11 -0700624def get_top_level():
Simon Glass26132882012-01-14 15:12:45 +0000625 """Return name of top-level directory for this git repo.
626
627 Returns:
Simon Glass7ade94e2025-03-16 08:00:18 +0000628 str: Full path to git top-level directory
Simon Glass26132882012-01-14 15:12:45 +0000629
630 This test makes sure that we are running tests in the right subdir
631
Doug Anderson51d73212012-11-26 15:21:40 +0000632 >>> os.path.realpath(os.path.dirname(__file__)) == \
Simon Glass761648b2022-01-29 14:14:11 -0700633 os.path.join(get_top_level(), 'tools', 'patman')
Simon Glass26132882012-01-14 15:12:45 +0000634 True
635 """
Simon Glass840be732022-01-29 14:14:05 -0700636 return command.output_one_line('git', 'rev-parse', '--show-toplevel')
Simon Glass26132882012-01-14 15:12:45 +0000637
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500638
Simon Glass761648b2022-01-29 14:14:11 -0700639def get_alias_file():
Simon Glass26132882012-01-14 15:12:45 +0000640 """Gets the name of the git alias file.
641
642 Returns:
Simon Glass7ade94e2025-03-16 08:00:18 +0000643 str: Filename of git alias file, or None if none
Simon Glass26132882012-01-14 15:12:45 +0000644 """
Simon Glass840be732022-01-29 14:14:05 -0700645 fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile',
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500646 raise_on_error=False)
Brian Norris94c775d2022-01-07 15:15:55 -0800647 if not fname:
648 return None
649
650 fname = os.path.expanduser(fname.strip())
651 if os.path.isabs(fname):
652 return fname
653
Simon Glass761648b2022-01-29 14:14:11 -0700654 return os.path.join(get_top_level(), fname)
Simon Glass26132882012-01-14 15:12:45 +0000655
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500656
Simon Glass761648b2022-01-29 14:14:11 -0700657def get_default_user_name():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000658 """Gets the user.name from .gitconfig file.
659
660 Returns:
661 User name found in .gitconfig file, or None if none
662 """
Simon Glass7ade94e2025-03-16 08:00:18 +0000663 uname = command.output_one_line('git', 'config', '--global', '--includes',
664 'user.name')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000665 return uname
666
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500667
Simon Glass761648b2022-01-29 14:14:11 -0700668def get_default_user_email():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000669 """Gets the user.email from the global .gitconfig file.
670
671 Returns:
672 User's email found in .gitconfig file, or None if none
673 """
Simon Glass7ade94e2025-03-16 08:00:18 +0000674 uemail = command.output_one_line('git', 'config', '--global', '--includes',
675 'user.email')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000676 return uemail
677
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500678
Simon Glass761648b2022-01-29 14:14:11 -0700679def get_default_subject_prefix():
Wu, Josh9873b912015-04-15 10:25:18 +0800680 """Gets the format.subjectprefix from local .git/config file.
681
682 Returns:
683 Subject prefix found in local .git/config file, or None if none
684 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500685 sub_prefix = command.output_one_line(
686 'git', 'config', 'format.subjectprefix', raise_on_error=False)
Wu, Josh9873b912015-04-15 10:25:18 +0800687
688 return sub_prefix
689
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500690
Simon Glass761648b2022-01-29 14:14:11 -0700691def setup():
Simon Glass7ade94e2025-03-16 08:00:18 +0000692 """setup() - Set up git utils, by reading the alias files."""
Simon Glass26132882012-01-14 15:12:45 +0000693 # Check for a git alias file also
Simon Glass7ade94e2025-03-16 08:00:18 +0000694 global USE_NO_DECORATE
Simon Glass81bcca82014-08-28 09:43:45 -0600695
Simon Glass761648b2022-01-29 14:14:11 -0700696 alias_fname = get_alias_file()
Simon Glass26132882012-01-14 15:12:45 +0000697 if alias_fname:
698 settings.ReadGitAliases(alias_fname)
Simon Glass761648b2022-01-29 14:14:11 -0700699 cmd = log_cmd(None, count=0)
Simon Glass7ade94e2025-03-16 08:00:18 +0000700 USE_NO_DECORATE = (command.run_one(*cmd, raise_on_error=False)
Simon Glass6af913d2014-08-09 15:33:11 -0600701 .return_code == 0)
Simon Glass26132882012-01-14 15:12:45 +0000702
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500703
Simon Glass761648b2022-01-29 14:14:11 -0700704def get_head():
Simon Glass11aba512012-12-15 10:42:07 +0000705 """Get the hash of the current HEAD
706
707 Returns:
708 Hash of HEAD
709 """
Simon Glass840be732022-01-29 14:14:05 -0700710 return command.output_one_line('git', 'show', '-s', '--pretty=format:%H')
Simon Glass11aba512012-12-15 10:42:07 +0000711
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500712
Simon Glass26132882012-01-14 15:12:45 +0000713if __name__ == "__main__":
714 import doctest
715
716 doctest.testmod()