blob: 588a64fd0c7deb939ba33024acf1c030b2f7a20a [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
Mike Frysinger71b0f312019-09-30 22:39:49 -040022from git_refs import HEAD
Renaud Paquay2e702912016-11-01 11:23:38 -070023import platform_utils
Mike Frysinger8a11f6f2019-08-27 00:26:15 -040024from repo_trace import REPO_TRACE, IsTrace, Trace
Conley Owensff0a3c82014-01-30 14:46:03 -080025from wrapper import Wrapper
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070026
Gavin Makea2e3302023-03-11 06:46:20 +000027GIT = "git"
Mike Frysinger82caef62020-02-11 18:51:08 -050028# NB: These do not need to be kept in sync with the repo launcher script.
29# These may be much newer as it allows the repo launcher to roll between
30# different repo releases while source versions might require a newer git.
31#
32# The soft version is when we start warning users that the version is old and
33# we'll be dropping support for it. We'll refuse to work with versions older
34# than the hard version.
35#
36# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
37MIN_GIT_VERSION_SOFT = (1, 9, 1)
38MIN_GIT_VERSION_HARD = (1, 7, 2)
Gavin Makea2e3302023-03-11 06:46:20 +000039GIT_DIR = "GIT_DIR"
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070040
41LAST_GITDIR = None
42LAST_CWD = None
Jason Changa6413f52023-07-26 13:23:40 -070043DEFAULT_GIT_FAIL_MESSAGE = "git command failure"
44# Common line length limit
45GIT_ERROR_STDOUT_LINES = 1
46GIT_ERROR_STDERR_LINES = 1
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070047
David Pursehouse819827a2020-02-12 15:20:19 +090048
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070049class _GitCall(object):
Gavin Makea2e3302023-03-11 06:46:20 +000050 @functools.lru_cache(maxsize=None)
51 def version_tuple(self):
52 ret = Wrapper().ParseGitVersion()
53 if ret is None:
54 print("fatal: unable to detect git version", file=sys.stderr)
55 sys.exit(1)
56 return ret
Shawn O. Pearce334851e2011-09-19 08:05:31 -070057
Gavin Makea2e3302023-03-11 06:46:20 +000058 def __getattr__(self, name):
59 name = name.replace("_", "-")
David Pursehouse819827a2020-02-12 15:20:19 +090060
Gavin Makea2e3302023-03-11 06:46:20 +000061 def fun(*cmdv):
62 command = [name]
63 command.extend(cmdv)
64 return GitCommand(None, command).Wait() == 0
65
66 return fun
David Pursehouse819827a2020-02-12 15:20:19 +090067
68
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070069git = _GitCall()
70
Mike Frysinger369814b2019-07-10 17:10:07 -040071
Mike Frysinger71b0f312019-09-30 22:39:49 -040072def RepoSourceVersion():
Gavin Makea2e3302023-03-11 06:46:20 +000073 """Return the version of the repo.git tree."""
74 ver = getattr(RepoSourceVersion, "version", None)
Mike Frysinger369814b2019-07-10 17:10:07 -040075
Gavin Makea2e3302023-03-11 06:46:20 +000076 # We avoid GitCommand so we don't run into circular deps -- GitCommand needs
77 # to initialize version info we provide.
78 if ver is None:
79 env = GitCommand._GetBasicEnv()
Mike Frysinger71b0f312019-09-30 22:39:49 -040080
Gavin Makea2e3302023-03-11 06:46:20 +000081 proj = os.path.dirname(os.path.abspath(__file__))
82 env[GIT_DIR] = os.path.join(proj, ".git")
83 result = subprocess.run(
84 [GIT, "describe", HEAD],
85 stdout=subprocess.PIPE,
86 stderr=subprocess.DEVNULL,
87 encoding="utf-8",
88 env=env,
89 check=False,
90 )
91 if result.returncode == 0:
92 ver = result.stdout.strip()
93 if ver.startswith("v"):
94 ver = ver[1:]
95 else:
96 ver = "unknown"
97 setattr(RepoSourceVersion, "version", ver)
Mike Frysinger71b0f312019-09-30 22:39:49 -040098
Gavin Makea2e3302023-03-11 06:46:20 +000099 return ver
Mike Frysinger71b0f312019-09-30 22:39:49 -0400100
101
102class UserAgent(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000103 """Mange User-Agent settings when talking to external services
Mike Frysinger369814b2019-07-10 17:10:07 -0400104
Gavin Makea2e3302023-03-11 06:46:20 +0000105 We follow the style as documented here:
106 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
107 """
Mike Frysinger369814b2019-07-10 17:10:07 -0400108
Gavin Makea2e3302023-03-11 06:46:20 +0000109 _os = None
110 _repo_ua = None
111 _git_ua = None
Mike Frysinger369814b2019-07-10 17:10:07 -0400112
Gavin Makea2e3302023-03-11 06:46:20 +0000113 @property
114 def os(self):
115 """The operating system name."""
116 if self._os is None:
117 os_name = sys.platform
118 if os_name.lower().startswith("linux"):
119 os_name = "Linux"
120 elif os_name == "win32":
121 os_name = "Win32"
122 elif os_name == "cygwin":
123 os_name = "Cygwin"
124 elif os_name == "darwin":
125 os_name = "Darwin"
126 self._os = os_name
Mike Frysinger369814b2019-07-10 17:10:07 -0400127
Gavin Makea2e3302023-03-11 06:46:20 +0000128 return self._os
Mike Frysinger369814b2019-07-10 17:10:07 -0400129
Gavin Makea2e3302023-03-11 06:46:20 +0000130 @property
131 def repo(self):
132 """The UA when connecting directly from repo."""
133 if self._repo_ua is None:
134 py_version = sys.version_info
135 self._repo_ua = "git-repo/%s (%s) git/%s Python/%d.%d.%d" % (
136 RepoSourceVersion(),
137 self.os,
138 git.version_tuple().full,
139 py_version.major,
140 py_version.minor,
141 py_version.micro,
142 )
Mike Frysinger369814b2019-07-10 17:10:07 -0400143
Gavin Makea2e3302023-03-11 06:46:20 +0000144 return self._repo_ua
Mike Frysinger369814b2019-07-10 17:10:07 -0400145
Gavin Makea2e3302023-03-11 06:46:20 +0000146 @property
147 def git(self):
148 """The UA when running git."""
149 if self._git_ua is None:
150 self._git_ua = "git/%s (%s) git-repo/%s" % (
151 git.version_tuple().full,
152 self.os,
153 RepoSourceVersion(),
154 )
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400155
Gavin Makea2e3302023-03-11 06:46:20 +0000156 return self._git_ua
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400157
David Pursehouse819827a2020-02-12 15:20:19 +0900158
Mike Frysinger71b0f312019-09-30 22:39:49 -0400159user_agent = UserAgent()
Mike Frysinger369814b2019-07-10 17:10:07 -0400160
David Pursehouse819827a2020-02-12 15:20:19 +0900161
Gavin Makea2e3302023-03-11 06:46:20 +0000162def git_require(min_version, fail=False, msg=""):
163 git_version = git.version_tuple()
164 if min_version <= git_version:
165 return True
166 if fail:
167 need = ".".join(map(str, min_version))
168 if msg:
169 msg = " for " + msg
170 print(
171 "fatal: git %s or later required%s" % (need, msg), file=sys.stderr
172 )
173 sys.exit(1)
174 return False
Shawn O. Pearce2ec00b92009-06-12 09:32:50 -0700175
David Pursehouse819827a2020-02-12 15:20:19 +0900176
Sam Sacconed6863652022-11-15 23:57:22 +0000177def _build_env(
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100178 _kwargs_only=(),
179 bare: Optional[bool] = False,
180 disable_editor: Optional[bool] = False,
181 ssh_proxy: Optional[Any] = None,
182 gitdir: Optional[str] = None,
Gavin Makea2e3302023-03-11 06:46:20 +0000183 objdir: Optional[str] = None,
Sam Sacconed6863652022-11-15 23:57:22 +0000184):
Gavin Makea2e3302023-03-11 06:46:20 +0000185 """Constucts an env dict for command execution."""
Sam Sacconed6863652022-11-15 23:57:22 +0000186
Gavin Makea2e3302023-03-11 06:46:20 +0000187 assert _kwargs_only == (), "_build_env only accepts keyword arguments."
Sam Sacconed6863652022-11-15 23:57:22 +0000188
Gavin Makea2e3302023-03-11 06:46:20 +0000189 env = GitCommand._GetBasicEnv()
Sam Sacconed6863652022-11-15 23:57:22 +0000190
Gavin Makea2e3302023-03-11 06:46:20 +0000191 if disable_editor:
192 env["GIT_EDITOR"] = ":"
193 if ssh_proxy:
194 env["REPO_SSH_SOCK"] = ssh_proxy.sock()
195 env["GIT_SSH"] = ssh_proxy.proxy
196 env["GIT_SSH_VARIANT"] = "ssh"
197 if "http_proxy" in env and "darwin" == sys.platform:
198 s = "'http.proxy=%s'" % (env["http_proxy"],)
199 p = env.get("GIT_CONFIG_PARAMETERS")
200 if p is not None:
201 s = p + " " + s
202 env["GIT_CONFIG_PARAMETERS"] = s
203 if "GIT_ALLOW_PROTOCOL" not in env:
204 env[
205 "GIT_ALLOW_PROTOCOL"
206 ] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
207 env["GIT_HTTP_USER_AGENT"] = user_agent.git
Sam Sacconed6863652022-11-15 23:57:22 +0000208
Gavin Makea2e3302023-03-11 06:46:20 +0000209 if objdir:
210 # Set to the place we want to save the objects.
211 env["GIT_OBJECT_DIRECTORY"] = objdir
Sam Sacconed6863652022-11-15 23:57:22 +0000212
Gavin Makea2e3302023-03-11 06:46:20 +0000213 alt_objects = os.path.join(gitdir, "objects") if gitdir else None
214 if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(
215 objdir
216 ):
217 # Allow git to search the original place in case of local or unique
218 # refs that git will attempt to resolve even if we aren't fetching
219 # them.
220 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = alt_objects
221 if bare and gitdir is not None:
222 env[GIT_DIR] = gitdir
Sam Sacconed6863652022-11-15 23:57:22 +0000223
Gavin Makea2e3302023-03-11 06:46:20 +0000224 return env
Sam Sacconed6863652022-11-15 23:57:22 +0000225
226
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700227class GitCommand(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000228 """Wrapper around a single git invocation."""
Mike Frysinger790f4ce2020-12-07 22:04:55 -0500229
Gavin Makea2e3302023-03-11 06:46:20 +0000230 def __init__(
231 self,
232 project,
233 cmdv,
234 bare=False,
235 input=None,
236 capture_stdout=False,
237 capture_stderr=False,
238 merge_output=False,
239 disable_editor=False,
240 ssh_proxy=None,
241 cwd=None,
242 gitdir=None,
243 objdir=None,
Jason Changa6413f52023-07-26 13:23:40 -0700244 verify_command=False,
Gavin Makea2e3302023-03-11 06:46:20 +0000245 ):
246 if project:
247 if not cwd:
248 cwd = project.worktree
249 if not gitdir:
250 gitdir = project.gitdir
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700251
Jason Changa6413f52023-07-26 13:23:40 -0700252 self.project = project
253 self.cmdv = cmdv
254 self.verify_command = verify_command
255
Gavin Makea2e3302023-03-11 06:46:20 +0000256 # Git on Windows wants its paths only using / for reliability.
257 if platform_utils.isWindows():
258 if objdir:
259 objdir = objdir.replace("\\", "/")
260 if gitdir:
261 gitdir = gitdir.replace("\\", "/")
Sam Sacconed6863652022-11-15 23:57:22 +0000262
Gavin Makea2e3302023-03-11 06:46:20 +0000263 env = _build_env(
264 disable_editor=disable_editor,
265 ssh_proxy=ssh_proxy,
266 objdir=objdir,
267 gitdir=gitdir,
268 bare=bare,
269 )
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500270
Gavin Makea2e3302023-03-11 06:46:20 +0000271 command = [GIT]
272 if bare:
273 cwd = None
274 command.append(cmdv[0])
275 # Need to use the --progress flag for fetch/clone so output will be
276 # displayed as by default git only does progress output if stderr is a
277 # TTY.
278 if sys.stderr.isatty() and cmdv[0] in ("fetch", "clone"):
279 if "--progress" not in cmdv and "--quiet" not in cmdv:
280 command.append("--progress")
281 command.extend(cmdv[1:])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700282
Gavin Makea2e3302023-03-11 06:46:20 +0000283 stdin = subprocess.PIPE if input else None
284 stdout = subprocess.PIPE if capture_stdout else None
285 stderr = (
286 subprocess.STDOUT
287 if merge_output
288 else (subprocess.PIPE if capture_stderr else None)
289 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700290
Gavin Makea2e3302023-03-11 06:46:20 +0000291 dbg = ""
292 if IsTrace():
293 global LAST_CWD
294 global LAST_GITDIR
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700295
Gavin Makea2e3302023-03-11 06:46:20 +0000296 if cwd and LAST_CWD != cwd:
297 if LAST_GITDIR or LAST_CWD:
298 dbg += "\n"
299 dbg += ": cd %s\n" % cwd
300 LAST_CWD = cwd
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700301
Gavin Makea2e3302023-03-11 06:46:20 +0000302 if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
303 if LAST_GITDIR or LAST_CWD:
304 dbg += "\n"
305 dbg += ": export GIT_DIR=%s\n" % env[GIT_DIR]
306 LAST_GITDIR = env[GIT_DIR]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700307
Gavin Makea2e3302023-03-11 06:46:20 +0000308 if "GIT_OBJECT_DIRECTORY" in env:
309 dbg += (
310 ": export GIT_OBJECT_DIRECTORY=%s\n"
311 % env["GIT_OBJECT_DIRECTORY"]
312 )
313 if "GIT_ALTERNATE_OBJECT_DIRECTORIES" in env:
314 dbg += ": export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n" % (
315 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"]
316 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700317
Gavin Makea2e3302023-03-11 06:46:20 +0000318 dbg += ": "
319 dbg += " ".join(command)
320 if stdin == subprocess.PIPE:
321 dbg += " 0<|"
322 if stdout == subprocess.PIPE:
323 dbg += " 1>|"
324 if stderr == subprocess.PIPE:
325 dbg += " 2>|"
326 elif stderr == subprocess.STDOUT:
327 dbg += " 2>&1"
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500328
Gavin Makea2e3302023-03-11 06:46:20 +0000329 with Trace(
330 "git command %s %s with debug: %s", LAST_GITDIR, command, dbg
331 ):
332 try:
333 p = subprocess.Popen(
334 command,
335 cwd=cwd,
336 env=env,
337 encoding="utf-8",
338 errors="backslashreplace",
339 stdin=stdin,
340 stdout=stdout,
341 stderr=stderr,
342 )
343 except Exception as e:
Jason Changa6413f52023-07-26 13:23:40 -0700344 raise GitCommandError(
345 message="%s: %s" % (command[1], e),
346 project=project.name if project else None,
347 command_args=cmdv,
348 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700349
Gavin Makea2e3302023-03-11 06:46:20 +0000350 if ssh_proxy:
351 ssh_proxy.add_client(p)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700352
Gavin Makea2e3302023-03-11 06:46:20 +0000353 self.process = p
Joanna Wanga6c52f52022-11-03 16:51:19 -0400354
Gavin Makea2e3302023-03-11 06:46:20 +0000355 try:
356 self.stdout, self.stderr = p.communicate(input=input)
357 finally:
358 if ssh_proxy:
359 ssh_proxy.remove_client(p)
360 self.rc = p.wait()
Joanna Wanga6c52f52022-11-03 16:51:19 -0400361
Gavin Makea2e3302023-03-11 06:46:20 +0000362 @staticmethod
363 def _GetBasicEnv():
364 """Return a basic env for running git under.
Mike Frysingerc5bbea82021-02-16 15:45:19 -0500365
Gavin Makea2e3302023-03-11 06:46:20 +0000366 This is guaranteed to be side-effect free.
367 """
368 env = os.environ.copy()
369 for key in (
370 REPO_TRACE,
371 GIT_DIR,
372 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
373 "GIT_OBJECT_DIRECTORY",
374 "GIT_WORK_TREE",
375 "GIT_GRAFT_FILE",
376 "GIT_INDEX_FILE",
377 ):
378 env.pop(key, None)
379 return env
Mike Frysinger71b0f312019-09-30 22:39:49 -0400380
Gavin Makea2e3302023-03-11 06:46:20 +0000381 def Wait(self):
Jason Changa6413f52023-07-26 13:23:40 -0700382 if not self.verify_command or self.rc == 0:
383 return self.rc
384
385 stdout = (
386 "\n".join(self.stdout.split("\n")[:GIT_ERROR_STDOUT_LINES])
387 if self.stdout
388 else None
389 )
390
391 stderr = (
392 "\n".join(self.stderr.split("\n")[:GIT_ERROR_STDERR_LINES])
393 if self.stderr
394 else None
395 )
396 project = self.project.name if self.project else None
397 raise GitCommandError(
398 project=project,
399 command_args=self.cmdv,
400 git_rc=self.rc,
401 git_stdout=stdout,
402 git_stderr=stderr,
403 )
404
405
406class GitCommandError(GitError):
407 """
408 Error raised from a failed git command.
409 Note that GitError can refer to any Git related error (e.g. branch not
410 specified for project.py 'UploadForReview'), while GitCommandError is
411 raised exclusively from non-zero exit codes returned from git commands.
412 """
413
414 def __init__(
415 self,
416 message: str = DEFAULT_GIT_FAIL_MESSAGE,
417 git_rc: int = None,
418 git_stdout: str = None,
419 git_stderr: str = None,
420 **kwargs,
421 ):
422 super().__init__(
423 message,
424 **kwargs,
425 )
426 self.git_rc = git_rc
427 self.git_stdout = git_stdout
428 self.git_stderr = git_stderr
429
430 def __str__(self):
431 args = "[]" if not self.command_args else " ".join(self.command_args)
432 error_type = type(self).__name__
433 return f"""{error_type}: {self.message}
434 Project: {self.project}
435 Args: {args}
436 Stdout:
437{self.git_stdout}
438 Stderr:
439{self.git_stderr}"""