blob: a5cf514bf971388fc331f398752ccf3160cff022 [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
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070016import os
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070017import subprocess
Mike Frysinger64477332023-08-21 21:20:32 -040018import sys
Sam Sacconed6863652022-11-15 23:57:22 +000019from typing import Any, Optional
Renaud Paquay2e702912016-11-01 11:23:38 -070020
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070021from error import GitError
Jason Changf9aacd42023-08-03 14:38:00 -070022from error import RepoExitError
Mike Frysinger71b0f312019-09-30 22:39:49 -040023from git_refs import HEAD
Renaud Paquay2e702912016-11-01 11:23:38 -070024import platform_utils
Mike Frysinger64477332023-08-21 21:20:32 -040025from repo_trace import IsTrace
26from repo_trace import REPO_TRACE
27from repo_trace import Trace
Conley Owensff0a3c82014-01-30 14:46:03 -080028from wrapper import Wrapper
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070029
Mike Frysinger64477332023-08-21 21:20:32 -040030
Gavin Makea2e3302023-03-11 06:46:20 +000031GIT = "git"
Mike Frysinger82caef62020-02-11 18:51:08 -050032# NB: These do not need to be kept in sync with the repo launcher script.
33# These may be much newer as it allows the repo launcher to roll between
34# different repo releases while source versions might require a newer git.
35#
36# The soft version is when we start warning users that the version is old and
37# we'll be dropping support for it. We'll refuse to work with versions older
38# than the hard version.
39#
40# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
41MIN_GIT_VERSION_SOFT = (1, 9, 1)
42MIN_GIT_VERSION_HARD = (1, 7, 2)
Gavin Makea2e3302023-03-11 06:46:20 +000043GIT_DIR = "GIT_DIR"
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070044
45LAST_GITDIR = None
46LAST_CWD = None
Jason Changa6413f52023-07-26 13:23:40 -070047DEFAULT_GIT_FAIL_MESSAGE = "git command failure"
48# Common line length limit
49GIT_ERROR_STDOUT_LINES = 1
50GIT_ERROR_STDERR_LINES = 1
Jason Changf9aacd42023-08-03 14:38:00 -070051INVALID_GIT_EXIT_CODE = 126
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070052
David Pursehouse819827a2020-02-12 15:20:19 +090053
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070054class _GitCall(object):
Gavin Makea2e3302023-03-11 06:46:20 +000055 @functools.lru_cache(maxsize=None)
56 def version_tuple(self):
57 ret = Wrapper().ParseGitVersion()
58 if ret is None:
Jason Changf9aacd42023-08-03 14:38:00 -070059 msg = "fatal: unable to detect git version"
60 print(msg, file=sys.stderr)
61 raise GitRequireError(msg)
Gavin Makea2e3302023-03-11 06:46:20 +000062 return ret
Shawn O. Pearce334851e2011-09-19 08:05:31 -070063
Gavin Makea2e3302023-03-11 06:46:20 +000064 def __getattr__(self, name):
65 name = name.replace("_", "-")
David Pursehouse819827a2020-02-12 15:20:19 +090066
Gavin Makea2e3302023-03-11 06:46:20 +000067 def fun(*cmdv):
68 command = [name]
69 command.extend(cmdv)
70 return GitCommand(None, command).Wait() == 0
71
72 return fun
David Pursehouse819827a2020-02-12 15:20:19 +090073
74
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070075git = _GitCall()
76
Mike Frysinger369814b2019-07-10 17:10:07 -040077
Mike Frysinger71b0f312019-09-30 22:39:49 -040078def RepoSourceVersion():
Gavin Makea2e3302023-03-11 06:46:20 +000079 """Return the version of the repo.git tree."""
80 ver = getattr(RepoSourceVersion, "version", None)
Mike Frysinger369814b2019-07-10 17:10:07 -040081
Gavin Makea2e3302023-03-11 06:46:20 +000082 # We avoid GitCommand so we don't run into circular deps -- GitCommand needs
83 # to initialize version info we provide.
84 if ver is None:
85 env = GitCommand._GetBasicEnv()
Mike Frysinger71b0f312019-09-30 22:39:49 -040086
Gavin Makea2e3302023-03-11 06:46:20 +000087 proj = os.path.dirname(os.path.abspath(__file__))
88 env[GIT_DIR] = os.path.join(proj, ".git")
89 result = subprocess.run(
90 [GIT, "describe", HEAD],
91 stdout=subprocess.PIPE,
92 stderr=subprocess.DEVNULL,
93 encoding="utf-8",
94 env=env,
95 check=False,
96 )
97 if result.returncode == 0:
98 ver = result.stdout.strip()
99 if ver.startswith("v"):
100 ver = ver[1:]
101 else:
102 ver = "unknown"
103 setattr(RepoSourceVersion, "version", ver)
Mike Frysinger71b0f312019-09-30 22:39:49 -0400104
Gavin Makea2e3302023-03-11 06:46:20 +0000105 return ver
Mike Frysinger71b0f312019-09-30 22:39:49 -0400106
107
108class UserAgent(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000109 """Mange User-Agent settings when talking to external services
Mike Frysinger369814b2019-07-10 17:10:07 -0400110
Gavin Makea2e3302023-03-11 06:46:20 +0000111 We follow the style as documented here:
112 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
113 """
Mike Frysinger369814b2019-07-10 17:10:07 -0400114
Gavin Makea2e3302023-03-11 06:46:20 +0000115 _os = None
116 _repo_ua = None
117 _git_ua = None
Mike Frysinger369814b2019-07-10 17:10:07 -0400118
Gavin Makea2e3302023-03-11 06:46:20 +0000119 @property
120 def os(self):
121 """The operating system name."""
122 if self._os is None:
123 os_name = sys.platform
124 if os_name.lower().startswith("linux"):
125 os_name = "Linux"
126 elif os_name == "win32":
127 os_name = "Win32"
128 elif os_name == "cygwin":
129 os_name = "Cygwin"
130 elif os_name == "darwin":
131 os_name = "Darwin"
132 self._os = os_name
Mike Frysinger369814b2019-07-10 17:10:07 -0400133
Gavin Makea2e3302023-03-11 06:46:20 +0000134 return self._os
Mike Frysinger369814b2019-07-10 17:10:07 -0400135
Gavin Makea2e3302023-03-11 06:46:20 +0000136 @property
137 def repo(self):
138 """The UA when connecting directly from repo."""
139 if self._repo_ua is None:
140 py_version = sys.version_info
141 self._repo_ua = "git-repo/%s (%s) git/%s Python/%d.%d.%d" % (
142 RepoSourceVersion(),
143 self.os,
144 git.version_tuple().full,
145 py_version.major,
146 py_version.minor,
147 py_version.micro,
148 )
Mike Frysinger369814b2019-07-10 17:10:07 -0400149
Gavin Makea2e3302023-03-11 06:46:20 +0000150 return self._repo_ua
Mike Frysinger369814b2019-07-10 17:10:07 -0400151
Gavin Makea2e3302023-03-11 06:46:20 +0000152 @property
153 def git(self):
154 """The UA when running git."""
155 if self._git_ua is None:
156 self._git_ua = "git/%s (%s) git-repo/%s" % (
157 git.version_tuple().full,
158 self.os,
159 RepoSourceVersion(),
160 )
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400161
Gavin Makea2e3302023-03-11 06:46:20 +0000162 return self._git_ua
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400163
David Pursehouse819827a2020-02-12 15:20:19 +0900164
Mike Frysinger71b0f312019-09-30 22:39:49 -0400165user_agent = UserAgent()
Mike Frysinger369814b2019-07-10 17:10:07 -0400166
David Pursehouse819827a2020-02-12 15:20:19 +0900167
Gavin Makea2e3302023-03-11 06:46:20 +0000168def git_require(min_version, fail=False, msg=""):
169 git_version = git.version_tuple()
170 if min_version <= git_version:
171 return True
172 if fail:
173 need = ".".join(map(str, min_version))
174 if msg:
175 msg = " for " + msg
Jason Changf9aacd42023-08-03 14:38:00 -0700176 error_msg = "fatal: git %s or later required%s" % (need, msg)
177 print(error_msg, file=sys.stderr)
178 raise GitRequireError(error_msg)
Gavin Makea2e3302023-03-11 06:46:20 +0000179 return False
Shawn O. Pearce2ec00b92009-06-12 09:32:50 -0700180
David Pursehouse819827a2020-02-12 15:20:19 +0900181
Sam Sacconed6863652022-11-15 23:57:22 +0000182def _build_env(
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100183 _kwargs_only=(),
184 bare: Optional[bool] = False,
185 disable_editor: Optional[bool] = False,
186 ssh_proxy: Optional[Any] = None,
187 gitdir: Optional[str] = None,
Gavin Makea2e3302023-03-11 06:46:20 +0000188 objdir: Optional[str] = None,
Sam Sacconed6863652022-11-15 23:57:22 +0000189):
Gavin Makea2e3302023-03-11 06:46:20 +0000190 """Constucts an env dict for command execution."""
Sam Sacconed6863652022-11-15 23:57:22 +0000191
Gavin Makea2e3302023-03-11 06:46:20 +0000192 assert _kwargs_only == (), "_build_env only accepts keyword arguments."
Sam Sacconed6863652022-11-15 23:57:22 +0000193
Gavin Makea2e3302023-03-11 06:46:20 +0000194 env = GitCommand._GetBasicEnv()
Sam Sacconed6863652022-11-15 23:57:22 +0000195
Gavin Makea2e3302023-03-11 06:46:20 +0000196 if disable_editor:
197 env["GIT_EDITOR"] = ":"
198 if ssh_proxy:
199 env["REPO_SSH_SOCK"] = ssh_proxy.sock()
200 env["GIT_SSH"] = ssh_proxy.proxy
201 env["GIT_SSH_VARIANT"] = "ssh"
202 if "http_proxy" in env and "darwin" == sys.platform:
203 s = "'http.proxy=%s'" % (env["http_proxy"],)
204 p = env.get("GIT_CONFIG_PARAMETERS")
205 if p is not None:
206 s = p + " " + s
207 env["GIT_CONFIG_PARAMETERS"] = s
208 if "GIT_ALLOW_PROTOCOL" not in env:
209 env[
210 "GIT_ALLOW_PROTOCOL"
211 ] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
212 env["GIT_HTTP_USER_AGENT"] = user_agent.git
Sam Sacconed6863652022-11-15 23:57:22 +0000213
Gavin Makea2e3302023-03-11 06:46:20 +0000214 if objdir:
215 # Set to the place we want to save the objects.
216 env["GIT_OBJECT_DIRECTORY"] = objdir
Sam Sacconed6863652022-11-15 23:57:22 +0000217
Gavin Makea2e3302023-03-11 06:46:20 +0000218 alt_objects = os.path.join(gitdir, "objects") if gitdir else None
219 if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(
220 objdir
221 ):
222 # Allow git to search the original place in case of local or unique
223 # refs that git will attempt to resolve even if we aren't fetching
224 # them.
225 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = alt_objects
226 if bare and gitdir is not None:
227 env[GIT_DIR] = gitdir
Sam Sacconed6863652022-11-15 23:57:22 +0000228
Gavin Makea2e3302023-03-11 06:46:20 +0000229 return env
Sam Sacconed6863652022-11-15 23:57:22 +0000230
231
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700232class GitCommand(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000233 """Wrapper around a single git invocation."""
Mike Frysinger790f4ce2020-12-07 22:04:55 -0500234
Gavin Makea2e3302023-03-11 06:46:20 +0000235 def __init__(
236 self,
237 project,
238 cmdv,
239 bare=False,
240 input=None,
241 capture_stdout=False,
242 capture_stderr=False,
243 merge_output=False,
244 disable_editor=False,
245 ssh_proxy=None,
246 cwd=None,
247 gitdir=None,
248 objdir=None,
Jason Changa6413f52023-07-26 13:23:40 -0700249 verify_command=False,
Gavin Makea2e3302023-03-11 06:46:20 +0000250 ):
251 if project:
252 if not cwd:
253 cwd = project.worktree
254 if not gitdir:
255 gitdir = project.gitdir
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700256
Jason Changa6413f52023-07-26 13:23:40 -0700257 self.project = project
258 self.cmdv = cmdv
259 self.verify_command = verify_command
260
Gavin Makea2e3302023-03-11 06:46:20 +0000261 # Git on Windows wants its paths only using / for reliability.
262 if platform_utils.isWindows():
263 if objdir:
264 objdir = objdir.replace("\\", "/")
265 if gitdir:
266 gitdir = gitdir.replace("\\", "/")
Sam Sacconed6863652022-11-15 23:57:22 +0000267
Gavin Makea2e3302023-03-11 06:46:20 +0000268 env = _build_env(
269 disable_editor=disable_editor,
270 ssh_proxy=ssh_proxy,
271 objdir=objdir,
272 gitdir=gitdir,
273 bare=bare,
274 )
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500275
Gavin Makea2e3302023-03-11 06:46:20 +0000276 command = [GIT]
277 if bare:
278 cwd = None
279 command.append(cmdv[0])
280 # Need to use the --progress flag for fetch/clone so output will be
281 # displayed as by default git only does progress output if stderr is a
282 # TTY.
283 if sys.stderr.isatty() and cmdv[0] in ("fetch", "clone"):
284 if "--progress" not in cmdv and "--quiet" not in cmdv:
285 command.append("--progress")
286 command.extend(cmdv[1:])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700287
Gavin Makea2e3302023-03-11 06:46:20 +0000288 stdin = subprocess.PIPE if input else None
289 stdout = subprocess.PIPE if capture_stdout else None
290 stderr = (
291 subprocess.STDOUT
292 if merge_output
293 else (subprocess.PIPE if capture_stderr else None)
294 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700295
Gavin Makea2e3302023-03-11 06:46:20 +0000296 dbg = ""
297 if IsTrace():
298 global LAST_CWD
299 global LAST_GITDIR
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700300
Gavin Makea2e3302023-03-11 06:46:20 +0000301 if cwd and LAST_CWD != cwd:
302 if LAST_GITDIR or LAST_CWD:
303 dbg += "\n"
304 dbg += ": cd %s\n" % cwd
305 LAST_CWD = cwd
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700306
Gavin Makea2e3302023-03-11 06:46:20 +0000307 if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
308 if LAST_GITDIR or LAST_CWD:
309 dbg += "\n"
310 dbg += ": export GIT_DIR=%s\n" % env[GIT_DIR]
311 LAST_GITDIR = env[GIT_DIR]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700312
Gavin Makea2e3302023-03-11 06:46:20 +0000313 if "GIT_OBJECT_DIRECTORY" in env:
314 dbg += (
315 ": export GIT_OBJECT_DIRECTORY=%s\n"
316 % env["GIT_OBJECT_DIRECTORY"]
317 )
318 if "GIT_ALTERNATE_OBJECT_DIRECTORIES" in env:
319 dbg += ": export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n" % (
320 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"]
321 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700322
Gavin Makea2e3302023-03-11 06:46:20 +0000323 dbg += ": "
324 dbg += " ".join(command)
325 if stdin == subprocess.PIPE:
326 dbg += " 0<|"
327 if stdout == subprocess.PIPE:
328 dbg += " 1>|"
329 if stderr == subprocess.PIPE:
330 dbg += " 2>|"
331 elif stderr == subprocess.STDOUT:
332 dbg += " 2>&1"
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500333
Gavin Makea2e3302023-03-11 06:46:20 +0000334 with Trace(
335 "git command %s %s with debug: %s", LAST_GITDIR, command, dbg
336 ):
337 try:
338 p = subprocess.Popen(
339 command,
340 cwd=cwd,
341 env=env,
342 encoding="utf-8",
343 errors="backslashreplace",
344 stdin=stdin,
345 stdout=stdout,
346 stderr=stderr,
347 )
348 except Exception as e:
Jason Changa6413f52023-07-26 13:23:40 -0700349 raise GitCommandError(
350 message="%s: %s" % (command[1], e),
351 project=project.name if project else None,
352 command_args=cmdv,
353 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700354
Gavin Makea2e3302023-03-11 06:46:20 +0000355 if ssh_proxy:
356 ssh_proxy.add_client(p)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700357
Gavin Makea2e3302023-03-11 06:46:20 +0000358 self.process = p
Joanna Wanga6c52f52022-11-03 16:51:19 -0400359
Gavin Makea2e3302023-03-11 06:46:20 +0000360 try:
361 self.stdout, self.stderr = p.communicate(input=input)
362 finally:
363 if ssh_proxy:
364 ssh_proxy.remove_client(p)
365 self.rc = p.wait()
Joanna Wanga6c52f52022-11-03 16:51:19 -0400366
Gavin Makea2e3302023-03-11 06:46:20 +0000367 @staticmethod
368 def _GetBasicEnv():
369 """Return a basic env for running git under.
Mike Frysingerc5bbea82021-02-16 15:45:19 -0500370
Gavin Makea2e3302023-03-11 06:46:20 +0000371 This is guaranteed to be side-effect free.
372 """
373 env = os.environ.copy()
374 for key in (
375 REPO_TRACE,
376 GIT_DIR,
377 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
378 "GIT_OBJECT_DIRECTORY",
379 "GIT_WORK_TREE",
380 "GIT_GRAFT_FILE",
381 "GIT_INDEX_FILE",
382 ):
383 env.pop(key, None)
384 return env
Mike Frysinger71b0f312019-09-30 22:39:49 -0400385
Gavin Makea2e3302023-03-11 06:46:20 +0000386 def Wait(self):
Jason Changa6413f52023-07-26 13:23:40 -0700387 if not self.verify_command or self.rc == 0:
388 return self.rc
389
390 stdout = (
391 "\n".join(self.stdout.split("\n")[:GIT_ERROR_STDOUT_LINES])
392 if self.stdout
393 else None
394 )
395
396 stderr = (
397 "\n".join(self.stderr.split("\n")[:GIT_ERROR_STDERR_LINES])
398 if self.stderr
399 else None
400 )
401 project = self.project.name if self.project else None
402 raise GitCommandError(
403 project=project,
404 command_args=self.cmdv,
405 git_rc=self.rc,
406 git_stdout=stdout,
407 git_stderr=stderr,
408 )
409
410
Jason Changf9aacd42023-08-03 14:38:00 -0700411class GitRequireError(RepoExitError):
412 """Error raised when git version is unavailable or invalid."""
413
414 def __init__(self, message, exit_code: int = INVALID_GIT_EXIT_CODE):
415 super().__init__(message, exit_code=exit_code)
416
417
Jason Changa6413f52023-07-26 13:23:40 -0700418class GitCommandError(GitError):
419 """
420 Error raised from a failed git command.
421 Note that GitError can refer to any Git related error (e.g. branch not
422 specified for project.py 'UploadForReview'), while GitCommandError is
423 raised exclusively from non-zero exit codes returned from git commands.
424 """
425
426 def __init__(
427 self,
428 message: str = DEFAULT_GIT_FAIL_MESSAGE,
429 git_rc: int = None,
430 git_stdout: str = None,
431 git_stderr: str = None,
432 **kwargs,
433 ):
434 super().__init__(
435 message,
436 **kwargs,
437 )
438 self.git_rc = git_rc
439 self.git_stdout = git_stdout
440 self.git_stderr = git_stderr
441
442 def __str__(self):
443 args = "[]" if not self.command_args else " ".join(self.command_args)
444 error_type = type(self).__name__
445 return f"""{error_type}: {self.message}
446 Project: {self.project}
447 Args: {args}
448 Stdout:
449{self.git_stdout}
450 Stderr:
451{self.git_stderr}"""