The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 1 | # 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 Frysinger | 8e768ea | 2021-05-06 00:28:32 -0400 | [diff] [blame] | 15 | import functools |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 16 | import json |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 17 | import os |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 18 | import re |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 19 | import subprocess |
Mike Frysinger | 6447733 | 2023-08-21 21:20:32 -0400 | [diff] [blame] | 20 | import sys |
Sam Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 21 | from typing import Any, Optional |
Renaud Paquay | 2e70291 | 2016-11-01 11:23:38 -0700 | [diff] [blame] | 22 | |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 23 | from error import GitError |
Jason Chang | f9aacd4 | 2023-08-03 14:38:00 -0700 | [diff] [blame] | 24 | from error import RepoExitError |
Mike Frysinger | 71b0f31 | 2019-09-30 22:39:49 -0400 | [diff] [blame] | 25 | from git_refs import HEAD |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 26 | from git_trace2_event_log_base import BaseEventLog |
Renaud Paquay | 2e70291 | 2016-11-01 11:23:38 -0700 | [diff] [blame] | 27 | import platform_utils |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 28 | from repo_logging import RepoLogger |
Mike Frysinger | 6447733 | 2023-08-21 21:20:32 -0400 | [diff] [blame] | 29 | from repo_trace import IsTrace |
| 30 | from repo_trace import REPO_TRACE |
| 31 | from repo_trace import Trace |
Conley Owens | ff0a3c8 | 2014-01-30 14:46:03 -0800 | [diff] [blame] | 32 | from wrapper import Wrapper |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 33 | |
Mike Frysinger | 6447733 | 2023-08-21 21:20:32 -0400 | [diff] [blame] | 34 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 35 | GIT = "git" |
Mike Frysinger | 82caef6 | 2020-02-11 18:51:08 -0500 | [diff] [blame] | 36 | # 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. |
| 45 | MIN_GIT_VERSION_SOFT = (1, 9, 1) |
| 46 | MIN_GIT_VERSION_HARD = (1, 7, 2) |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 47 | GIT_DIR = "GIT_DIR" |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 48 | |
| 49 | LAST_GITDIR = None |
| 50 | LAST_CWD = None |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 51 | DEFAULT_GIT_FAIL_MESSAGE = "git command failure" |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 52 | ERROR_EVENT_LOGGING_PREFIX = "RepoGitCommandError" |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 53 | # Common line length limit |
| 54 | GIT_ERROR_STDOUT_LINES = 1 |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 55 | GIT_ERROR_STDERR_LINES = 10 |
Jason Chang | f9aacd4 | 2023-08-03 14:38:00 -0700 | [diff] [blame] | 56 | INVALID_GIT_EXIT_CODE = 126 |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 57 | |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 58 | logger = RepoLogger(__file__) |
| 59 | |
David Pursehouse | 819827a | 2020-02-12 15:20:19 +0900 | [diff] [blame] | 60 | |
Mike Frysinger | d4aee65 | 2023-10-19 05:13:32 -0400 | [diff] [blame] | 61 | class _GitCall: |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 62 | @functools.lru_cache(maxsize=None) |
| 63 | def version_tuple(self): |
| 64 | ret = Wrapper().ParseGitVersion() |
| 65 | if ret is None: |
Jason Chang | f9aacd4 | 2023-08-03 14:38:00 -0700 | [diff] [blame] | 66 | msg = "fatal: unable to detect git version" |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 67 | logger.error(msg) |
Jason Chang | f9aacd4 | 2023-08-03 14:38:00 -0700 | [diff] [blame] | 68 | raise GitRequireError(msg) |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 69 | return ret |
Shawn O. Pearce | 334851e | 2011-09-19 08:05:31 -0700 | [diff] [blame] | 70 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 71 | def __getattr__(self, name): |
| 72 | name = name.replace("_", "-") |
David Pursehouse | 819827a | 2020-02-12 15:20:19 +0900 | [diff] [blame] | 73 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 74 | def fun(*cmdv): |
| 75 | command = [name] |
| 76 | command.extend(cmdv) |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 77 | return GitCommand(None, command, add_event_log=False).Wait() == 0 |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 78 | |
| 79 | return fun |
David Pursehouse | 819827a | 2020-02-12 15:20:19 +0900 | [diff] [blame] | 80 | |
| 81 | |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 82 | git = _GitCall() |
| 83 | |
Mike Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 84 | |
Mike Frysinger | 71b0f31 | 2019-09-30 22:39:49 -0400 | [diff] [blame] | 85 | def RepoSourceVersion(): |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 86 | """Return the version of the repo.git tree.""" |
| 87 | ver = getattr(RepoSourceVersion, "version", None) |
Mike Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 88 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 89 | # 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 Frysinger | 71b0f31 | 2019-09-30 22:39:49 -0400 | [diff] [blame] | 93 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 94 | 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 Frysinger | 71b0f31 | 2019-09-30 22:39:49 -0400 | [diff] [blame] | 111 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 112 | return ver |
Mike Frysinger | 71b0f31 | 2019-09-30 22:39:49 -0400 | [diff] [blame] | 113 | |
| 114 | |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 115 | @functools.lru_cache(maxsize=None) |
| 116 | def 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 Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 142 | logger.error( |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 143 | "repo: error: 'git config --get' call failed with return code: " |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 144 | "%r, stderr: %r", |
| 145 | retval, |
| 146 | p.stderr, |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 147 | ) |
| 148 | return path |
| 149 | |
| 150 | |
Mike Frysinger | d4aee65 | 2023-10-19 05:13:32 -0400 | [diff] [blame] | 151 | class UserAgent: |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 152 | """Mange User-Agent settings when talking to external services |
Mike Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 153 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 154 | We follow the style as documented here: |
| 155 | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent |
| 156 | """ |
Mike Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 157 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 158 | _os = None |
| 159 | _repo_ua = None |
| 160 | _git_ua = None |
Mike Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 161 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 162 | @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 Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 176 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 177 | return self._os |
Mike Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 178 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 179 | @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 Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 192 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 193 | return self._repo_ua |
Mike Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 194 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 195 | @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 Frysinger | 2f0951b | 2019-07-10 17:13:46 -0400 | [diff] [blame] | 204 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 205 | return self._git_ua |
Mike Frysinger | 2f0951b | 2019-07-10 17:13:46 -0400 | [diff] [blame] | 206 | |
David Pursehouse | 819827a | 2020-02-12 15:20:19 +0900 | [diff] [blame] | 207 | |
Mike Frysinger | 71b0f31 | 2019-09-30 22:39:49 -0400 | [diff] [blame] | 208 | user_agent = UserAgent() |
Mike Frysinger | 369814b | 2019-07-10 17:10:07 -0400 | [diff] [blame] | 209 | |
David Pursehouse | 819827a | 2020-02-12 15:20:19 +0900 | [diff] [blame] | 210 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 211 | def 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 Chang | f9aacd4 | 2023-08-03 14:38:00 -0700 | [diff] [blame] | 219 | error_msg = "fatal: git %s or later required%s" % (need, msg) |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 220 | logger.error(error_msg) |
Jason Chang | f9aacd4 | 2023-08-03 14:38:00 -0700 | [diff] [blame] | 221 | raise GitRequireError(error_msg) |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 222 | return False |
Shawn O. Pearce | 2ec00b9 | 2009-06-12 09:32:50 -0700 | [diff] [blame] | 223 | |
David Pursehouse | 819827a | 2020-02-12 15:20:19 +0900 | [diff] [blame] | 224 | |
Sam Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 225 | def _build_env( |
Sergiy Belozorov | 78e82ec | 2023-01-05 18:57:31 +0100 | [diff] [blame] | 226 | _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 Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 231 | objdir: Optional[str] = None, |
Sam Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 232 | ): |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 233 | """Constucts an env dict for command execution.""" |
Sam Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 234 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 235 | assert _kwargs_only == (), "_build_env only accepts keyword arguments." |
Sam Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 236 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 237 | env = GitCommand._GetBasicEnv() |
Sam Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 238 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 239 | 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 Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 256 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 257 | if objdir: |
| 258 | # Set to the place we want to save the objects. |
| 259 | env["GIT_OBJECT_DIRECTORY"] = objdir |
Sam Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 260 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 261 | 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 Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 271 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 272 | return env |
Sam Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 273 | |
| 274 | |
Mike Frysinger | d4aee65 | 2023-10-19 05:13:32 -0400 | [diff] [blame] | 275 | class GitCommand: |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 276 | """Wrapper around a single git invocation.""" |
Mike Frysinger | 790f4ce | 2020-12-07 22:04:55 -0500 | [diff] [blame] | 277 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 278 | 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 Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 292 | verify_command=False, |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 293 | add_event_log=True, |
Jason Chang | 87058c6 | 2023-09-27 11:34:43 -0700 | [diff] [blame] | 294 | log_as_error=True, |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 295 | ): |
| 296 | if project: |
| 297 | if not cwd: |
| 298 | cwd = project.worktree |
| 299 | if not gitdir: |
| 300 | gitdir = project.gitdir |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 301 | |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 302 | self.project = project |
| 303 | self.cmdv = cmdv |
| 304 | self.verify_command = verify_command |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 305 | self.stdout, self.stderr = None, None |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 306 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 307 | # 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 Saccone | d686365 | 2022-11-15 23:57:22 +0000 | [diff] [blame] | 313 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 314 | env = _build_env( |
| 315 | disable_editor=disable_editor, |
| 316 | ssh_proxy=ssh_proxy, |
| 317 | objdir=objdir, |
| 318 | gitdir=gitdir, |
| 319 | bare=bare, |
| 320 | ) |
Mike Frysinger | 67d6cdf | 2021-12-23 17:36:09 -0500 | [diff] [blame] | 321 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 322 | command = [GIT] |
| 323 | if bare: |
| 324 | cwd = None |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 325 | command_name = cmdv[0] |
| 326 | command.append(command_name) |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 327 | # 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 Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 330 | if sys.stderr.isatty() and command_name in ("fetch", "clone"): |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 331 | if "--progress" not in cmdv and "--quiet" not in cmdv: |
| 332 | command.append("--progress") |
| 333 | command.extend(cmdv[1:]) |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 334 | |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 335 | 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 Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 345 | capture_stdout=capture_stdout, |
| 346 | capture_stderr=capture_stderr, |
| 347 | merge_output=merge_output, |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 348 | 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 Chang | 87058c6 | 2023-09-27 11:34:43 -0700 | [diff] [blame] | 364 | "IsError": log_as_error, |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 365 | } |
| 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 Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 378 | capture_stdout=False, |
| 379 | capture_stderr=False, |
| 380 | merge_output=False, |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 381 | ssh_proxy=None, |
| 382 | cwd=None, |
| 383 | input=None, |
| 384 | ): |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 385 | # 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 Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 418 | dbg = "" |
| 419 | if IsTrace(): |
| 420 | global LAST_CWD |
| 421 | global LAST_GITDIR |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 422 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 423 | 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 Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 428 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 429 | 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 Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 434 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 435 | 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 Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 444 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 445 | 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 Frysinger | 67d6cdf | 2021-12-23 17:36:09 -0500 | [diff] [blame] | 455 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 456 | 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 Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 464 | stdin=stdin, |
| 465 | stdout=stdout, |
| 466 | stderr=stderr, |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 467 | **kwargs, |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 468 | ) |
| 469 | except Exception as e: |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 470 | raise GitPopenCommandError( |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 471 | message="%s: %s" % (command[1], e), |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 472 | project=self.project.name if self.project else None, |
| 473 | command_args=self.cmdv, |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 474 | ) |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 475 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 476 | if ssh_proxy: |
| 477 | ssh_proxy.add_client(p) |
The Android Open Source Project | cf31fe9 | 2008-10-21 07:00:00 -0700 | [diff] [blame] | 478 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 479 | self.process = p |
Joanna Wang | a6c52f5 | 2022-11-03 16:51:19 -0400 | [diff] [blame] | 480 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 481 | try: |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 482 | 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 Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 489 | finally: |
| 490 | if ssh_proxy: |
| 491 | ssh_proxy.remove_client(p) |
| 492 | self.rc = p.wait() |
Joanna Wang | a6c52f5 | 2022-11-03 16:51:19 -0400 | [diff] [blame] | 493 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 494 | @staticmethod |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 495 | 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 = "" |
Daniel Kutik | 6a7f73b | 2023-10-09 13:21:25 +0200 | [diff] [blame] | 506 | read_size = 1024 if sys.version_info < (3, 7) else -1 |
| 507 | chunk = in_stream.read1(read_size) |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 508 | while chunk: |
| 509 | # Convert to str. |
| 510 | if not hasattr(chunk, "encode"): |
| 511 | chunk = chunk.decode("utf-8", "backslashreplace") |
| 512 | |
| 513 | buffer += chunk |
| 514 | out_stream.write(chunk) |
| 515 | out_stream.flush() |
| 516 | |
Daniel Kutik | 6a7f73b | 2023-10-09 13:21:25 +0200 | [diff] [blame] | 517 | chunk = in_stream.read1(read_size) |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 518 | |
| 519 | return buffer |
| 520 | |
| 521 | @staticmethod |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 522 | def _GetBasicEnv(): |
| 523 | """Return a basic env for running git under. |
Mike Frysinger | c5bbea8 | 2021-02-16 15:45:19 -0500 | [diff] [blame] | 524 | |
Gavin Mak | ea2e330 | 2023-03-11 06:46:20 +0000 | [diff] [blame] | 525 | This is guaranteed to be side-effect free. |
| 526 | """ |
| 527 | env = os.environ.copy() |
| 528 | for key in ( |
| 529 | REPO_TRACE, |
| 530 | GIT_DIR, |
| 531 | "GIT_ALTERNATE_OBJECT_DIRECTORIES", |
| 532 | "GIT_OBJECT_DIRECTORY", |
| 533 | "GIT_WORK_TREE", |
| 534 | "GIT_GRAFT_FILE", |
| 535 | "GIT_INDEX_FILE", |
| 536 | ): |
| 537 | env.pop(key, None) |
| 538 | return env |
Mike Frysinger | 71b0f31 | 2019-09-30 22:39:49 -0400 | [diff] [blame] | 539 | |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 540 | def VerifyCommand(self): |
| 541 | if self.rc == 0: |
| 542 | return None |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 543 | stdout = ( |
| 544 | "\n".join(self.stdout.split("\n")[:GIT_ERROR_STDOUT_LINES]) |
| 545 | if self.stdout |
| 546 | else None |
| 547 | ) |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 548 | stderr = ( |
| 549 | "\n".join(self.stderr.split("\n")[:GIT_ERROR_STDERR_LINES]) |
| 550 | if self.stderr |
| 551 | else None |
| 552 | ) |
| 553 | project = self.project.name if self.project else None |
| 554 | raise GitCommandError( |
| 555 | project=project, |
| 556 | command_args=self.cmdv, |
| 557 | git_rc=self.rc, |
| 558 | git_stdout=stdout, |
| 559 | git_stderr=stderr, |
| 560 | ) |
| 561 | |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 562 | def Wait(self): |
| 563 | if self.verify_command: |
| 564 | self.VerifyCommand() |
| 565 | return self.rc |
| 566 | |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 567 | |
Jason Chang | f9aacd4 | 2023-08-03 14:38:00 -0700 | [diff] [blame] | 568 | class GitRequireError(RepoExitError): |
| 569 | """Error raised when git version is unavailable or invalid.""" |
| 570 | |
| 571 | def __init__(self, message, exit_code: int = INVALID_GIT_EXIT_CODE): |
| 572 | super().__init__(message, exit_code=exit_code) |
| 573 | |
| 574 | |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 575 | class GitCommandError(GitError): |
| 576 | """ |
| 577 | Error raised from a failed git command. |
| 578 | Note that GitError can refer to any Git related error (e.g. branch not |
| 579 | specified for project.py 'UploadForReview'), while GitCommandError is |
| 580 | raised exclusively from non-zero exit codes returned from git commands. |
| 581 | """ |
| 582 | |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 583 | # Tuples with error formats and suggestions for those errors. |
| 584 | _ERROR_TO_SUGGESTION = [ |
| 585 | ( |
| 586 | re.compile("couldn't find remote ref .*"), |
| 587 | "Check if the provided ref exists in the remote.", |
| 588 | ), |
| 589 | ( |
| 590 | re.compile("unable to access '.*': .*"), |
| 591 | ( |
| 592 | "Please make sure you have the correct access rights and the " |
| 593 | "repository exists." |
| 594 | ), |
| 595 | ), |
| 596 | ( |
| 597 | re.compile("'.*' does not appear to be a git repository"), |
| 598 | "Are you running this repo command outside of a repo workspace?", |
| 599 | ), |
| 600 | ( |
| 601 | re.compile("not a git repository"), |
| 602 | "Are you running this repo command outside of a repo workspace?", |
| 603 | ), |
| 604 | ] |
| 605 | |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 606 | def __init__( |
| 607 | self, |
| 608 | message: str = DEFAULT_GIT_FAIL_MESSAGE, |
| 609 | git_rc: int = None, |
| 610 | git_stdout: str = None, |
| 611 | git_stderr: str = None, |
| 612 | **kwargs, |
| 613 | ): |
| 614 | super().__init__( |
| 615 | message, |
| 616 | **kwargs, |
| 617 | ) |
| 618 | self.git_rc = git_rc |
| 619 | self.git_stdout = git_stdout |
| 620 | self.git_stderr = git_stderr |
| 621 | |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 622 | @property |
Daniel Kutik | 23d063b | 2023-10-09 13:09:38 +0200 | [diff] [blame] | 623 | @functools.lru_cache(maxsize=None) |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 624 | def suggestion(self): |
| 625 | """Returns helpful next steps for the given stderr.""" |
| 626 | if not self.git_stderr: |
| 627 | return self.git_stderr |
| 628 | |
| 629 | for err, suggestion in self._ERROR_TO_SUGGESTION: |
| 630 | if err.search(self.git_stderr): |
| 631 | return suggestion |
| 632 | |
| 633 | return None |
| 634 | |
Jason Chang | a6413f5 | 2023-07-26 13:23:40 -0700 | [diff] [blame] | 635 | def __str__(self): |
| 636 | args = "[]" if not self.command_args else " ".join(self.command_args) |
| 637 | error_type = type(self).__name__ |
Aravind Vasudevan | 2844a5f | 2023-10-06 00:40:25 +0000 | [diff] [blame] | 638 | string = f"{error_type}: '{args}' on {self.project} failed" |
| 639 | |
| 640 | if self.message != DEFAULT_GIT_FAIL_MESSAGE: |
| 641 | string += f": {self.message}" |
| 642 | |
| 643 | if self.git_stdout: |
| 644 | string += f"\nstdout: {self.git_stdout}" |
| 645 | |
| 646 | if self.git_stderr: |
| 647 | string += f"\nstderr: {self.git_stderr}" |
| 648 | |
| 649 | if self.suggestion: |
| 650 | string += f"\nsuggestion: {self.suggestion}" |
| 651 | |
| 652 | return string |
Jason Chang | f19b310 | 2023-09-01 16:07:34 -0700 | [diff] [blame] | 653 | |
| 654 | |
| 655 | class GitPopenCommandError(GitError): |
| 656 | """ |
| 657 | Error raised when subprocess.Popen fails for a GitCommand |
| 658 | """ |