blob: 3c3869a279c11b8bc4c11982cf2517a6c98b56ee [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
Mike Frysingerd4aee652023-10-19 05:13:32 -040061class _GitCall:
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 Frysingerd4aee652023-10-19 05:13:32 -0400151class UserAgent:
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:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400199 self._git_ua = (
200 f"git/{git.version_tuple().full} ({self.os}) "
201 f"git-repo/{RepoSourceVersion()}"
Gavin Makea2e3302023-03-11 06:46:20 +0000202 )
Gavin Makea2e3302023-03-11 06:46:20 +0000203 return self._git_ua
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400204
David Pursehouse819827a2020-02-12 15:20:19 +0900205
Mike Frysinger71b0f312019-09-30 22:39:49 -0400206user_agent = UserAgent()
Mike Frysinger369814b2019-07-10 17:10:07 -0400207
David Pursehouse819827a2020-02-12 15:20:19 +0900208
Gavin Makea2e3302023-03-11 06:46:20 +0000209def git_require(min_version, fail=False, msg=""):
210 git_version = git.version_tuple()
211 if min_version <= git_version:
212 return True
213 if fail:
214 need = ".".join(map(str, min_version))
215 if msg:
216 msg = " for " + msg
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400217 error_msg = f"fatal: git {need} or later required{msg}"
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000218 logger.error(error_msg)
Jason Changf9aacd42023-08-03 14:38:00 -0700219 raise GitRequireError(error_msg)
Gavin Makea2e3302023-03-11 06:46:20 +0000220 return False
Shawn O. Pearce2ec00b92009-06-12 09:32:50 -0700221
David Pursehouse819827a2020-02-12 15:20:19 +0900222
Sam Sacconed6863652022-11-15 23:57:22 +0000223def _build_env(
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100224 _kwargs_only=(),
225 bare: Optional[bool] = False,
226 disable_editor: Optional[bool] = False,
227 ssh_proxy: Optional[Any] = None,
228 gitdir: Optional[str] = None,
Gavin Makea2e3302023-03-11 06:46:20 +0000229 objdir: Optional[str] = None,
Sam Sacconed6863652022-11-15 23:57:22 +0000230):
Gavin Makea2e3302023-03-11 06:46:20 +0000231 """Constucts an env dict for command execution."""
Sam Sacconed6863652022-11-15 23:57:22 +0000232
Gavin Makea2e3302023-03-11 06:46:20 +0000233 assert _kwargs_only == (), "_build_env only accepts keyword arguments."
Sam Sacconed6863652022-11-15 23:57:22 +0000234
Gavin Makea2e3302023-03-11 06:46:20 +0000235 env = GitCommand._GetBasicEnv()
Sam Sacconed6863652022-11-15 23:57:22 +0000236
Gavin Makea2e3302023-03-11 06:46:20 +0000237 if disable_editor:
238 env["GIT_EDITOR"] = ":"
239 if ssh_proxy:
240 env["REPO_SSH_SOCK"] = ssh_proxy.sock()
241 env["GIT_SSH"] = ssh_proxy.proxy
242 env["GIT_SSH_VARIANT"] = "ssh"
243 if "http_proxy" in env and "darwin" == sys.platform:
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400244 s = f"'http.proxy={env['http_proxy']}'"
Gavin Makea2e3302023-03-11 06:46:20 +0000245 p = env.get("GIT_CONFIG_PARAMETERS")
246 if p is not None:
247 s = p + " " + s
248 env["GIT_CONFIG_PARAMETERS"] = s
249 if "GIT_ALLOW_PROTOCOL" not in env:
250 env[
251 "GIT_ALLOW_PROTOCOL"
252 ] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
253 env["GIT_HTTP_USER_AGENT"] = user_agent.git
Sam Sacconed6863652022-11-15 23:57:22 +0000254
Gavin Makea2e3302023-03-11 06:46:20 +0000255 if objdir:
256 # Set to the place we want to save the objects.
257 env["GIT_OBJECT_DIRECTORY"] = objdir
Sam Sacconed6863652022-11-15 23:57:22 +0000258
Gavin Makea2e3302023-03-11 06:46:20 +0000259 alt_objects = os.path.join(gitdir, "objects") if gitdir else None
260 if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(
261 objdir
262 ):
263 # Allow git to search the original place in case of local or unique
264 # refs that git will attempt to resolve even if we aren't fetching
265 # them.
266 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = alt_objects
267 if bare and gitdir is not None:
268 env[GIT_DIR] = gitdir
Sam Sacconed6863652022-11-15 23:57:22 +0000269
Gavin Makea2e3302023-03-11 06:46:20 +0000270 return env
Sam Sacconed6863652022-11-15 23:57:22 +0000271
272
Mike Frysingerd4aee652023-10-19 05:13:32 -0400273class GitCommand:
Gavin Makea2e3302023-03-11 06:46:20 +0000274 """Wrapper around a single git invocation."""
Mike Frysinger790f4ce2020-12-07 22:04:55 -0500275
Gavin Makea2e3302023-03-11 06:46:20 +0000276 def __init__(
277 self,
278 project,
279 cmdv,
280 bare=False,
281 input=None,
282 capture_stdout=False,
283 capture_stderr=False,
284 merge_output=False,
285 disable_editor=False,
286 ssh_proxy=None,
287 cwd=None,
288 gitdir=None,
289 objdir=None,
Jason Changa6413f52023-07-26 13:23:40 -0700290 verify_command=False,
Jason Changf19b3102023-09-01 16:07:34 -0700291 add_event_log=True,
Jason Chang87058c62023-09-27 11:34:43 -0700292 log_as_error=True,
Gavin Makea2e3302023-03-11 06:46:20 +0000293 ):
294 if project:
295 if not cwd:
296 cwd = project.worktree
297 if not gitdir:
298 gitdir = project.gitdir
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700299
Jason Changa6413f52023-07-26 13:23:40 -0700300 self.project = project
301 self.cmdv = cmdv
302 self.verify_command = verify_command
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000303 self.stdout, self.stderr = None, None
Jason Changa6413f52023-07-26 13:23:40 -0700304
Gavin Makea2e3302023-03-11 06:46:20 +0000305 # Git on Windows wants its paths only using / for reliability.
306 if platform_utils.isWindows():
307 if objdir:
308 objdir = objdir.replace("\\", "/")
309 if gitdir:
310 gitdir = gitdir.replace("\\", "/")
Sam Sacconed6863652022-11-15 23:57:22 +0000311
Gavin Makea2e3302023-03-11 06:46:20 +0000312 env = _build_env(
313 disable_editor=disable_editor,
314 ssh_proxy=ssh_proxy,
315 objdir=objdir,
316 gitdir=gitdir,
317 bare=bare,
318 )
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500319
Gavin Makea2e3302023-03-11 06:46:20 +0000320 command = [GIT]
321 if bare:
322 cwd = None
Jason Changf19b3102023-09-01 16:07:34 -0700323 command_name = cmdv[0]
324 command.append(command_name)
Gavin Makea2e3302023-03-11 06:46:20 +0000325 # Need to use the --progress flag for fetch/clone so output will be
326 # displayed as by default git only does progress output if stderr is a
327 # TTY.
Jason Changf19b3102023-09-01 16:07:34 -0700328 if sys.stderr.isatty() and command_name in ("fetch", "clone"):
Gavin Makea2e3302023-03-11 06:46:20 +0000329 if "--progress" not in cmdv and "--quiet" not in cmdv:
330 command.append("--progress")
331 command.extend(cmdv[1:])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700332
Jason Changf19b3102023-09-01 16:07:34 -0700333 event_log = (
334 BaseEventLog(env=env, add_init_count=True)
335 if add_event_log
336 else None
337 )
338
339 try:
340 self._RunCommand(
341 command,
342 env,
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000343 capture_stdout=capture_stdout,
344 capture_stderr=capture_stderr,
345 merge_output=merge_output,
Jason Changf19b3102023-09-01 16:07:34 -0700346 ssh_proxy=ssh_proxy,
347 cwd=cwd,
348 input=input,
349 )
350 self.VerifyCommand()
351 except GitCommandError as e:
352 if event_log is not None:
353 error_info = json.dumps(
354 {
355 "ErrorType": type(e).__name__,
356 "Project": e.project,
357 "CommandName": command_name,
358 "Message": str(e),
359 "ReturnCode": str(e.git_rc)
360 if e.git_rc is not None
361 else None,
Jason Chang87058c62023-09-27 11:34:43 -0700362 "IsError": log_as_error,
Jason Changf19b3102023-09-01 16:07:34 -0700363 }
364 )
365 event_log.ErrorEvent(
366 f"{ERROR_EVENT_LOGGING_PREFIX}:{error_info}"
367 )
368 event_log.Write(GetEventTargetPath())
369 if isinstance(e, GitPopenCommandError):
370 raise
371
372 def _RunCommand(
373 self,
374 command,
375 env,
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000376 capture_stdout=False,
377 capture_stderr=False,
378 merge_output=False,
Jason Changf19b3102023-09-01 16:07:34 -0700379 ssh_proxy=None,
380 cwd=None,
381 input=None,
382 ):
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000383 # Set subprocess.PIPE for streams that need to be captured.
384 stdin = subprocess.PIPE if input else None
385 stdout = subprocess.PIPE if capture_stdout else None
386 stderr = (
387 subprocess.STDOUT
388 if merge_output
389 else (subprocess.PIPE if capture_stderr else None)
390 )
391
392 # tee_stderr acts like a tee command for stderr, in that, it captures
393 # stderr from the subprocess and streams it back to sys.stderr, while
394 # keeping a copy in-memory.
395 # This allows us to store stderr logs from the subprocess into
396 # GitCommandError.
397 # Certain git operations, such as `git push`, writes diagnostic logs,
398 # such as, progress bar for pushing, into stderr. To ensure we don't
399 # break git's UX, we need to write to sys.stderr as we read from the
400 # subprocess. Setting encoding or errors makes subprocess return
401 # io.TextIOWrapper, which is line buffered. To avoid line-buffering
402 # while tee-ing stderr, we unset these kwargs. See GitCommand._Tee
403 # for tee-ing between the streams.
404 # We tee stderr iff the caller doesn't want to capture any stream to
405 # not disrupt the existing flow.
406 # See go/tee-repo-stderr for more context.
407 tee_stderr = False
408 kwargs = {"encoding": "utf-8", "errors": "backslashreplace"}
409 if not (stdin or stdout or stderr):
410 tee_stderr = True
411 # stderr will be written back to sys.stderr even though it is
412 # piped here.
413 stderr = subprocess.PIPE
414 kwargs = {}
415
Gavin Makea2e3302023-03-11 06:46:20 +0000416 dbg = ""
417 if IsTrace():
418 global LAST_CWD
419 global LAST_GITDIR
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700420
Gavin Makea2e3302023-03-11 06:46:20 +0000421 if cwd and LAST_CWD != cwd:
422 if LAST_GITDIR or LAST_CWD:
423 dbg += "\n"
424 dbg += ": cd %s\n" % cwd
425 LAST_CWD = cwd
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700426
Gavin Makea2e3302023-03-11 06:46:20 +0000427 if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
428 if LAST_GITDIR or LAST_CWD:
429 dbg += "\n"
430 dbg += ": export GIT_DIR=%s\n" % env[GIT_DIR]
431 LAST_GITDIR = env[GIT_DIR]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700432
Gavin Makea2e3302023-03-11 06:46:20 +0000433 if "GIT_OBJECT_DIRECTORY" in env:
434 dbg += (
435 ": export GIT_OBJECT_DIRECTORY=%s\n"
436 % env["GIT_OBJECT_DIRECTORY"]
437 )
438 if "GIT_ALTERNATE_OBJECT_DIRECTORIES" in env:
439 dbg += ": export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n" % (
440 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"]
441 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700442
Gavin Makea2e3302023-03-11 06:46:20 +0000443 dbg += ": "
444 dbg += " ".join(command)
445 if stdin == subprocess.PIPE:
446 dbg += " 0<|"
447 if stdout == subprocess.PIPE:
448 dbg += " 1>|"
449 if stderr == subprocess.PIPE:
450 dbg += " 2>|"
451 elif stderr == subprocess.STDOUT:
452 dbg += " 2>&1"
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500453
Gavin Makea2e3302023-03-11 06:46:20 +0000454 with Trace(
455 "git command %s %s with debug: %s", LAST_GITDIR, command, dbg
456 ):
457 try:
458 p = subprocess.Popen(
459 command,
460 cwd=cwd,
461 env=env,
Gavin Makea2e3302023-03-11 06:46:20 +0000462 stdin=stdin,
463 stdout=stdout,
464 stderr=stderr,
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000465 **kwargs,
Gavin Makea2e3302023-03-11 06:46:20 +0000466 )
467 except Exception as e:
Jason Changf19b3102023-09-01 16:07:34 -0700468 raise GitPopenCommandError(
Jason R. Coombsb32ccbb2023-09-29 11:04:49 -0400469 message=f"{command[1]}: {e}",
Jason Changf19b3102023-09-01 16:07:34 -0700470 project=self.project.name if self.project else None,
471 command_args=self.cmdv,
Jason Changa6413f52023-07-26 13:23:40 -0700472 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700473
Gavin Makea2e3302023-03-11 06:46:20 +0000474 if ssh_proxy:
475 ssh_proxy.add_client(p)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700476
Gavin Makea2e3302023-03-11 06:46:20 +0000477 self.process = p
Joanna Wanga6c52f52022-11-03 16:51:19 -0400478
Gavin Makea2e3302023-03-11 06:46:20 +0000479 try:
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000480 if tee_stderr:
481 # tee_stderr streams stderr to sys.stderr while capturing
482 # a copy within self.stderr. tee_stderr is only enabled
483 # when the caller wants to pipe no stream.
484 self.stderr = self._Tee(p.stderr, sys.stderr)
485 else:
486 self.stdout, self.stderr = p.communicate(input=input)
Gavin Makea2e3302023-03-11 06:46:20 +0000487 finally:
488 if ssh_proxy:
489 ssh_proxy.remove_client(p)
490 self.rc = p.wait()
Joanna Wanga6c52f52022-11-03 16:51:19 -0400491
Gavin Makea2e3302023-03-11 06:46:20 +0000492 @staticmethod
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000493 def _Tee(in_stream, out_stream):
494 """Writes text from in_stream to out_stream while recording in buffer.
495
496 Args:
497 in_stream: I/O stream to be read from.
498 out_stream: I/O stream to write to.
499
500 Returns:
501 A str containing everything read from the in_stream.
502 """
503 buffer = ""
Daniel Kutik6a7f73b2023-10-09 13:21:25 +0200504 read_size = 1024 if sys.version_info < (3, 7) else -1
505 chunk = in_stream.read1(read_size)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000506 while chunk:
507 # Convert to str.
508 if not hasattr(chunk, "encode"):
509 chunk = chunk.decode("utf-8", "backslashreplace")
510
511 buffer += chunk
512 out_stream.write(chunk)
513 out_stream.flush()
514
Daniel Kutik6a7f73b2023-10-09 13:21:25 +0200515 chunk = in_stream.read1(read_size)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000516
517 return buffer
518
519 @staticmethod
Gavin Makea2e3302023-03-11 06:46:20 +0000520 def _GetBasicEnv():
521 """Return a basic env for running git under.
Mike Frysingerc5bbea82021-02-16 15:45:19 -0500522
Gavin Makea2e3302023-03-11 06:46:20 +0000523 This is guaranteed to be side-effect free.
524 """
525 env = os.environ.copy()
526 for key in (
527 REPO_TRACE,
528 GIT_DIR,
529 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
530 "GIT_OBJECT_DIRECTORY",
531 "GIT_WORK_TREE",
532 "GIT_GRAFT_FILE",
533 "GIT_INDEX_FILE",
534 ):
535 env.pop(key, None)
536 return env
Mike Frysinger71b0f312019-09-30 22:39:49 -0400537
Jason Changf19b3102023-09-01 16:07:34 -0700538 def VerifyCommand(self):
539 if self.rc == 0:
540 return None
Jason Changa6413f52023-07-26 13:23:40 -0700541 stdout = (
542 "\n".join(self.stdout.split("\n")[:GIT_ERROR_STDOUT_LINES])
543 if self.stdout
544 else None
545 )
Jason Changa6413f52023-07-26 13:23:40 -0700546 stderr = (
547 "\n".join(self.stderr.split("\n")[:GIT_ERROR_STDERR_LINES])
548 if self.stderr
549 else None
550 )
551 project = self.project.name if self.project else None
552 raise GitCommandError(
553 project=project,
554 command_args=self.cmdv,
555 git_rc=self.rc,
556 git_stdout=stdout,
557 git_stderr=stderr,
558 )
559
Jason Changf19b3102023-09-01 16:07:34 -0700560 def Wait(self):
561 if self.verify_command:
562 self.VerifyCommand()
563 return self.rc
564
Jason Changa6413f52023-07-26 13:23:40 -0700565
Jason Changf9aacd42023-08-03 14:38:00 -0700566class GitRequireError(RepoExitError):
567 """Error raised when git version is unavailable or invalid."""
568
569 def __init__(self, message, exit_code: int = INVALID_GIT_EXIT_CODE):
570 super().__init__(message, exit_code=exit_code)
571
572
Jason Changa6413f52023-07-26 13:23:40 -0700573class GitCommandError(GitError):
574 """
575 Error raised from a failed git command.
576 Note that GitError can refer to any Git related error (e.g. branch not
577 specified for project.py 'UploadForReview'), while GitCommandError is
578 raised exclusively from non-zero exit codes returned from git commands.
579 """
580
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000581 # Tuples with error formats and suggestions for those errors.
582 _ERROR_TO_SUGGESTION = [
583 (
584 re.compile("couldn't find remote ref .*"),
585 "Check if the provided ref exists in the remote.",
586 ),
587 (
588 re.compile("unable to access '.*': .*"),
589 (
590 "Please make sure you have the correct access rights and the "
591 "repository exists."
592 ),
593 ),
594 (
595 re.compile("'.*' does not appear to be a git repository"),
596 "Are you running this repo command outside of a repo workspace?",
597 ),
598 (
599 re.compile("not a git repository"),
600 "Are you running this repo command outside of a repo workspace?",
601 ),
602 ]
603
Jason Changa6413f52023-07-26 13:23:40 -0700604 def __init__(
605 self,
606 message: str = DEFAULT_GIT_FAIL_MESSAGE,
607 git_rc: int = None,
608 git_stdout: str = None,
609 git_stderr: str = None,
610 **kwargs,
611 ):
612 super().__init__(
613 message,
614 **kwargs,
615 )
616 self.git_rc = git_rc
617 self.git_stdout = git_stdout
618 self.git_stderr = git_stderr
619
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000620 @property
Daniel Kutik23d063b2023-10-09 13:09:38 +0200621 @functools.lru_cache(maxsize=None)
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000622 def suggestion(self):
623 """Returns helpful next steps for the given stderr."""
624 if not self.git_stderr:
625 return self.git_stderr
626
627 for err, suggestion in self._ERROR_TO_SUGGESTION:
628 if err.search(self.git_stderr):
629 return suggestion
630
631 return None
632
Jason Changa6413f52023-07-26 13:23:40 -0700633 def __str__(self):
634 args = "[]" if not self.command_args else " ".join(self.command_args)
635 error_type = type(self).__name__
Aravind Vasudevan2844a5f2023-10-06 00:40:25 +0000636 string = f"{error_type}: '{args}' on {self.project} failed"
637
638 if self.message != DEFAULT_GIT_FAIL_MESSAGE:
639 string += f": {self.message}"
640
641 if self.git_stdout:
642 string += f"\nstdout: {self.git_stdout}"
643
644 if self.git_stderr:
645 string += f"\nstderr: {self.git_stderr}"
646
647 if self.suggestion:
648 string += f"\nsuggestion: {self.suggestion}"
649
650 return string
Jason Changf19b3102023-09-01 16:07:34 -0700651
652
653class GitPopenCommandError(GitError):
654 """
655 Error raised when subprocess.Popen fails for a GitCommand
656 """