blob: 7d001d03bed6f75297e9443ee5106256241243f6 [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 Glass12946012025-05-10 13:04:54 +02005"""Basic utilities for running the git command-line tool from Python"""
6
Simon Glass26132882012-01-14 15:12:45 +00007import os
Simon Glass26132882012-01-14 15:12:45 +00008import sys
Simon Glass26132882012-01-14 15:12:45 +00009
Simon Glass131444f2023-02-23 18:18:04 -070010from u_boot_pylib import command
11from u_boot_pylib import terminal
Simon Glass11aba512012-12-15 10:42:07 +000012
Simon Glass761648b2022-01-29 14:14:11 -070013# True to use --no-decorate - we check this in setup()
Simon Glass7ade94e2025-03-16 08:00:18 +000014USE_NO_DECORATE = True
Simon Glass6af913d2014-08-09 15:33:11 -060015
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050016
Simon Glass761648b2022-01-29 14:14:11 -070017def log_cmd(commit_range, git_dir=None, oneline=False, reverse=False,
Simon Glassb26edf22025-05-08 04:30:55 +020018 count=None, decorate=False):
Simon Glassb9dbcb42014-08-09 15:33:10 -060019 """Create a command to perform a 'git log'
20
21 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +000022 commit_range (str): Range expression to use for log, None for none
23 git_dir (str): Path to git repository (None to use default)
24 oneline (bool): True to use --oneline, else False
25 reverse (bool): True to reverse the log (--reverse)
26 count (int or None): Number of commits to list, or None for no limit
Simon Glass12946012025-05-10 13:04:54 +020027 decorate (bool): True to use --decorate
28
Simon Glassb9dbcb42014-08-09 15:33:10 -060029 Return:
30 List containing command and arguments to run
31 """
32 cmd = ['git']
33 if git_dir:
34 cmd += ['--git-dir', git_dir]
Simon Glass5f4e00d2014-08-28 09:43:37 -060035 cmd += ['--no-pager', 'log', '--no-color']
Simon Glassb9dbcb42014-08-09 15:33:10 -060036 if oneline:
37 cmd.append('--oneline')
Simon Glassb26edf22025-05-08 04:30:55 +020038 if USE_NO_DECORATE and not decorate:
Simon Glass6af913d2014-08-09 15:33:11 -060039 cmd.append('--no-decorate')
Simon Glassb26edf22025-05-08 04:30:55 +020040 if decorate:
41 cmd.append('--decorate')
Simon Glass299b9092014-08-14 21:59:11 -060042 if reverse:
43 cmd.append('--reverse')
Simon Glassb9dbcb42014-08-09 15:33:10 -060044 if count is not None:
Simon Glass7ade94e2025-03-16 08:00:18 +000045 cmd.append(f'-n{count}')
Simon Glassb9dbcb42014-08-09 15:33:10 -060046 if commit_range:
47 cmd.append(commit_range)
Simon Glass642e9a62016-03-12 18:50:31 -070048
49 # Add this in case we have a branch with the same name as a directory.
50 # This avoids messages like this, for example:
51 # fatal: ambiguous argument 'test': both revision and filename
52 cmd.append('--')
Simon Glassb9dbcb42014-08-09 15:33:10 -060053 return cmd
Simon Glass26132882012-01-14 15:12:45 +000054
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050055
Simon Glass1bc26b22025-05-08 04:22:18 +020056def count_commits_to_branch(branch, git_dir=None, end=None):
Simon Glass26132882012-01-14 15:12:45 +000057 """Returns number of commits between HEAD and the tracking branch.
58
59 This looks back to the tracking branch and works out the number of commits
60 since then.
61
Simon Glass2eb4da72020-07-05 21:41:51 -060062 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +000063 branch (str or None): Branch to count from (None for current branch)
Simon Glass1bc26b22025-05-08 04:22:18 +020064 git_dir (str): Path to git repository (None to use default)
65 end (str): End commit to stop before
Simon Glass2eb4da72020-07-05 21:41:51 -060066
Simon Glass26132882012-01-14 15:12:45 +000067 Return:
68 Number of patches that exist on top of the branch
69 """
Simon Glass1bc26b22025-05-08 04:22:18 +020070 if end:
71 rev_range = f'{end}..{branch}'
72 elif branch:
73 us, msg = get_upstream(git_dir or '.git', branch)
74 if not us:
75 raise ValueError(msg)
Simon Glass7ade94e2025-03-16 08:00:18 +000076 rev_range = f'{us}..{branch}'
Simon Glass2eb4da72020-07-05 21:41:51 -060077 else:
78 rev_range = '@{upstream}..'
Simon Glass1bc26b22025-05-08 04:22:18 +020079 cmd = log_cmd(rev_range, git_dir=git_dir, oneline=True)
Simon Glass51f55182025-02-03 09:26:45 -070080 result = command.run_one(*cmd, capture=True, capture_stderr=True,
81 oneline=True, raise_on_error=False)
Simon Glass1c1f2072020-10-29 21:46:34 -060082 if result.return_code:
Simon Glass7ade94e2025-03-16 08:00:18 +000083 raise ValueError(
84 f'Failed to determine upstream: {result.stderr.strip()}')
Simon Glass1c1f2072020-10-29 21:46:34 -060085 patch_count = len(result.stdout.splitlines())
Simon Glass26132882012-01-14 15:12:45 +000086 return patch_count
87
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -050088
Simon Glass761648b2022-01-29 14:14:11 -070089def name_revision(commit_hash):
Simon Glassf204ab12014-12-01 17:33:54 -070090 """Gets the revision name for a commit
91
92 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +000093 commit_hash (str): Commit hash to look up
Simon Glassf204ab12014-12-01 17:33:54 -070094
95 Return:
96 Name of revision, if any, else None
97 """
Simon Glass51f55182025-02-03 09:26:45 -070098 stdout = command.output_one_line('git', 'name-rev', commit_hash)
Simon Glassb26edf22025-05-08 04:30:55 +020099 if not stdout:
100 return None
Simon Glassf204ab12014-12-01 17:33:54 -0700101
102 # We expect a commit, a space, then a revision name
Simon Glassb26edf22025-05-08 04:30:55 +0200103 name = stdout.split()[1].strip()
Simon Glassf204ab12014-12-01 17:33:54 -0700104 return name
105
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500106
Simon Glass761648b2022-01-29 14:14:11 -0700107def guess_upstream(git_dir, branch):
Simon Glassf204ab12014-12-01 17:33:54 -0700108 """Tries to guess the upstream for a branch
109
110 This lists out top commits on a branch and tries to find a suitable
111 upstream. It does this by looking for the first commit where
112 'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
113
114 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000115 git_dir (str): Git directory containing repo
116 branch (str): Name of branch
Simon Glassf204ab12014-12-01 17:33:54 -0700117
118 Returns:
119 Tuple:
120 Name of upstream branch (e.g. 'upstream/master') or None if none
121 Warning/error message, or None if none
122 """
Simon Glassb26edf22025-05-08 04:30:55 +0200123 cmd = log_cmd(branch, git_dir=git_dir, oneline=True, count=100,
124 decorate=True)
Simon Glass51f55182025-02-03 09:26:45 -0700125 result = command.run_one(*cmd, capture=True, capture_stderr=True,
126 raise_on_error=False)
Simon Glassf204ab12014-12-01 17:33:54 -0700127 if result.return_code:
Simon Glass7ade94e2025-03-16 08:00:18 +0000128 return None, f"Branch '{branch}' not found"
Simon Glassf204ab12014-12-01 17:33:54 -0700129 for line in result.stdout.splitlines()[1:]:
Simon Glassb26edf22025-05-08 04:30:55 +0200130 parts = line.split(maxsplit=1)
131 if len(parts) >= 2 and parts[1].startswith('('):
132 commit_hash = parts[0]
133 name = name_revision(commit_hash)
134 if '~' not in name and '^' not in name:
135 if name.startswith('remotes/'):
136 name = name[8:]
137 return name, f"Guessing upstream as '{name}'"
Simon Glass7ade94e2025-03-16 08:00:18 +0000138 return None, f"Cannot find a suitable upstream for branch '{branch}'"
Simon Glassf204ab12014-12-01 17:33:54 -0700139
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500140
Simon Glass761648b2022-01-29 14:14:11 -0700141def get_upstream(git_dir, branch):
Simon Glass11aba512012-12-15 10:42:07 +0000142 """Returns the name of the upstream for a branch
143
144 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000145 git_dir (str): Git directory containing repo
146 branch (str): Name of branch
Simon Glass11aba512012-12-15 10:42:07 +0000147
148 Returns:
Simon Glassf204ab12014-12-01 17:33:54 -0700149 Tuple:
150 Name of upstream branch (e.g. 'upstream/master') or None if none
151 Warning/error message, or None if none
Simon Glass11aba512012-12-15 10:42:07 +0000152 """
Simon Glassd2e95382013-05-08 08:06:08 +0000153 try:
Simon Glass840be732022-01-29 14:14:05 -0700154 remote = command.output_one_line('git', '--git-dir', git_dir, 'config',
Simon Glass7ade94e2025-03-16 08:00:18 +0000155 f'branch.{branch}.remote')
Simon Glass840be732022-01-29 14:14:05 -0700156 merge = command.output_one_line('git', '--git-dir', git_dir, 'config',
Simon Glass7ade94e2025-03-16 08:00:18 +0000157 f'branch.{branch}.merge')
Simon Glass82327692025-02-03 09:26:43 -0700158 except command.CommandExc:
Simon Glass761648b2022-01-29 14:14:11 -0700159 upstream, msg = guess_upstream(git_dir, branch)
Simon Glassf204ab12014-12-01 17:33:54 -0700160 return upstream, msg
Simon Glassd2e95382013-05-08 08:06:08 +0000161
Simon Glass11aba512012-12-15 10:42:07 +0000162 if remote == '.':
Simon Glass7e92f5c2015-01-29 11:35:16 -0700163 return merge, None
Simon Glass7ade94e2025-03-16 08:00:18 +0000164 if remote and merge:
Simon Glass978c1fb2023-10-30 10:22:30 -0700165 # Drop the initial refs/heads from merge
166 leaf = merge.split('/', maxsplit=2)[2:]
Simon Glass7ade94e2025-03-16 08:00:18 +0000167 return f'{remote}/{"/".join(leaf)}', None
168 raise ValueError("Cannot determine upstream branch for branch "
169 f"'{branch}' remote='{remote}', merge='{merge}'")
Simon Glass11aba512012-12-15 10:42:07 +0000170
171
Simon Glass761648b2022-01-29 14:14:11 -0700172def get_range_in_branch(git_dir, branch, include_upstream=False):
Simon Glass11aba512012-12-15 10:42:07 +0000173 """Returns an expression for the commits in the given branch.
174
175 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000176 git_dir (str): Directory containing git repo
177 branch (str): Name of branch
178 include_upstream (bool): Include the upstream commit as well
Simon Glass11aba512012-12-15 10:42:07 +0000179 Return:
180 Expression in the form 'upstream..branch' which can be used to
Simon Glassd2e95382013-05-08 08:06:08 +0000181 access the commits. If the branch does not exist, returns None.
Simon Glass11aba512012-12-15 10:42:07 +0000182 """
Simon Glass761648b2022-01-29 14:14:11 -0700183 upstream, msg = get_upstream(git_dir, branch)
Simon Glassd2e95382013-05-08 08:06:08 +0000184 if not upstream:
Simon Glassf204ab12014-12-01 17:33:54 -0700185 return None, msg
Simon Glass7ade94e2025-03-16 08:00:18 +0000186 rstr = f"{upstream}{'~' if include_upstream else ''}..{branch}"
Simon Glassf204ab12014-12-01 17:33:54 -0700187 return rstr, msg
Simon Glass11aba512012-12-15 10:42:07 +0000188
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500189
Simon Glass761648b2022-01-29 14:14:11 -0700190def count_commits_in_range(git_dir, range_expr):
Simon Glass5eeef462014-12-01 17:33:57 -0700191 """Returns the number of commits in the given range.
192
193 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000194 git_dir (str): Directory containing git repo
195 range_expr (str): Range to check
Simon Glass5eeef462014-12-01 17:33:57 -0700196 Return:
Anatolij Gustschinf2bcb322019-10-27 17:55:04 +0100197 Number of patches that exist in the supplied range or None if none
Simon Glass5eeef462014-12-01 17:33:57 -0700198 were found
199 """
Simon Glass51f55182025-02-03 09:26:45 -0700200 cmd = log_cmd(range_expr, git_dir=git_dir, oneline=True)
201 result = command.run_one(*cmd, capture=True, capture_stderr=True,
202 raise_on_error=False)
Simon Glass5eeef462014-12-01 17:33:57 -0700203 if result.return_code:
Simon Glass7ade94e2025-03-16 08:00:18 +0000204 return None, f"Range '{range_expr}' not found or is invalid"
Simon Glass5eeef462014-12-01 17:33:57 -0700205 patch_count = len(result.stdout.splitlines())
206 return patch_count, None
207
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500208
Simon Glass761648b2022-01-29 14:14:11 -0700209def count_commits_in_branch(git_dir, branch, include_upstream=False):
Simon Glass11aba512012-12-15 10:42:07 +0000210 """Returns the number of commits in the given branch.
211
212 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000213 git_dir (str): Directory containing git repo
214 branch (str): Name of branch
215 include_upstream (bool): Include the upstream commit as well
Simon Glass11aba512012-12-15 10:42:07 +0000216 Return:
Simon Glassd2e95382013-05-08 08:06:08 +0000217 Number of patches that exist on top of the branch, or None if the
218 branch does not exist.
Simon Glass11aba512012-12-15 10:42:07 +0000219 """
Simon Glass761648b2022-01-29 14:14:11 -0700220 range_expr, msg = get_range_in_branch(git_dir, branch, include_upstream)
Simon Glassd2e95382013-05-08 08:06:08 +0000221 if not range_expr:
Simon Glassf204ab12014-12-01 17:33:54 -0700222 return None, msg
Simon Glass761648b2022-01-29 14:14:11 -0700223 return count_commits_in_range(git_dir, range_expr)
Simon Glass11aba512012-12-15 10:42:07 +0000224
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500225
Simon Glass761648b2022-01-29 14:14:11 -0700226def count_commits(commit_range):
Simon Glass11aba512012-12-15 10:42:07 +0000227 """Returns the number of commits in the given range.
228
229 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000230 commit_range (str): Range of commits to count (e.g. 'HEAD..base')
Simon Glass11aba512012-12-15 10:42:07 +0000231 Return:
232 Number of patches that exist on top of the branch
233 """
Simon Glass761648b2022-01-29 14:14:11 -0700234 pipe = [log_cmd(commit_range, oneline=True),
Simon Glass11aba512012-12-15 10:42:07 +0000235 ['wc', '-l']]
Simon Glass840be732022-01-29 14:14:05 -0700236 stdout = command.run_pipe(pipe, capture=True, oneline=True).stdout
Simon Glass11aba512012-12-15 10:42:07 +0000237 patch_count = int(stdout)
238 return patch_count
239
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500240
Simon Glass761648b2022-01-29 14:14:11 -0700241def checkout(commit_hash, git_dir=None, work_tree=None, force=False):
Simon Glass11aba512012-12-15 10:42:07 +0000242 """Checkout the selected commit for this build
243
244 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000245 commit_hash (str): Commit hash to check out
246 git_dir (str): Directory containing git repo, or None for current dir
247 work_tree (str): Git worktree to use, or None if none
248 force (bool): True to force the checkout (git checkout -f)
Simon Glass11aba512012-12-15 10:42:07 +0000249 """
250 pipe = ['git']
251 if git_dir:
252 pipe.extend(['--git-dir', git_dir])
253 if work_tree:
254 pipe.extend(['--work-tree', work_tree])
255 pipe.append('checkout')
256 if force:
257 pipe.append('-f')
258 pipe.append(commit_hash)
Simon Glass840be732022-01-29 14:14:05 -0700259 result = command.run_pipe([pipe], capture=True, raise_on_error=False,
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500260 capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000261 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000262 raise OSError(f'git checkout ({pipe}): {result.stderr}')
Simon Glass11aba512012-12-15 10:42:07 +0000263
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500264
Simon Glass7ade94e2025-03-16 08:00:18 +0000265def clone(repo, output_dir):
266 """Clone a repo
Simon Glass11aba512012-12-15 10:42:07 +0000267
268 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000269 repo (str): Repo to clone (e.g. web address)
270 output_dir (str): Directory to close into
Simon Glass11aba512012-12-15 10:42:07 +0000271 """
Simon Glass7ade94e2025-03-16 08:00:18 +0000272 result = command.run_one('git', 'clone', repo, '.', capture=True,
Simon Glass51f55182025-02-03 09:26:45 -0700273 cwd=output_dir, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000274 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000275 raise OSError(f'git clone: {result.stderr}')
Simon Glass11aba512012-12-15 10:42:07 +0000276
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500277
Simon Glass761648b2022-01-29 14:14:11 -0700278def fetch(git_dir=None, work_tree=None):
Simon Glass11aba512012-12-15 10:42:07 +0000279 """Fetch from the origin repo
280
281 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000282 git_dir (str): Directory containing git repo, or None for current dir
283 work_tree (str or None): Git worktree to use, or None if none
Simon Glass11aba512012-12-15 10:42:07 +0000284 """
Simon Glass51f55182025-02-03 09:26:45 -0700285 cmd = ['git']
Simon Glass11aba512012-12-15 10:42:07 +0000286 if git_dir:
Simon Glass51f55182025-02-03 09:26:45 -0700287 cmd.extend(['--git-dir', git_dir])
Simon Glass11aba512012-12-15 10:42:07 +0000288 if work_tree:
Simon Glass51f55182025-02-03 09:26:45 -0700289 cmd.extend(['--work-tree', work_tree])
290 cmd.append('fetch')
291 result = command.run_one(*cmd, capture=True, capture_stderr=True)
Simon Glass11aba512012-12-15 10:42:07 +0000292 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000293 raise OSError(f'git fetch: {result.stderr}')
Simon Glass11aba512012-12-15 10:42:07 +0000294
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500295
Simon Glass761648b2022-01-29 14:14:11 -0700296def check_worktree_is_available(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300297 """Check if git-worktree functionality is available
298
299 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000300 git_dir (str): The repository to test in
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300301
302 Returns:
303 True if git-worktree commands will work, False otherwise.
304 """
Simon Glass51f55182025-02-03 09:26:45 -0700305 result = command.run_one('git', '--git-dir', git_dir, 'worktree', 'list',
306 capture=True, capture_stderr=True,
307 raise_on_error=False)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300308 return result.return_code == 0
309
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500310
Simon Glass761648b2022-01-29 14:14:11 -0700311def add_worktree(git_dir, output_dir, commit_hash=None):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300312 """Create and checkout a new git worktree for this build
313
314 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000315 git_dir (str): The repository to checkout the worktree from
316 output_dir (str): Path for the new worktree
317 commit_hash (str): Commit hash to checkout
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300318 """
319 # We need to pass --detach to avoid creating a new branch
Simon Glass51f55182025-02-03 09:26:45 -0700320 cmd = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach']
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300321 if commit_hash:
Simon Glass51f55182025-02-03 09:26:45 -0700322 cmd.append(commit_hash)
323 result = command.run_one(*cmd, capture=True, cwd=output_dir,
324 capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300325 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000326 raise OSError(f'git worktree add: {result.stderr}')
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300327
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500328
Simon Glass761648b2022-01-29 14:14:11 -0700329def prune_worktrees(git_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300330 """Remove administrative files for deleted worktrees
331
332 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000333 git_dir (str): The repository whose deleted worktrees should be pruned
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300334 """
Simon Glass51f55182025-02-03 09:26:45 -0700335 result = command.run_one('git', '--git-dir', git_dir, 'worktree', 'prune',
336 capture=True, capture_stderr=True)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300337 if result.return_code != 0:
Simon Glass7ade94e2025-03-16 08:00:18 +0000338 raise OSError(f'git worktree prune: {result.stderr}')
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +0300339
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500340
Simon Glass1bc26b22025-05-08 04:22:18 +0200341def create_patches(branch, start, count, ignore_binary, series, signoff=True,
342 git_dir=None, cwd=None):
Simon Glass26132882012-01-14 15:12:45 +0000343 """Create a series of patches from the top of the current branch.
344
345 The patch files are written to the current directory using
346 git format-patch.
347
348 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000349 branch (str): Branch to create patches from (None for current branch)
350 start (int): Commit to start from: 0=HEAD, 1=next one, etc.
351 count (int): number of commits to include
352 ignore_binary (bool): Don't generate patches for binary files
353 series (Series): Series object for this series (set of patches)
354 signoff (bool): True to add signoff lines automatically
Simon Glass1bc26b22025-05-08 04:22:18 +0200355 git_dir (str): Path to git repository (None to use default)
356 cwd (str): Path to use for git operations
Simon Glass26132882012-01-14 15:12:45 +0000357 Return:
Simon Glass24725af2020-07-05 21:41:49 -0600358 Filename of cover letter (None if none)
Simon Glass26132882012-01-14 15:12:45 +0000359 List of filenames of patch files
360 """
Simon Glass1bc26b22025-05-08 04:22:18 +0200361 cmd = ['git']
362 if git_dir:
363 cmd += ['--git-dir', git_dir]
364 cmd += ['format-patch', '-M']
Philipp Tomsich858531a2020-11-24 18:14:52 +0100365 if signoff:
366 cmd.append('--signoff')
Bin Menga04f1212020-05-04 00:52:44 -0700367 if ignore_binary:
368 cmd.append('--no-binary')
Simon Glass26132882012-01-14 15:12:45 +0000369 if series.get('cover'):
370 cmd.append('--cover-letter')
371 prefix = series.GetPatchPrefix()
372 if prefix:
Simon Glass7ade94e2025-03-16 08:00:18 +0000373 cmd += [f'--subject-prefix={prefix}']
Simon Glass2eb4da72020-07-05 21:41:51 -0600374 brname = branch or 'HEAD'
Simon Glass7ade94e2025-03-16 08:00:18 +0000375 cmd += [f'{brname}~{start + count}..{brname}~{start}']
Simon Glass26132882012-01-14 15:12:45 +0000376
Simon Glass1bc26b22025-05-08 04:22:18 +0200377 stdout = command.run_list(cmd, cwd=cwd)
Simon Glass26132882012-01-14 15:12:45 +0000378 files = stdout.splitlines()
379
380 # We have an extra file if there is a cover letter
381 if series.get('cover'):
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500382 return files[0], files[1:]
Simon Glass7ade94e2025-03-16 08:00:18 +0000383 return None, files
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500384
Simon Glass26132882012-01-14 15:12:45 +0000385
Simon Glassdea1ddf2025-04-07 22:51:44 +1200386def build_email_list(in_list, alias, tag=None, warn_on_error=True):
Simon Glass26132882012-01-14 15:12:45 +0000387 """Build a list of email addresses based on an input list.
388
389 Takes a list of email addresses and aliases, and turns this into a list
390 of only email address, by resolving any aliases that are present.
391
392 If the tag is given, then each email address is prepended with this
393 tag and a space. If the tag starts with a minus sign (indicating a
394 command line parameter) then the email address is quoted.
395
396 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000397 in_list (list of str): List of aliases/email addresses
Simon Glass7ade94e2025-03-16 08:00:18 +0000398 alias (dict): Alias dictionary:
399 key: alias
400 value: list of aliases or email addresses
Simon Glassdea1ddf2025-04-07 22:51:44 +1200401 tag (str): Text to put before each address
Simon Glass7ade94e2025-03-16 08:00:18 +0000402 warn_on_error (bool): True to raise an error when an alias fails to
403 match, False to just print a message.
Simon Glass26132882012-01-14 15:12:45 +0000404
405 Returns:
406 List of email addresses
407
408 >>> alias = {}
409 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
410 >>> alias['john'] = ['j.bloggs@napier.co.nz']
411 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
412 >>> alias['boys'] = ['fred', ' john']
413 >>> alias['all'] = ['fred ', 'john', ' mary ']
Simon Glassdea1ddf2025-04-07 22:51:44 +1200414 >>> build_email_list(['john', 'mary'], alias, None)
Simon Glass26132882012-01-14 15:12:45 +0000415 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
Simon Glassdea1ddf2025-04-07 22:51:44 +1200416 >>> build_email_list(['john', 'mary'], alias, '--to')
Simon Glass26132882012-01-14 15:12:45 +0000417 ['--to "j.bloggs@napier.co.nz"', \
418'--to "Mary Poppins <m.poppins@cloud.net>"']
Simon Glassdea1ddf2025-04-07 22:51:44 +1200419 >>> build_email_list(['john', 'mary'], alias, 'Cc')
Simon Glass26132882012-01-14 15:12:45 +0000420 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
421 """
Simon Glass26132882012-01-14 15:12:45 +0000422 raw = []
423 for item in in_list:
Simon Glass761648b2022-01-29 14:14:11 -0700424 raw += lookup_email(item, alias, warn_on_error=warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000425 result = []
426 for item in raw:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500427 if item not in result:
Simon Glass26132882012-01-14 15:12:45 +0000428 result.append(item)
429 if tag:
Simon Glassa8ba0792025-05-08 04:38:30 +0200430 return [x for email in result for x in (tag, email)]
Simon Glass26132882012-01-14 15:12:45 +0000431 return result
432
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500433
Simon Glass761648b2022-01-29 14:14:11 -0700434def check_suppress_cc_config():
Nicolas Boichat0da95742020-07-13 10:50:00 +0800435 """Check if sendemail.suppresscc is configured correctly.
436
437 Returns:
Simon Glass7ade94e2025-03-16 08:00:18 +0000438 bool: True if the option is configured correctly, False otherwise.
Nicolas Boichat0da95742020-07-13 10:50:00 +0800439 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500440 suppresscc = command.output_one_line(
441 'git', 'config', 'sendemail.suppresscc', raise_on_error=False)
Nicolas Boichat0da95742020-07-13 10:50:00 +0800442
443 # Other settings should be fine.
Simon Glass7ade94e2025-03-16 08:00:18 +0000444 if suppresscc in ('all', 'cccmd'):
Nicolas Boichat0da95742020-07-13 10:50:00 +0800445 col = terminal.Color()
446
Simon Glass7ade94e2025-03-16 08:00:18 +0000447 print(col.build(col.RED, 'error') +
448 f': git config sendemail.suppresscc set to {suppresscc}\n' +
449 ' patman needs --cc-cmd to be run to set the cc list.\n' +
450 ' Please run:\n' +
451 ' git config --unset sendemail.suppresscc\n' +
452 ' Or read the man page:\n' +
453 ' git send-email --help\n' +
454 ' and set an option that runs --cc-cmd\n')
Nicolas Boichat0da95742020-07-13 10:50:00 +0800455 return False
456
457 return True
458
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500459
Simon Glass761648b2022-01-29 14:14:11 -0700460def email_patches(series, cover_fname, args, dry_run, warn_on_error, cc_fname,
Simon Glass5efa3662025-04-07 22:51:45 +1200461 alias, self_only=False, in_reply_to=None, thread=False,
Simon Glass1bc26b22025-05-08 04:22:18 +0200462 smtp_server=None, cwd=None):
Simon Glass26132882012-01-14 15:12:45 +0000463 """Email a patch series.
464
465 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000466 series (Series): Series object containing destination info
467 cover_fname (str or None): filename of cover letter
468 args (list of str): list of filenames of patch files
469 dry_run (bool): Just return the command that would be run
470 warn_on_error (bool): True to print a warning when an alias fails to
471 match, False to ignore it.
472 cc_fname (str): Filename of Cc file for per-commit Cc
Simon Glass5efa3662025-04-07 22:51:45 +1200473 alias (dict): Alias dictionary:
Simon Glass7ade94e2025-03-16 08:00:18 +0000474 key: alias
475 value: list of aliases or email addresses
Simon Glass5efa3662025-04-07 22:51:45 +1200476 self_only (bool): True to just email to yourself as a test
Simon Glass7ade94e2025-03-16 08:00:18 +0000477 in_reply_to (str or None): If set we'll pass this to git as
478 --in-reply-to - should be a message ID that this is in reply to.
479 thread (bool): True to add --thread to git send-email (make
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100480 all patches reply to cover-letter or first patch in series)
Simon Glass7ade94e2025-03-16 08:00:18 +0000481 smtp_server (str or None): SMTP server to use to send patches
Simon Glass1bc26b22025-05-08 04:22:18 +0200482 cwd (str): Path to use for patch files (None to use current dir)
Simon Glass26132882012-01-14 15:12:45 +0000483
484 Returns:
485 Git command that was/would be run
486
Doug Anderson51d73212012-11-26 15:21:40 +0000487 # For the duration of this doctest pretend that we ran patman with ./patman
488 >>> _old_argv0 = sys.argv[0]
489 >>> sys.argv[0] = './patman'
490
Simon Glass26132882012-01-14 15:12:45 +0000491 >>> alias = {}
492 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
493 >>> alias['john'] = ['j.bloggs@napier.co.nz']
494 >>> alias['mary'] = ['m.poppins@cloud.net']
495 >>> alias['boys'] = ['fred', ' john']
496 >>> alias['all'] = ['fred ', 'john', ' mary ']
497 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
Simon Glass794c2592020-06-07 06:45:47 -0600498 >>> series = {}
499 >>> series['to'] = ['fred']
500 >>> series['cc'] = ['mary']
Simon Glass761648b2022-01-29 14:14:11 -0700501 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000502 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000503 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700504"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700505 >>> email_patches(series, None, ['p1'], True, True, 'cc-fname', False, \
Simon Glass12ea5f42013-03-26 13:09:42 +0000506 alias)
Simon Glass26132882012-01-14 15:12:45 +0000507 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700508"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" p1'
Simon Glass794c2592020-06-07 06:45:47 -0600509 >>> series['cc'] = ['all']
Simon Glass761648b2022-01-29 14:14:11 -0700510 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000511 True, alias)
Simon Glass26132882012-01-14 15:12:45 +0000512 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
Simon Glass1ee91c12020-11-03 13:54:10 -0700513send --cc-cmd cc-fname" cover p1 p2'
Simon Glass761648b2022-01-29 14:14:11 -0700514 >>> email_patches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
Simon Glass12ea5f42013-03-26 13:09:42 +0000515 False, alias)
Simon Glass26132882012-01-14 15:12:45 +0000516 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
517"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
Simon Glass1ee91c12020-11-03 13:54:10 -0700518"m.poppins@cloud.net" --cc-cmd "./patman send --cc-cmd cc-fname" cover p1 p2'
Doug Anderson51d73212012-11-26 15:21:40 +0000519
520 # Restore argv[0] since we clobbered it.
521 >>> sys.argv[0] = _old_argv0
Simon Glass26132882012-01-14 15:12:45 +0000522 """
Simon Glass5efa3662025-04-07 22:51:45 +1200523 to = build_email_list(series.get('to'), alias, '--to', warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000524 if not to:
Simon Glass12946012025-05-10 13:04:54 +0200525 if not command.output('git', 'config', 'sendemail.to',
526 raise_on_error=False):
Simon Glass23b8a192019-05-14 15:53:36 -0600527 print("No recipient.\n"
528 "Please add something like this to a commit\n"
529 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>\n"
530 "Or do something like this\n"
531 "git config sendemail.to u-boot@lists.denx.de")
Simon Glass7ade94e2025-03-16 08:00:18 +0000532 return None
Simon Glass761648b2022-01-29 14:14:11 -0700533 cc = build_email_list(list(set(series.get('cc')) - set(series.get('to'))),
Simon Glass5efa3662025-04-07 22:51:45 +1200534 alias, '--cc', warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000535 if self_only:
Simon Glass5efa3662025-04-07 22:51:45 +1200536 to = build_email_list([os.getenv('USER')], '--to', alias,
Simon Glassdea1ddf2025-04-07 22:51:44 +1200537 warn_on_error)
Simon Glass26132882012-01-14 15:12:45 +0000538 cc = []
539 cmd = ['git', 'send-email', '--annotate']
Simon Glass8137e302018-06-19 09:56:07 -0600540 if smtp_server:
Simon Glass7ade94e2025-03-16 08:00:18 +0000541 cmd.append(f'--smtp-server={smtp_server}')
Doug Anderson06f27ac2013-03-17 10:31:04 +0000542 if in_reply_to:
Simon Glass7ade94e2025-03-16 08:00:18 +0000543 cmd.append(f'--in-reply-to="{in_reply_to}"')
Mateusz Kulikowski80c2ebc2016-01-14 20:37:41 +0100544 if thread:
545 cmd.append('--thread')
Doug Anderson06f27ac2013-03-17 10:31:04 +0000546
Simon Glass26132882012-01-14 15:12:45 +0000547 cmd += to
548 cmd += cc
Simon Glassa8ba0792025-05-08 04:38:30 +0200549 cmd += ['--cc-cmd', f'{sys.argv[0]} send --cc-cmd {cc_fname}']
Simon Glass26132882012-01-14 15:12:45 +0000550 if cover_fname:
551 cmd.append(cover_fname)
552 cmd += args
Simon Glass26132882012-01-14 15:12:45 +0000553 if not dry_run:
Simon Glass1bc26b22025-05-08 04:22:18 +0200554 command.run(*cmd, capture=False, capture_stderr=False, cwd=cwd)
Simon Glass12946012025-05-10 13:04:54 +0200555 return' '.join([f'"{x}"' if ' ' in x and '"' not in x else x
556 for x in cmd])
Simon Glass26132882012-01-14 15:12:45 +0000557
558
Simon Glass5efa3662025-04-07 22:51:45 +1200559def lookup_email(lookup_name, alias, warn_on_error=True, level=0):
Simon Glass26132882012-01-14 15:12:45 +0000560 """If an email address is an alias, look it up and return the full name
561
562 TODO: Why not just use git's own alias feature?
563
564 Args:
Simon Glass7ade94e2025-03-16 08:00:18 +0000565 lookup_name (str): Alias or email address to look up
Simon Glass5efa3662025-04-07 22:51:45 +1200566 alias (dict): Alias dictionary
Simon Glass7ade94e2025-03-16 08:00:18 +0000567 key: alias
568 value: list of aliases or email addresses
569 warn_on_error (bool): True to print a warning when an alias fails to
570 match, False to ignore it.
571 level (int): Depth of alias stack, used to detect recusion/loops
Simon Glass26132882012-01-14 15:12:45 +0000572
573 Returns:
574 tuple:
575 list containing a list of email addresses
576
577 Raises:
578 OSError if a recursive alias reference was found
579 ValueError if an alias was not found
580
581 >>> alias = {}
582 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
583 >>> alias['john'] = ['j.bloggs@napier.co.nz']
584 >>> alias['mary'] = ['m.poppins@cloud.net']
585 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
586 >>> alias['all'] = ['fred ', 'john', ' mary ']
587 >>> alias['loop'] = ['other', 'john', ' mary ']
588 >>> alias['other'] = ['loop', 'john', ' mary ']
Simon Glass761648b2022-01-29 14:14:11 -0700589 >>> lookup_email('mary', alias)
Simon Glass26132882012-01-14 15:12:45 +0000590 ['m.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700591 >>> lookup_email('arthur.wellesley@howe.ro.uk', alias)
Simon Glass26132882012-01-14 15:12:45 +0000592 ['arthur.wellesley@howe.ro.uk']
Simon Glass761648b2022-01-29 14:14:11 -0700593 >>> lookup_email('boys', alias)
Simon Glass26132882012-01-14 15:12:45 +0000594 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
Simon Glass761648b2022-01-29 14:14:11 -0700595 >>> lookup_email('all', alias)
Simon Glass26132882012-01-14 15:12:45 +0000596 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass761648b2022-01-29 14:14:11 -0700597 >>> lookup_email('odd', alias)
Simon Glass1f975b92021-01-23 08:56:15 -0700598 Alias 'odd' not found
599 []
Simon Glass761648b2022-01-29 14:14:11 -0700600 >>> lookup_email('loop', alias)
Simon Glass26132882012-01-14 15:12:45 +0000601 Traceback (most recent call last):
602 ...
603 OSError: Recursive email alias at 'other'
Simon Glass761648b2022-01-29 14:14:11 -0700604 >>> lookup_email('odd', alias, warn_on_error=False)
Simon Glass12ea5f42013-03-26 13:09:42 +0000605 []
606 >>> # In this case the loop part will effectively be ignored.
Simon Glass761648b2022-01-29 14:14:11 -0700607 >>> lookup_email('loop', alias, warn_on_error=False)
Simon Glassb0cd3412014-08-28 09:43:35 -0600608 Recursive email alias at 'other'
609 Recursive email alias at 'john'
610 Recursive email alias at 'mary'
Simon Glass12ea5f42013-03-26 13:09:42 +0000611 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
Simon Glass26132882012-01-14 15:12:45 +0000612 """
Simon Glass26132882012-01-14 15:12:45 +0000613 lookup_name = lookup_name.strip()
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500614 if '@' in lookup_name: # Perhaps a real email address
Simon Glass26132882012-01-14 15:12:45 +0000615 return [lookup_name]
616
617 lookup_name = lookup_name.lower()
Simon Glass12ea5f42013-03-26 13:09:42 +0000618 col = terminal.Color()
Simon Glass26132882012-01-14 15:12:45 +0000619
Simon Glass12ea5f42013-03-26 13:09:42 +0000620 out_list = []
Simon Glass26132882012-01-14 15:12:45 +0000621 if level > 10:
Simon Glass7ade94e2025-03-16 08:00:18 +0000622 msg = f"Recursive email alias at '{lookup_name}'"
Simon Glass1f975b92021-01-23 08:56:15 -0700623 if warn_on_error:
Paul Burtonf14a1312016-09-27 16:03:51 +0100624 raise OSError(msg)
Simon Glass7ade94e2025-03-16 08:00:18 +0000625 print(col.build(col.RED, msg))
626 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000627
Simon Glass26132882012-01-14 15:12:45 +0000628 if lookup_name:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500629 if lookup_name not in alias:
Simon Glass7ade94e2025-03-16 08:00:18 +0000630 msg = f"Alias '{lookup_name}' not found"
Simon Glass1f975b92021-01-23 08:56:15 -0700631 if warn_on_error:
Simon Glassf45d3742022-01-29 14:14:17 -0700632 print(col.build(col.RED, msg))
Simon Glass1f975b92021-01-23 08:56:15 -0700633 return out_list
Simon Glass26132882012-01-14 15:12:45 +0000634 for item in alias[lookup_name]:
Simon Glass761648b2022-01-29 14:14:11 -0700635 todo = lookup_email(item, alias, warn_on_error, level + 1)
Simon Glass26132882012-01-14 15:12:45 +0000636 for new_item in todo:
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500637 if new_item not in out_list:
Simon Glass26132882012-01-14 15:12:45 +0000638 out_list.append(new_item)
639
Simon Glass26132882012-01-14 15:12:45 +0000640 return out_list
641
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500642
Simon Glass761648b2022-01-29 14:14:11 -0700643def get_top_level():
Simon Glass26132882012-01-14 15:12:45 +0000644 """Return name of top-level directory for this git repo.
645
646 Returns:
Simon Glass0e43cad2025-05-10 13:04:55 +0200647 str: Full path to git top-level directory, or None if not found
Simon Glass26132882012-01-14 15:12:45 +0000648
649 This test makes sure that we are running tests in the right subdir
650
Doug Anderson51d73212012-11-26 15:21:40 +0000651 >>> os.path.realpath(os.path.dirname(__file__)) == \
Simon Glass761648b2022-01-29 14:14:11 -0700652 os.path.join(get_top_level(), 'tools', 'patman')
Simon Glass26132882012-01-14 15:12:45 +0000653 True
654 """
Simon Glass0e43cad2025-05-10 13:04:55 +0200655 result = command.run_one(
656 'git', 'rev-parse', '--show-toplevel', oneline=True, capture=True,
657 capture_stderr=True, raise_on_error=False)
658 if result.return_code:
659 return None
660 return result.stdout.strip()
Simon Glass26132882012-01-14 15:12:45 +0000661
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500662
Simon Glass761648b2022-01-29 14:14:11 -0700663def get_alias_file():
Simon Glass26132882012-01-14 15:12:45 +0000664 """Gets the name of the git alias file.
665
666 Returns:
Simon Glass7ade94e2025-03-16 08:00:18 +0000667 str: Filename of git alias file, or None if none
Simon Glass26132882012-01-14 15:12:45 +0000668 """
Simon Glass840be732022-01-29 14:14:05 -0700669 fname = command.output_one_line('git', 'config', 'sendemail.aliasesfile',
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500670 raise_on_error=False)
Brian Norris94c775d2022-01-07 15:15:55 -0800671 if not fname:
672 return None
673
674 fname = os.path.expanduser(fname.strip())
675 if os.path.isabs(fname):
676 return fname
677
Simon Glass0e43cad2025-05-10 13:04:55 +0200678 return os.path.join(get_top_level() or '', fname)
Simon Glass26132882012-01-14 15:12:45 +0000679
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500680
Simon Glass761648b2022-01-29 14:14:11 -0700681def get_default_user_name():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000682 """Gets the user.name from .gitconfig file.
683
684 Returns:
685 User name found in .gitconfig file, or None if none
686 """
Simon Glass7ade94e2025-03-16 08:00:18 +0000687 uname = command.output_one_line('git', 'config', '--global', '--includes',
688 'user.name')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000689 return uname
690
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500691
Simon Glass761648b2022-01-29 14:14:11 -0700692def get_default_user_email():
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000693 """Gets the user.email from the global .gitconfig file.
694
695 Returns:
696 User's email found in .gitconfig file, or None if none
697 """
Simon Glass7ade94e2025-03-16 08:00:18 +0000698 uemail = command.output_one_line('git', 'config', '--global', '--includes',
699 'user.email')
Vikram Narayanan12fb29a2012-05-23 09:01:06 +0000700 return uemail
701
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500702
Simon Glass761648b2022-01-29 14:14:11 -0700703def get_default_subject_prefix():
Wu, Josh9873b912015-04-15 10:25:18 +0800704 """Gets the format.subjectprefix from local .git/config file.
705
706 Returns:
707 Subject prefix found in local .git/config file, or None if none
708 """
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500709 sub_prefix = command.output_one_line(
710 'git', 'config', 'format.subjectprefix', raise_on_error=False)
Wu, Josh9873b912015-04-15 10:25:18 +0800711
712 return sub_prefix
713
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500714
Simon Glass761648b2022-01-29 14:14:11 -0700715def setup():
Simon Glass7ade94e2025-03-16 08:00:18 +0000716 """setup() - Set up git utils, by reading the alias files."""
Simon Glass26132882012-01-14 15:12:45 +0000717 # Check for a git alias file also
Simon Glass7ade94e2025-03-16 08:00:18 +0000718 global USE_NO_DECORATE
Simon Glass81bcca82014-08-28 09:43:45 -0600719
Simon Glass761648b2022-01-29 14:14:11 -0700720 cmd = log_cmd(None, count=0)
Simon Glass7ade94e2025-03-16 08:00:18 +0000721 USE_NO_DECORATE = (command.run_one(*cmd, raise_on_error=False)
Simon Glass6af913d2014-08-09 15:33:11 -0600722 .return_code == 0)
Simon Glass26132882012-01-14 15:12:45 +0000723
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500724
Simon Glass1bc26b22025-05-08 04:22:18 +0200725def get_hash(spec, git_dir=None):
Simon Glass414f1e02025-02-27 12:27:30 -0700726 """Get the hash of a commit
727
728 Args:
729 spec (str): Git commit to show, e.g. 'my-branch~12'
Simon Glass12946012025-05-10 13:04:54 +0200730 git_dir (str): Path to git repository (None to use default)
Simon Glass414f1e02025-02-27 12:27:30 -0700731
732 Returns:
733 str: Hash of commit
734 """
Simon Glass1bc26b22025-05-08 04:22:18 +0200735 cmd = ['git']
736 if git_dir:
737 cmd += ['--git-dir', git_dir]
738 cmd += ['show', '-s', '--pretty=format:%H', spec]
739 return command.output_one_line(*cmd)
Simon Glass414f1e02025-02-27 12:27:30 -0700740
741
Simon Glass761648b2022-01-29 14:14:11 -0700742def get_head():
Simon Glass11aba512012-12-15 10:42:07 +0000743 """Get the hash of the current HEAD
744
745 Returns:
746 Hash of HEAD
747 """
Simon Glass414f1e02025-02-27 12:27:30 -0700748 return get_hash('HEAD')
749
750
Simon Glass1bc26b22025-05-08 04:22:18 +0200751def get_branch(git_dir=None):
Simon Glass414f1e02025-02-27 12:27:30 -0700752 """Get the branch we are currently on
753
754 Return:
755 str: branch name, or None if none
Simon Glass1bc26b22025-05-08 04:22:18 +0200756 git_dir (str): Path to git repository (None to use default)
Simon Glass414f1e02025-02-27 12:27:30 -0700757 """
Simon Glass1bc26b22025-05-08 04:22:18 +0200758 cmd = ['git']
759 if git_dir:
760 cmd += ['--git-dir', git_dir]
761 cmd += ['rev-parse', '--abbrev-ref', 'HEAD']
762 out = command.output_one_line(*cmd, raise_on_error=False)
Simon Glass414f1e02025-02-27 12:27:30 -0700763 if out == 'HEAD':
764 return None
765 return out
Simon Glass11aba512012-12-15 10:42:07 +0000766
Maxim Cournoyer9a196fb2022-12-19 17:32:38 -0500767
Simon Glass573abf82025-05-08 05:23:41 +0200768def check_dirty(git_dir=None, work_tree=None):
769 """Check if the tree is dirty
770
771 Args:
772 git_dir (str): Path to git repository (None to use default)
Simon Glass12946012025-05-10 13:04:54 +0200773 work_tree (str): Git worktree to use, or None if none
Simon Glass573abf82025-05-08 05:23:41 +0200774
775 Return:
776 str: List of dirty filenames and state
777 """
778 cmd = ['git']
779 if git_dir:
780 cmd += ['--git-dir', git_dir]
781 if work_tree:
782 cmd += ['--work-tree', work_tree]
783 cmd += ['status', '--porcelain', '--untracked-files=no']
784 return command.output(*cmd).splitlines()
785
786
Simon Glass26132882012-01-14 15:12:45 +0000787if __name__ == "__main__":
788 import doctest
789
790 doctest.testmod()