blob: 36fcfe7c8342241acafae67e8153271c3183088f [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
17import sys
18import subprocess
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 Frysinger8a11f6f2019-08-27 00:26:15 -040025from repo_trace import REPO_TRACE, IsTrace, Trace
Conley Owensff0a3c82014-01-30 14:46:03 -080026from wrapper import Wrapper
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070027
Gavin Makea2e3302023-03-11 06:46:20 +000028GIT = "git"
Mike Frysinger82caef62020-02-11 18:51:08 -050029# NB: These do not need to be kept in sync with the repo launcher script.
30# These may be much newer as it allows the repo launcher to roll between
31# different repo releases while source versions might require a newer git.
32#
33# The soft version is when we start warning users that the version is old and
34# we'll be dropping support for it. We'll refuse to work with versions older
35# than the hard version.
36#
37# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
38MIN_GIT_VERSION_SOFT = (1, 9, 1)
39MIN_GIT_VERSION_HARD = (1, 7, 2)
Gavin Makea2e3302023-03-11 06:46:20 +000040GIT_DIR = "GIT_DIR"
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070041
42LAST_GITDIR = None
43LAST_CWD = None
Jason Changa6413f52023-07-26 13:23:40 -070044DEFAULT_GIT_FAIL_MESSAGE = "git command failure"
45# Common line length limit
46GIT_ERROR_STDOUT_LINES = 1
47GIT_ERROR_STDERR_LINES = 1
Jason Changf9aacd42023-08-03 14:38:00 -070048INVALID_GIT_EXIT_CODE = 126
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070049
David Pursehouse819827a2020-02-12 15:20:19 +090050
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070051class _GitCall(object):
Gavin Makea2e3302023-03-11 06:46:20 +000052 @functools.lru_cache(maxsize=None)
53 def version_tuple(self):
54 ret = Wrapper().ParseGitVersion()
55 if ret is None:
Jason Changf9aacd42023-08-03 14:38:00 -070056 msg = "fatal: unable to detect git version"
57 print(msg, file=sys.stderr)
58 raise GitRequireError(msg)
Gavin Makea2e3302023-03-11 06:46:20 +000059 return ret
Shawn O. Pearce334851e2011-09-19 08:05:31 -070060
Gavin Makea2e3302023-03-11 06:46:20 +000061 def __getattr__(self, name):
62 name = name.replace("_", "-")
David Pursehouse819827a2020-02-12 15:20:19 +090063
Gavin Makea2e3302023-03-11 06:46:20 +000064 def fun(*cmdv):
65 command = [name]
66 command.extend(cmdv)
67 return GitCommand(None, command).Wait() == 0
68
69 return fun
David Pursehouse819827a2020-02-12 15:20:19 +090070
71
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070072git = _GitCall()
73
Mike Frysinger369814b2019-07-10 17:10:07 -040074
Mike Frysinger71b0f312019-09-30 22:39:49 -040075def RepoSourceVersion():
Gavin Makea2e3302023-03-11 06:46:20 +000076 """Return the version of the repo.git tree."""
77 ver = getattr(RepoSourceVersion, "version", None)
Mike Frysinger369814b2019-07-10 17:10:07 -040078
Gavin Makea2e3302023-03-11 06:46:20 +000079 # We avoid GitCommand so we don't run into circular deps -- GitCommand needs
80 # to initialize version info we provide.
81 if ver is None:
82 env = GitCommand._GetBasicEnv()
Mike Frysinger71b0f312019-09-30 22:39:49 -040083
Gavin Makea2e3302023-03-11 06:46:20 +000084 proj = os.path.dirname(os.path.abspath(__file__))
85 env[GIT_DIR] = os.path.join(proj, ".git")
86 result = subprocess.run(
87 [GIT, "describe", HEAD],
88 stdout=subprocess.PIPE,
89 stderr=subprocess.DEVNULL,
90 encoding="utf-8",
91 env=env,
92 check=False,
93 )
94 if result.returncode == 0:
95 ver = result.stdout.strip()
96 if ver.startswith("v"):
97 ver = ver[1:]
98 else:
99 ver = "unknown"
100 setattr(RepoSourceVersion, "version", ver)
Mike Frysinger71b0f312019-09-30 22:39:49 -0400101
Gavin Makea2e3302023-03-11 06:46:20 +0000102 return ver
Mike Frysinger71b0f312019-09-30 22:39:49 -0400103
104
105class UserAgent(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000106 """Mange User-Agent settings when talking to external services
Mike Frysinger369814b2019-07-10 17:10:07 -0400107
Gavin Makea2e3302023-03-11 06:46:20 +0000108 We follow the style as documented here:
109 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
110 """
Mike Frysinger369814b2019-07-10 17:10:07 -0400111
Gavin Makea2e3302023-03-11 06:46:20 +0000112 _os = None
113 _repo_ua = None
114 _git_ua = None
Mike Frysinger369814b2019-07-10 17:10:07 -0400115
Gavin Makea2e3302023-03-11 06:46:20 +0000116 @property
117 def os(self):
118 """The operating system name."""
119 if self._os is None:
120 os_name = sys.platform
121 if os_name.lower().startswith("linux"):
122 os_name = "Linux"
123 elif os_name == "win32":
124 os_name = "Win32"
125 elif os_name == "cygwin":
126 os_name = "Cygwin"
127 elif os_name == "darwin":
128 os_name = "Darwin"
129 self._os = os_name
Mike Frysinger369814b2019-07-10 17:10:07 -0400130
Gavin Makea2e3302023-03-11 06:46:20 +0000131 return self._os
Mike Frysinger369814b2019-07-10 17:10:07 -0400132
Gavin Makea2e3302023-03-11 06:46:20 +0000133 @property
134 def repo(self):
135 """The UA when connecting directly from repo."""
136 if self._repo_ua is None:
137 py_version = sys.version_info
138 self._repo_ua = "git-repo/%s (%s) git/%s Python/%d.%d.%d" % (
139 RepoSourceVersion(),
140 self.os,
141 git.version_tuple().full,
142 py_version.major,
143 py_version.minor,
144 py_version.micro,
145 )
Mike Frysinger369814b2019-07-10 17:10:07 -0400146
Gavin Makea2e3302023-03-11 06:46:20 +0000147 return self._repo_ua
Mike Frysinger369814b2019-07-10 17:10:07 -0400148
Gavin Makea2e3302023-03-11 06:46:20 +0000149 @property
150 def git(self):
151 """The UA when running git."""
152 if self._git_ua is None:
153 self._git_ua = "git/%s (%s) git-repo/%s" % (
154 git.version_tuple().full,
155 self.os,
156 RepoSourceVersion(),
157 )
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400158
Gavin Makea2e3302023-03-11 06:46:20 +0000159 return self._git_ua
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400160
David Pursehouse819827a2020-02-12 15:20:19 +0900161
Mike Frysinger71b0f312019-09-30 22:39:49 -0400162user_agent = UserAgent()
Mike Frysinger369814b2019-07-10 17:10:07 -0400163
David Pursehouse819827a2020-02-12 15:20:19 +0900164
Gavin Makea2e3302023-03-11 06:46:20 +0000165def git_require(min_version, fail=False, msg=""):
166 git_version = git.version_tuple()
167 if min_version <= git_version:
168 return True
169 if fail:
170 need = ".".join(map(str, min_version))
171 if msg:
172 msg = " for " + msg
Jason Changf9aacd42023-08-03 14:38:00 -0700173 error_msg = "fatal: git %s or later required%s" % (need, msg)
174 print(error_msg, file=sys.stderr)
175 raise GitRequireError(error_msg)
Gavin Makea2e3302023-03-11 06:46:20 +0000176 return False
Shawn O. Pearce2ec00b92009-06-12 09:32:50 -0700177
David Pursehouse819827a2020-02-12 15:20:19 +0900178
Sam Sacconed6863652022-11-15 23:57:22 +0000179def _build_env(
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100180 _kwargs_only=(),
181 bare: Optional[bool] = False,
182 disable_editor: Optional[bool] = False,
183 ssh_proxy: Optional[Any] = None,
184 gitdir: Optional[str] = None,
Gavin Makea2e3302023-03-11 06:46:20 +0000185 objdir: Optional[str] = None,
Sam Sacconed6863652022-11-15 23:57:22 +0000186):
Gavin Makea2e3302023-03-11 06:46:20 +0000187 """Constucts an env dict for command execution."""
Sam Sacconed6863652022-11-15 23:57:22 +0000188
Gavin Makea2e3302023-03-11 06:46:20 +0000189 assert _kwargs_only == (), "_build_env only accepts keyword arguments."
Sam Sacconed6863652022-11-15 23:57:22 +0000190
Gavin Makea2e3302023-03-11 06:46:20 +0000191 env = GitCommand._GetBasicEnv()
Sam Sacconed6863652022-11-15 23:57:22 +0000192
Gavin Makea2e3302023-03-11 06:46:20 +0000193 if disable_editor:
194 env["GIT_EDITOR"] = ":"
195 if ssh_proxy:
196 env["REPO_SSH_SOCK"] = ssh_proxy.sock()
197 env["GIT_SSH"] = ssh_proxy.proxy
198 env["GIT_SSH_VARIANT"] = "ssh"
199 if "http_proxy" in env and "darwin" == sys.platform:
200 s = "'http.proxy=%s'" % (env["http_proxy"],)
201 p = env.get("GIT_CONFIG_PARAMETERS")
202 if p is not None:
203 s = p + " " + s
204 env["GIT_CONFIG_PARAMETERS"] = s
205 if "GIT_ALLOW_PROTOCOL" not in env:
206 env[
207 "GIT_ALLOW_PROTOCOL"
208 ] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
209 env["GIT_HTTP_USER_AGENT"] = user_agent.git
Sam Sacconed6863652022-11-15 23:57:22 +0000210
Gavin Makea2e3302023-03-11 06:46:20 +0000211 if objdir:
212 # Set to the place we want to save the objects.
213 env["GIT_OBJECT_DIRECTORY"] = objdir
Sam Sacconed6863652022-11-15 23:57:22 +0000214
Gavin Makea2e3302023-03-11 06:46:20 +0000215 alt_objects = os.path.join(gitdir, "objects") if gitdir else None
216 if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(
217 objdir
218 ):
219 # Allow git to search the original place in case of local or unique
220 # refs that git will attempt to resolve even if we aren't fetching
221 # them.
222 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = alt_objects
223 if bare and gitdir is not None:
224 env[GIT_DIR] = gitdir
Sam Sacconed6863652022-11-15 23:57:22 +0000225
Gavin Makea2e3302023-03-11 06:46:20 +0000226 return env
Sam Sacconed6863652022-11-15 23:57:22 +0000227
228
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700229class GitCommand(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000230 """Wrapper around a single git invocation."""
Mike Frysinger790f4ce2020-12-07 22:04:55 -0500231
Gavin Makea2e3302023-03-11 06:46:20 +0000232 def __init__(
233 self,
234 project,
235 cmdv,
236 bare=False,
237 input=None,
238 capture_stdout=False,
239 capture_stderr=False,
240 merge_output=False,
241 disable_editor=False,
242 ssh_proxy=None,
243 cwd=None,
244 gitdir=None,
245 objdir=None,
Jason Changa6413f52023-07-26 13:23:40 -0700246 verify_command=False,
Gavin Makea2e3302023-03-11 06:46:20 +0000247 ):
248 if project:
249 if not cwd:
250 cwd = project.worktree
251 if not gitdir:
252 gitdir = project.gitdir
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700253
Jason Changa6413f52023-07-26 13:23:40 -0700254 self.project = project
255 self.cmdv = cmdv
256 self.verify_command = verify_command
257
Gavin Makea2e3302023-03-11 06:46:20 +0000258 # Git on Windows wants its paths only using / for reliability.
259 if platform_utils.isWindows():
260 if objdir:
261 objdir = objdir.replace("\\", "/")
262 if gitdir:
263 gitdir = gitdir.replace("\\", "/")
Sam Sacconed6863652022-11-15 23:57:22 +0000264
Gavin Makea2e3302023-03-11 06:46:20 +0000265 env = _build_env(
266 disable_editor=disable_editor,
267 ssh_proxy=ssh_proxy,
268 objdir=objdir,
269 gitdir=gitdir,
270 bare=bare,
271 )
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500272
Gavin Makea2e3302023-03-11 06:46:20 +0000273 command = [GIT]
274 if bare:
275 cwd = None
276 command.append(cmdv[0])
277 # Need to use the --progress flag for fetch/clone so output will be
278 # displayed as by default git only does progress output if stderr is a
279 # TTY.
280 if sys.stderr.isatty() and cmdv[0] in ("fetch", "clone"):
281 if "--progress" not in cmdv and "--quiet" not in cmdv:
282 command.append("--progress")
283 command.extend(cmdv[1:])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700284
Gavin Makea2e3302023-03-11 06:46:20 +0000285 stdin = subprocess.PIPE if input else None
286 stdout = subprocess.PIPE if capture_stdout else None
287 stderr = (
288 subprocess.STDOUT
289 if merge_output
290 else (subprocess.PIPE if capture_stderr else None)
291 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700292
Gavin Makea2e3302023-03-11 06:46:20 +0000293 dbg = ""
294 if IsTrace():
295 global LAST_CWD
296 global LAST_GITDIR
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700297
Gavin Makea2e3302023-03-11 06:46:20 +0000298 if cwd and LAST_CWD != cwd:
299 if LAST_GITDIR or LAST_CWD:
300 dbg += "\n"
301 dbg += ": cd %s\n" % cwd
302 LAST_CWD = cwd
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700303
Gavin Makea2e3302023-03-11 06:46:20 +0000304 if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
305 if LAST_GITDIR or LAST_CWD:
306 dbg += "\n"
307 dbg += ": export GIT_DIR=%s\n" % env[GIT_DIR]
308 LAST_GITDIR = env[GIT_DIR]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700309
Gavin Makea2e3302023-03-11 06:46:20 +0000310 if "GIT_OBJECT_DIRECTORY" in env:
311 dbg += (
312 ": export GIT_OBJECT_DIRECTORY=%s\n"
313 % env["GIT_OBJECT_DIRECTORY"]
314 )
315 if "GIT_ALTERNATE_OBJECT_DIRECTORIES" in env:
316 dbg += ": export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n" % (
317 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"]
318 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700319
Gavin Makea2e3302023-03-11 06:46:20 +0000320 dbg += ": "
321 dbg += " ".join(command)
322 if stdin == subprocess.PIPE:
323 dbg += " 0<|"
324 if stdout == subprocess.PIPE:
325 dbg += " 1>|"
326 if stderr == subprocess.PIPE:
327 dbg += " 2>|"
328 elif stderr == subprocess.STDOUT:
329 dbg += " 2>&1"
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500330
Gavin Makea2e3302023-03-11 06:46:20 +0000331 with Trace(
332 "git command %s %s with debug: %s", LAST_GITDIR, command, dbg
333 ):
334 try:
335 p = subprocess.Popen(
336 command,
337 cwd=cwd,
338 env=env,
339 encoding="utf-8",
340 errors="backslashreplace",
341 stdin=stdin,
342 stdout=stdout,
343 stderr=stderr,
344 )
345 except Exception as e:
Jason Changa6413f52023-07-26 13:23:40 -0700346 raise GitCommandError(
347 message="%s: %s" % (command[1], e),
348 project=project.name if project else None,
349 command_args=cmdv,
350 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700351
Gavin Makea2e3302023-03-11 06:46:20 +0000352 if ssh_proxy:
353 ssh_proxy.add_client(p)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700354
Gavin Makea2e3302023-03-11 06:46:20 +0000355 self.process = p
Joanna Wanga6c52f52022-11-03 16:51:19 -0400356
Gavin Makea2e3302023-03-11 06:46:20 +0000357 try:
358 self.stdout, self.stderr = p.communicate(input=input)
359 finally:
360 if ssh_proxy:
361 ssh_proxy.remove_client(p)
362 self.rc = p.wait()
Joanna Wanga6c52f52022-11-03 16:51:19 -0400363
Gavin Makea2e3302023-03-11 06:46:20 +0000364 @staticmethod
365 def _GetBasicEnv():
366 """Return a basic env for running git under.
Mike Frysingerc5bbea82021-02-16 15:45:19 -0500367
Gavin Makea2e3302023-03-11 06:46:20 +0000368 This is guaranteed to be side-effect free.
369 """
370 env = os.environ.copy()
371 for key in (
372 REPO_TRACE,
373 GIT_DIR,
374 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
375 "GIT_OBJECT_DIRECTORY",
376 "GIT_WORK_TREE",
377 "GIT_GRAFT_FILE",
378 "GIT_INDEX_FILE",
379 ):
380 env.pop(key, None)
381 return env
Mike Frysinger71b0f312019-09-30 22:39:49 -0400382
Gavin Makea2e3302023-03-11 06:46:20 +0000383 def Wait(self):
Jason Changa6413f52023-07-26 13:23:40 -0700384 if not self.verify_command or self.rc == 0:
385 return self.rc
386
387 stdout = (
388 "\n".join(self.stdout.split("\n")[:GIT_ERROR_STDOUT_LINES])
389 if self.stdout
390 else None
391 )
392
393 stderr = (
394 "\n".join(self.stderr.split("\n")[:GIT_ERROR_STDERR_LINES])
395 if self.stderr
396 else None
397 )
398 project = self.project.name if self.project else None
399 raise GitCommandError(
400 project=project,
401 command_args=self.cmdv,
402 git_rc=self.rc,
403 git_stdout=stdout,
404 git_stderr=stderr,
405 )
406
407
Jason Changf9aacd42023-08-03 14:38:00 -0700408class GitRequireError(RepoExitError):
409 """Error raised when git version is unavailable or invalid."""
410
411 def __init__(self, message, exit_code: int = INVALID_GIT_EXIT_CODE):
412 super().__init__(message, exit_code=exit_code)
413
414
Jason Changa6413f52023-07-26 13:23:40 -0700415class GitCommandError(GitError):
416 """
417 Error raised from a failed git command.
418 Note that GitError can refer to any Git related error (e.g. branch not
419 specified for project.py 'UploadForReview'), while GitCommandError is
420 raised exclusively from non-zero exit codes returned from git commands.
421 """
422
423 def __init__(
424 self,
425 message: str = DEFAULT_GIT_FAIL_MESSAGE,
426 git_rc: int = None,
427 git_stdout: str = None,
428 git_stderr: str = None,
429 **kwargs,
430 ):
431 super().__init__(
432 message,
433 **kwargs,
434 )
435 self.git_rc = git_rc
436 self.git_stdout = git_stdout
437 self.git_stderr = git_stderr
438
439 def __str__(self):
440 args = "[]" if not self.command_args else " ".join(self.command_args)
441 error_type = type(self).__name__
442 return f"""{error_type}: {self.message}
443 Project: {self.project}
444 Args: {args}
445 Stdout:
446{self.git_stdout}
447 Stderr:
448{self.git_stderr}"""