blob: 4b17f78d2719d21e882ee4a85d24f17ac7d40c52 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Mike Frysinger8e768ea2021-05-06 00:28:32 -040015import functools
Jason Changf19b3102023-09-01 16:07:34 -070016import json
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070017import os
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +000018import re
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070019import subprocess
Mike Frysinger64477332023-08-21 21:20:32 -040020import sys
Sam Sacconed6863652022-11-15 23:57:22 +000021from typing import Any, Optional
Renaud Paquay2e702912016-11-01 11:23:38 -070022
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070023from error import GitError
Jason Changf9aacd42023-08-03 14:38:00 -070024from error import RepoExitError
Mike Frysinger71b0f312019-09-30 22:39:49 -040025from git_refs import HEAD
Jason Changf19b3102023-09-01 16:07:34 -070026from git_trace2_event_log_base import BaseEventLog
Renaud Paquay2e702912016-11-01 11:23:38 -070027import platform_utils
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +000028from repo_logging import RepoLogger
Mike Frysinger64477332023-08-21 21:20:32 -040029from repo_trace import IsTrace
30from repo_trace import REPO_TRACE
31from repo_trace import Trace
Conley Owensff0a3c82014-01-30 14:46:03 -080032from wrapper import Wrapper
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070033
Mike Frysinger64477332023-08-21 21:20:32 -040034
Gavin Makea2e3302023-03-11 06:46:20 +000035GIT = "git"
Mike Frysinger82caef62020-02-11 18:51:08 -050036# NB: These do not need to be kept in sync with the repo launcher script.
37# These may be much newer as it allows the repo launcher to roll between
38# different repo releases while source versions might require a newer git.
39#
40# The soft version is when we start warning users that the version is old and
41# we'll be dropping support for it. We'll refuse to work with versions older
42# than the hard version.
43#
44# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
45MIN_GIT_VERSION_SOFT = (1, 9, 1)
46MIN_GIT_VERSION_HARD = (1, 7, 2)
Gavin Makea2e3302023-03-11 06:46:20 +000047GIT_DIR = "GIT_DIR"
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070048
49LAST_GITDIR = None
50LAST_CWD = None
Jason Changa6413f52023-07-26 13:23:40 -070051DEFAULT_GIT_FAIL_MESSAGE = "git command failure"
Jason Changf19b3102023-09-01 16:07:34 -070052ERROR_EVENT_LOGGING_PREFIX = "RepoGitCommandError"
Jason Changa6413f52023-07-26 13:23:40 -070053# Common line length limit
54GIT_ERROR_STDOUT_LINES = 1
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +000055GIT_ERROR_STDERR_LINES = 10
Jason Changf9aacd42023-08-03 14:38:00 -070056INVALID_GIT_EXIT_CODE = 126
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070057
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +000058logger = RepoLogger(__file__)
59
David Pursehouse819827a2020-02-12 15:20:19 +090060
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070061class _GitCall(object):
Gavin Makea2e3302023-03-11 06:46:20 +000062 @functools.lru_cache(maxsize=None)
63 def version_tuple(self):
64 ret = Wrapper().ParseGitVersion()
65 if ret is None:
Jason Changf9aacd42023-08-03 14:38:00 -070066 msg = "fatal: unable to detect git version"
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +000067 logger.error(msg)
Jason Changf9aacd42023-08-03 14:38:00 -070068 raise GitRequireError(msg)
Gavin Makea2e3302023-03-11 06:46:20 +000069 return ret
Shawn O. Pearce334851e2011-09-19 08:05:31 -070070
Gavin Makea2e3302023-03-11 06:46:20 +000071 def __getattr__(self, name):
72 name = name.replace("_", "-")
David Pursehouse819827a2020-02-12 15:20:19 +090073
Gavin Makea2e3302023-03-11 06:46:20 +000074 def fun(*cmdv):
75 command = [name]
76 command.extend(cmdv)
Jason Changf19b3102023-09-01 16:07:34 -070077 return GitCommand(None, command, add_event_log=False).Wait() == 0
Gavin Makea2e3302023-03-11 06:46:20 +000078
79 return fun
David Pursehouse819827a2020-02-12 15:20:19 +090080
81
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070082git = _GitCall()
83
Mike Frysinger369814b2019-07-10 17:10:07 -040084
Mike Frysinger71b0f312019-09-30 22:39:49 -040085def RepoSourceVersion():
Gavin Makea2e3302023-03-11 06:46:20 +000086 """Return the version of the repo.git tree."""
87 ver = getattr(RepoSourceVersion, "version", None)
Mike Frysinger369814b2019-07-10 17:10:07 -040088
Gavin Makea2e3302023-03-11 06:46:20 +000089 # We avoid GitCommand so we don't run into circular deps -- GitCommand needs
90 # to initialize version info we provide.
91 if ver is None:
92 env = GitCommand._GetBasicEnv()
Mike Frysinger71b0f312019-09-30 22:39:49 -040093
Gavin Makea2e3302023-03-11 06:46:20 +000094 proj = os.path.dirname(os.path.abspath(__file__))
95 env[GIT_DIR] = os.path.join(proj, ".git")
96 result = subprocess.run(
97 [GIT, "describe", HEAD],
98 stdout=subprocess.PIPE,
99 stderr=subprocess.DEVNULL,
100 encoding="utf-8",
101 env=env,
102 check=False,
103 )
104 if result.returncode == 0:
105 ver = result.stdout.strip()
106 if ver.startswith("v"):
107 ver = ver[1:]
108 else:
109 ver = "unknown"
110 setattr(RepoSourceVersion, "version", ver)
Mike Frysinger71b0f312019-09-30 22:39:49 -0400111
Gavin Makea2e3302023-03-11 06:46:20 +0000112 return ver
Mike Frysinger71b0f312019-09-30 22:39:49 -0400113
114
Jason Changf19b3102023-09-01 16:07:34 -0700115@functools.lru_cache(maxsize=None)
116def GetEventTargetPath():
117 """Get the 'trace2.eventtarget' path from git configuration.
118
119 Returns:
120 path: git config's 'trace2.eventtarget' path if it exists, or None
121 """
122 path = None
123 cmd = ["config", "--get", "trace2.eventtarget"]
124 # TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
125 # system git config variables.
126 p = GitCommand(
127 None,
128 cmd,
129 capture_stdout=True,
130 capture_stderr=True,
131 bare=True,
132 add_event_log=False,
133 )
134 retval = p.Wait()
135 if retval == 0:
136 # Strip trailing carriage-return in path.
137 path = p.stdout.rstrip("\n")
138 elif retval != 1:
139 # `git config --get` is documented to produce an exit status of `1`
140 # if the requested variable is not present in the configuration.
141 # Report any other return value as an error.
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000142 logger.error(
Jason Changf19b3102023-09-01 16:07:34 -0700143 "repo: error: 'git config --get' call failed with return code: "
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000144 "%r, stderr: %r",
145 retval,
146 p.stderr,
Jason Changf19b3102023-09-01 16:07:34 -0700147 )
148 return path
149
150
Mike Frysinger71b0f312019-09-30 22:39:49 -0400151class UserAgent(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000152 """Mange User-Agent settings when talking to external services
Mike Frysinger369814b2019-07-10 17:10:07 -0400153
Gavin Makea2e3302023-03-11 06:46:20 +0000154 We follow the style as documented here:
155 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
156 """
Mike Frysinger369814b2019-07-10 17:10:07 -0400157
Gavin Makea2e3302023-03-11 06:46:20 +0000158 _os = None
159 _repo_ua = None
160 _git_ua = None
Mike Frysinger369814b2019-07-10 17:10:07 -0400161
Gavin Makea2e3302023-03-11 06:46:20 +0000162 @property
163 def os(self):
164 """The operating system name."""
165 if self._os is None:
166 os_name = sys.platform
167 if os_name.lower().startswith("linux"):
168 os_name = "Linux"
169 elif os_name == "win32":
170 os_name = "Win32"
171 elif os_name == "cygwin":
172 os_name = "Cygwin"
173 elif os_name == "darwin":
174 os_name = "Darwin"
175 self._os = os_name
Mike Frysinger369814b2019-07-10 17:10:07 -0400176
Gavin Makea2e3302023-03-11 06:46:20 +0000177 return self._os
Mike Frysinger369814b2019-07-10 17:10:07 -0400178
Gavin Makea2e3302023-03-11 06:46:20 +0000179 @property
180 def repo(self):
181 """The UA when connecting directly from repo."""
182 if self._repo_ua is None:
183 py_version = sys.version_info
184 self._repo_ua = "git-repo/%s (%s) git/%s Python/%d.%d.%d" % (
185 RepoSourceVersion(),
186 self.os,
187 git.version_tuple().full,
188 py_version.major,
189 py_version.minor,
190 py_version.micro,
191 )
Mike Frysinger369814b2019-07-10 17:10:07 -0400192
Gavin Makea2e3302023-03-11 06:46:20 +0000193 return self._repo_ua
Mike Frysinger369814b2019-07-10 17:10:07 -0400194
Gavin Makea2e3302023-03-11 06:46:20 +0000195 @property
196 def git(self):
197 """The UA when running git."""
198 if self._git_ua is None:
199 self._git_ua = "git/%s (%s) git-repo/%s" % (
200 git.version_tuple().full,
201 self.os,
202 RepoSourceVersion(),
203 )
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400204
Gavin Makea2e3302023-03-11 06:46:20 +0000205 return self._git_ua
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400206
David Pursehouse819827a2020-02-12 15:20:19 +0900207
Mike Frysinger71b0f312019-09-30 22:39:49 -0400208user_agent = UserAgent()
Mike Frysinger369814b2019-07-10 17:10:07 -0400209
David Pursehouse819827a2020-02-12 15:20:19 +0900210
Gavin Makea2e3302023-03-11 06:46:20 +0000211def git_require(min_version, fail=False, msg=""):
212 git_version = git.version_tuple()
213 if min_version <= git_version:
214 return True
215 if fail:
216 need = ".".join(map(str, min_version))
217 if msg:
218 msg = " for " + msg
Jason Changf9aacd42023-08-03 14:38:00 -0700219 error_msg = "fatal: git %s or later required%s" % (need, msg)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000220 logger.error(error_msg)
Jason Changf9aacd42023-08-03 14:38:00 -0700221 raise GitRequireError(error_msg)
Gavin Makea2e3302023-03-11 06:46:20 +0000222 return False
Shawn O. Pearce2ec00b92009-06-12 09:32:50 -0700223
David Pursehouse819827a2020-02-12 15:20:19 +0900224
Sam Sacconed6863652022-11-15 23:57:22 +0000225def _build_env(
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100226 _kwargs_only=(),
227 bare: Optional[bool] = False,
228 disable_editor: Optional[bool] = False,
229 ssh_proxy: Optional[Any] = None,
230 gitdir: Optional[str] = None,
Gavin Makea2e3302023-03-11 06:46:20 +0000231 objdir: Optional[str] = None,
Sam Sacconed6863652022-11-15 23:57:22 +0000232):
Gavin Makea2e3302023-03-11 06:46:20 +0000233 """Constucts an env dict for command execution."""
Sam Sacconed6863652022-11-15 23:57:22 +0000234
Gavin Makea2e3302023-03-11 06:46:20 +0000235 assert _kwargs_only == (), "_build_env only accepts keyword arguments."
Sam Sacconed6863652022-11-15 23:57:22 +0000236
Gavin Makea2e3302023-03-11 06:46:20 +0000237 env = GitCommand._GetBasicEnv()
Sam Sacconed6863652022-11-15 23:57:22 +0000238
Gavin Makea2e3302023-03-11 06:46:20 +0000239 if disable_editor:
240 env["GIT_EDITOR"] = ":"
241 if ssh_proxy:
242 env["REPO_SSH_SOCK"] = ssh_proxy.sock()
243 env["GIT_SSH"] = ssh_proxy.proxy
244 env["GIT_SSH_VARIANT"] = "ssh"
245 if "http_proxy" in env and "darwin" == sys.platform:
246 s = "'http.proxy=%s'" % (env["http_proxy"],)
247 p = env.get("GIT_CONFIG_PARAMETERS")
248 if p is not None:
249 s = p + " " + s
250 env["GIT_CONFIG_PARAMETERS"] = s
251 if "GIT_ALLOW_PROTOCOL" not in env:
252 env[
253 "GIT_ALLOW_PROTOCOL"
254 ] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
255 env["GIT_HTTP_USER_AGENT"] = user_agent.git
Sam Sacconed6863652022-11-15 23:57:22 +0000256
Gavin Makea2e3302023-03-11 06:46:20 +0000257 if objdir:
258 # Set to the place we want to save the objects.
259 env["GIT_OBJECT_DIRECTORY"] = objdir
Sam Sacconed6863652022-11-15 23:57:22 +0000260
Gavin Makea2e3302023-03-11 06:46:20 +0000261 alt_objects = os.path.join(gitdir, "objects") if gitdir else None
262 if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(
263 objdir
264 ):
265 # Allow git to search the original place in case of local or unique
266 # refs that git will attempt to resolve even if we aren't fetching
267 # them.
268 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = alt_objects
269 if bare and gitdir is not None:
270 env[GIT_DIR] = gitdir
Sam Sacconed6863652022-11-15 23:57:22 +0000271
Gavin Makea2e3302023-03-11 06:46:20 +0000272 return env
Sam Sacconed6863652022-11-15 23:57:22 +0000273
274
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700275class GitCommand(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000276 """Wrapper around a single git invocation."""
Mike Frysinger790f4ce2020-12-07 22:04:55 -0500277
Gavin Makea2e3302023-03-11 06:46:20 +0000278 def __init__(
279 self,
280 project,
281 cmdv,
282 bare=False,
283 input=None,
284 capture_stdout=False,
285 capture_stderr=False,
286 merge_output=False,
287 disable_editor=False,
288 ssh_proxy=None,
289 cwd=None,
290 gitdir=None,
291 objdir=None,
Jason Changa6413f52023-07-26 13:23:40 -0700292 verify_command=False,
Jason Changf19b3102023-09-01 16:07:34 -0700293 add_event_log=True,
Jason Chang87058c62023-09-27 11:34:43 -0700294 log_as_error=True,
Gavin Makea2e3302023-03-11 06:46:20 +0000295 ):
296 if project:
297 if not cwd:
298 cwd = project.worktree
299 if not gitdir:
300 gitdir = project.gitdir
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700301
Jason Changa6413f52023-07-26 13:23:40 -0700302 self.project = project
303 self.cmdv = cmdv
304 self.verify_command = verify_command
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000305 self.stdout, self.stderr = None, None
Jason Changa6413f52023-07-26 13:23:40 -0700306
Gavin Makea2e3302023-03-11 06:46:20 +0000307 # Git on Windows wants its paths only using / for reliability.
308 if platform_utils.isWindows():
309 if objdir:
310 objdir = objdir.replace("\\", "/")
311 if gitdir:
312 gitdir = gitdir.replace("\\", "/")
Sam Sacconed6863652022-11-15 23:57:22 +0000313
Gavin Makea2e3302023-03-11 06:46:20 +0000314 env = _build_env(
315 disable_editor=disable_editor,
316 ssh_proxy=ssh_proxy,
317 objdir=objdir,
318 gitdir=gitdir,
319 bare=bare,
320 )
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500321
Gavin Makea2e3302023-03-11 06:46:20 +0000322 command = [GIT]
323 if bare:
324 cwd = None
Jason Changf19b3102023-09-01 16:07:34 -0700325 command_name = cmdv[0]
326 command.append(command_name)
Gavin Makea2e3302023-03-11 06:46:20 +0000327 # Need to use the --progress flag for fetch/clone so output will be
328 # displayed as by default git only does progress output if stderr is a
329 # TTY.
Jason Changf19b3102023-09-01 16:07:34 -0700330 if sys.stderr.isatty() and command_name in ("fetch", "clone"):
Gavin Makea2e3302023-03-11 06:46:20 +0000331 if "--progress" not in cmdv and "--quiet" not in cmdv:
332 command.append("--progress")
333 command.extend(cmdv[1:])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700334
Jason Changf19b3102023-09-01 16:07:34 -0700335 event_log = (
336 BaseEventLog(env=env, add_init_count=True)
337 if add_event_log
338 else None
339 )
340
341 try:
342 self._RunCommand(
343 command,
344 env,
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000345 capture_stdout=capture_stdout,
346 capture_stderr=capture_stderr,
347 merge_output=merge_output,
Jason Changf19b3102023-09-01 16:07:34 -0700348 ssh_proxy=ssh_proxy,
349 cwd=cwd,
350 input=input,
351 )
352 self.VerifyCommand()
353 except GitCommandError as e:
354 if event_log is not None:
355 error_info = json.dumps(
356 {
357 "ErrorType": type(e).__name__,
358 "Project": e.project,
359 "CommandName": command_name,
360 "Message": str(e),
361 "ReturnCode": str(e.git_rc)
362 if e.git_rc is not None
363 else None,
Jason Chang87058c62023-09-27 11:34:43 -0700364 "IsError": log_as_error,
Jason Changf19b3102023-09-01 16:07:34 -0700365 }
366 )
367 event_log.ErrorEvent(
368 f"{ERROR_EVENT_LOGGING_PREFIX}:{error_info}"
369 )
370 event_log.Write(GetEventTargetPath())
371 if isinstance(e, GitPopenCommandError):
372 raise
373
374 def _RunCommand(
375 self,
376 command,
377 env,
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000378 capture_stdout=False,
379 capture_stderr=False,
380 merge_output=False,
Jason Changf19b3102023-09-01 16:07:34 -0700381 ssh_proxy=None,
382 cwd=None,
383 input=None,
384 ):
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000385 # Set subprocess.PIPE for streams that need to be captured.
386 stdin = subprocess.PIPE if input else None
387 stdout = subprocess.PIPE if capture_stdout else None
388 stderr = (
389 subprocess.STDOUT
390 if merge_output
391 else (subprocess.PIPE if capture_stderr else None)
392 )
393
394 # tee_stderr acts like a tee command for stderr, in that, it captures
395 # stderr from the subprocess and streams it back to sys.stderr, while
396 # keeping a copy in-memory.
397 # This allows us to store stderr logs from the subprocess into
398 # GitCommandError.
399 # Certain git operations, such as `git push`, writes diagnostic logs,
400 # such as, progress bar for pushing, into stderr. To ensure we don't
401 # break git's UX, we need to write to sys.stderr as we read from the
402 # subprocess. Setting encoding or errors makes subprocess return
403 # io.TextIOWrapper, which is line buffered. To avoid line-buffering
404 # while tee-ing stderr, we unset these kwargs. See GitCommand._Tee
405 # for tee-ing between the streams.
406 # We tee stderr iff the caller doesn't want to capture any stream to
407 # not disrupt the existing flow.
408 # See go/tee-repo-stderr for more context.
409 tee_stderr = False
410 kwargs = {"encoding": "utf-8", "errors": "backslashreplace"}
411 if not (stdin or stdout or stderr):
412 tee_stderr = True
413 # stderr will be written back to sys.stderr even though it is
414 # piped here.
415 stderr = subprocess.PIPE
416 kwargs = {}
417
Gavin Makea2e3302023-03-11 06:46:20 +0000418 dbg = ""
419 if IsTrace():
420 global LAST_CWD
421 global LAST_GITDIR
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700422
Gavin Makea2e3302023-03-11 06:46:20 +0000423 if cwd and LAST_CWD != cwd:
424 if LAST_GITDIR or LAST_CWD:
425 dbg += "\n"
426 dbg += ": cd %s\n" % cwd
427 LAST_CWD = cwd
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700428
Gavin Makea2e3302023-03-11 06:46:20 +0000429 if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
430 if LAST_GITDIR or LAST_CWD:
431 dbg += "\n"
432 dbg += ": export GIT_DIR=%s\n" % env[GIT_DIR]
433 LAST_GITDIR = env[GIT_DIR]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700434
Gavin Makea2e3302023-03-11 06:46:20 +0000435 if "GIT_OBJECT_DIRECTORY" in env:
436 dbg += (
437 ": export GIT_OBJECT_DIRECTORY=%s\n"
438 % env["GIT_OBJECT_DIRECTORY"]
439 )
440 if "GIT_ALTERNATE_OBJECT_DIRECTORIES" in env:
441 dbg += ": export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n" % (
442 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"]
443 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700444
Gavin Makea2e3302023-03-11 06:46:20 +0000445 dbg += ": "
446 dbg += " ".join(command)
447 if stdin == subprocess.PIPE:
448 dbg += " 0<|"
449 if stdout == subprocess.PIPE:
450 dbg += " 1>|"
451 if stderr == subprocess.PIPE:
452 dbg += " 2>|"
453 elif stderr == subprocess.STDOUT:
454 dbg += " 2>&1"
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500455
Gavin Makea2e3302023-03-11 06:46:20 +0000456 with Trace(
457 "git command %s %s with debug: %s", LAST_GITDIR, command, dbg
458 ):
459 try:
460 p = subprocess.Popen(
461 command,
462 cwd=cwd,
463 env=env,
Gavin Makea2e3302023-03-11 06:46:20 +0000464 stdin=stdin,
465 stdout=stdout,
466 stderr=stderr,
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000467 **kwargs,
Gavin Makea2e3302023-03-11 06:46:20 +0000468 )
469 except Exception as e:
Jason Changf19b3102023-09-01 16:07:34 -0700470 raise GitPopenCommandError(
Jason Changa6413f52023-07-26 13:23:40 -0700471 message="%s: %s" % (command[1], e),
Jason Changf19b3102023-09-01 16:07:34 -0700472 project=self.project.name if self.project else None,
473 command_args=self.cmdv,
Jason Changa6413f52023-07-26 13:23:40 -0700474 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700475
Gavin Makea2e3302023-03-11 06:46:20 +0000476 if ssh_proxy:
477 ssh_proxy.add_client(p)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700478
Gavin Makea2e3302023-03-11 06:46:20 +0000479 self.process = p
Joanna Wanga6c52f52022-11-03 16:51:19 -0400480
Gavin Makea2e3302023-03-11 06:46:20 +0000481 try:
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000482 if tee_stderr:
483 # tee_stderr streams stderr to sys.stderr while capturing
484 # a copy within self.stderr. tee_stderr is only enabled
485 # when the caller wants to pipe no stream.
486 self.stderr = self._Tee(p.stderr, sys.stderr)
487 else:
488 self.stdout, self.stderr = p.communicate(input=input)
Gavin Makea2e3302023-03-11 06:46:20 +0000489 finally:
490 if ssh_proxy:
491 ssh_proxy.remove_client(p)
492 self.rc = p.wait()
Joanna Wanga6c52f52022-11-03 16:51:19 -0400493
Gavin Makea2e3302023-03-11 06:46:20 +0000494 @staticmethod
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000495 def _Tee(in_stream, out_stream):
496 """Writes text from in_stream to out_stream while recording in buffer.
497
498 Args:
499 in_stream: I/O stream to be read from.
500 out_stream: I/O stream to write to.
501
502 Returns:
503 A str containing everything read from the in_stream.
504 """
505 buffer = ""
506 chunk = in_stream.read1()
507 while chunk:
508 # Convert to str.
509 if not hasattr(chunk, "encode"):
510 chunk = chunk.decode("utf-8", "backslashreplace")
511
512 buffer += chunk
513 out_stream.write(chunk)
514 out_stream.flush()
515
516 chunk = in_stream.read1()
517
518 return buffer
519
520 @staticmethod
Gavin Makea2e3302023-03-11 06:46:20 +0000521 def _GetBasicEnv():
522 """Return a basic env for running git under.
Mike Frysingerc5bbea82021-02-16 15:45:19 -0500523
Gavin Makea2e3302023-03-11 06:46:20 +0000524 This is guaranteed to be side-effect free.
525 """
526 env = os.environ.copy()
527 for key in (
528 REPO_TRACE,
529 GIT_DIR,
530 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
531 "GIT_OBJECT_DIRECTORY",
532 "GIT_WORK_TREE",
533 "GIT_GRAFT_FILE",
534 "GIT_INDEX_FILE",
535 ):
536 env.pop(key, None)
537 return env
Mike Frysinger71b0f312019-09-30 22:39:49 -0400538
Jason Changf19b3102023-09-01 16:07:34 -0700539 def VerifyCommand(self):
540 if self.rc == 0:
541 return None
Jason Changa6413f52023-07-26 13:23:40 -0700542 stdout = (
543 "\n".join(self.stdout.split("\n")[:GIT_ERROR_STDOUT_LINES])
544 if self.stdout
545 else None
546 )
Jason Changa6413f52023-07-26 13:23:40 -0700547 stderr = (
548 "\n".join(self.stderr.split("\n")[:GIT_ERROR_STDERR_LINES])
549 if self.stderr
550 else None
551 )
552 project = self.project.name if self.project else None
553 raise GitCommandError(
554 project=project,
555 command_args=self.cmdv,
556 git_rc=self.rc,
557 git_stdout=stdout,
558 git_stderr=stderr,
559 )
560
Jason Changf19b3102023-09-01 16:07:34 -0700561 def Wait(self):
562 if self.verify_command:
563 self.VerifyCommand()
564 return self.rc
565
Jason Changa6413f52023-07-26 13:23:40 -0700566
Jason Changf9aacd42023-08-03 14:38:00 -0700567class GitRequireError(RepoExitError):
568 """Error raised when git version is unavailable or invalid."""
569
570 def __init__(self, message, exit_code: int = INVALID_GIT_EXIT_CODE):
571 super().__init__(message, exit_code=exit_code)
572
573
Jason Changa6413f52023-07-26 13:23:40 -0700574class GitCommandError(GitError):
575 """
576 Error raised from a failed git command.
577 Note that GitError can refer to any Git related error (e.g. branch not
578 specified for project.py 'UploadForReview'), while GitCommandError is
579 raised exclusively from non-zero exit codes returned from git commands.
580 """
581
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000582 # Tuples with error formats and suggestions for those errors.
583 _ERROR_TO_SUGGESTION = [
584 (
585 re.compile("couldn't find remote ref .*"),
586 "Check if the provided ref exists in the remote.",
587 ),
588 (
589 re.compile("unable to access '.*': .*"),
590 (
591 "Please make sure you have the correct access rights and the "
592 "repository exists."
593 ),
594 ),
595 (
596 re.compile("'.*' does not appear to be a git repository"),
597 "Are you running this repo command outside of a repo workspace?",
598 ),
599 (
600 re.compile("not a git repository"),
601 "Are you running this repo command outside of a repo workspace?",
602 ),
603 ]
604
Jason Changa6413f52023-07-26 13:23:40 -0700605 def __init__(
606 self,
607 message: str = DEFAULT_GIT_FAIL_MESSAGE,
608 git_rc: int = None,
609 git_stdout: str = None,
610 git_stderr: str = None,
611 **kwargs,
612 ):
613 super().__init__(
614 message,
615 **kwargs,
616 )
617 self.git_rc = git_rc
618 self.git_stdout = git_stdout
619 self.git_stderr = git_stderr
620
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000621 @property
Daniel Kutik23d063b2023-10-09 13:09:38 +0200622 @functools.lru_cache(maxsize=None)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000623 def suggestion(self):
624 """Returns helpful next steps for the given stderr."""
625 if not self.git_stderr:
626 return self.git_stderr
627
628 for err, suggestion in self._ERROR_TO_SUGGESTION:
629 if err.search(self.git_stderr):
630 return suggestion
631
632 return None
633
Jason Changa6413f52023-07-26 13:23:40 -0700634 def __str__(self):
635 args = "[]" if not self.command_args else " ".join(self.command_args)
636 error_type = type(self).__name__
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000637 string = f"{error_type}: '{args}' on {self.project} failed"
638
639 if self.message != DEFAULT_GIT_FAIL_MESSAGE:
640 string += f": {self.message}"
641
642 if self.git_stdout:
643 string += f"\nstdout: {self.git_stdout}"
644
645 if self.git_stderr:
646 string += f"\nstderr: {self.git_stderr}"
647
648 if self.suggestion:
649 string += f"\nsuggestion: {self.suggestion}"
650
651 return string
Jason Changf19b3102023-09-01 16:07:34 -0700652
653
654class GitPopenCommandError(GitError):
655 """
656 Error raised when subprocess.Popen fails for a GitCommand
657 """