blob: c7245adef414b7e3367984954010cab15d31ea13 [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
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070043
David Pursehouse819827a2020-02-12 15:20:19 +090044
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070045class _GitCall(object):
Gavin Makea2e3302023-03-11 06:46:20 +000046 @functools.lru_cache(maxsize=None)
47 def version_tuple(self):
48 ret = Wrapper().ParseGitVersion()
49 if ret is None:
50 print("fatal: unable to detect git version", file=sys.stderr)
51 sys.exit(1)
52 return ret
Shawn O. Pearce334851e2011-09-19 08:05:31 -070053
Gavin Makea2e3302023-03-11 06:46:20 +000054 def __getattr__(self, name):
55 name = name.replace("_", "-")
David Pursehouse819827a2020-02-12 15:20:19 +090056
Gavin Makea2e3302023-03-11 06:46:20 +000057 def fun(*cmdv):
58 command = [name]
59 command.extend(cmdv)
60 return GitCommand(None, command).Wait() == 0
61
62 return fun
David Pursehouse819827a2020-02-12 15:20:19 +090063
64
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070065git = _GitCall()
66
Mike Frysinger369814b2019-07-10 17:10:07 -040067
Mike Frysinger71b0f312019-09-30 22:39:49 -040068def RepoSourceVersion():
Gavin Makea2e3302023-03-11 06:46:20 +000069 """Return the version of the repo.git tree."""
70 ver = getattr(RepoSourceVersion, "version", None)
Mike Frysinger369814b2019-07-10 17:10:07 -040071
Gavin Makea2e3302023-03-11 06:46:20 +000072 # We avoid GitCommand so we don't run into circular deps -- GitCommand needs
73 # to initialize version info we provide.
74 if ver is None:
75 env = GitCommand._GetBasicEnv()
Mike Frysinger71b0f312019-09-30 22:39:49 -040076
Gavin Makea2e3302023-03-11 06:46:20 +000077 proj = os.path.dirname(os.path.abspath(__file__))
78 env[GIT_DIR] = os.path.join(proj, ".git")
79 result = subprocess.run(
80 [GIT, "describe", HEAD],
81 stdout=subprocess.PIPE,
82 stderr=subprocess.DEVNULL,
83 encoding="utf-8",
84 env=env,
85 check=False,
86 )
87 if result.returncode == 0:
88 ver = result.stdout.strip()
89 if ver.startswith("v"):
90 ver = ver[1:]
91 else:
92 ver = "unknown"
93 setattr(RepoSourceVersion, "version", ver)
Mike Frysinger71b0f312019-09-30 22:39:49 -040094
Gavin Makea2e3302023-03-11 06:46:20 +000095 return ver
Mike Frysinger71b0f312019-09-30 22:39:49 -040096
97
98class UserAgent(object):
Gavin Makea2e3302023-03-11 06:46:20 +000099 """Mange User-Agent settings when talking to external services
Mike Frysinger369814b2019-07-10 17:10:07 -0400100
Gavin Makea2e3302023-03-11 06:46:20 +0000101 We follow the style as documented here:
102 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
103 """
Mike Frysinger369814b2019-07-10 17:10:07 -0400104
Gavin Makea2e3302023-03-11 06:46:20 +0000105 _os = None
106 _repo_ua = None
107 _git_ua = None
Mike Frysinger369814b2019-07-10 17:10:07 -0400108
Gavin Makea2e3302023-03-11 06:46:20 +0000109 @property
110 def os(self):
111 """The operating system name."""
112 if self._os is None:
113 os_name = sys.platform
114 if os_name.lower().startswith("linux"):
115 os_name = "Linux"
116 elif os_name == "win32":
117 os_name = "Win32"
118 elif os_name == "cygwin":
119 os_name = "Cygwin"
120 elif os_name == "darwin":
121 os_name = "Darwin"
122 self._os = os_name
Mike Frysinger369814b2019-07-10 17:10:07 -0400123
Gavin Makea2e3302023-03-11 06:46:20 +0000124 return self._os
Mike Frysinger369814b2019-07-10 17:10:07 -0400125
Gavin Makea2e3302023-03-11 06:46:20 +0000126 @property
127 def repo(self):
128 """The UA when connecting directly from repo."""
129 if self._repo_ua is None:
130 py_version = sys.version_info
131 self._repo_ua = "git-repo/%s (%s) git/%s Python/%d.%d.%d" % (
132 RepoSourceVersion(),
133 self.os,
134 git.version_tuple().full,
135 py_version.major,
136 py_version.minor,
137 py_version.micro,
138 )
Mike Frysinger369814b2019-07-10 17:10:07 -0400139
Gavin Makea2e3302023-03-11 06:46:20 +0000140 return self._repo_ua
Mike Frysinger369814b2019-07-10 17:10:07 -0400141
Gavin Makea2e3302023-03-11 06:46:20 +0000142 @property
143 def git(self):
144 """The UA when running git."""
145 if self._git_ua is None:
146 self._git_ua = "git/%s (%s) git-repo/%s" % (
147 git.version_tuple().full,
148 self.os,
149 RepoSourceVersion(),
150 )
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400151
Gavin Makea2e3302023-03-11 06:46:20 +0000152 return self._git_ua
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400153
David Pursehouse819827a2020-02-12 15:20:19 +0900154
Mike Frysinger71b0f312019-09-30 22:39:49 -0400155user_agent = UserAgent()
Mike Frysinger369814b2019-07-10 17:10:07 -0400156
David Pursehouse819827a2020-02-12 15:20:19 +0900157
Gavin Makea2e3302023-03-11 06:46:20 +0000158def git_require(min_version, fail=False, msg=""):
159 git_version = git.version_tuple()
160 if min_version <= git_version:
161 return True
162 if fail:
163 need = ".".join(map(str, min_version))
164 if msg:
165 msg = " for " + msg
166 print(
167 "fatal: git %s or later required%s" % (need, msg), file=sys.stderr
168 )
169 sys.exit(1)
170 return False
Shawn O. Pearce2ec00b92009-06-12 09:32:50 -0700171
David Pursehouse819827a2020-02-12 15:20:19 +0900172
Sam Sacconed6863652022-11-15 23:57:22 +0000173def _build_env(
Sergiy Belozorov78e82ec2023-01-05 18:57:31 +0100174 _kwargs_only=(),
175 bare: Optional[bool] = False,
176 disable_editor: Optional[bool] = False,
177 ssh_proxy: Optional[Any] = None,
178 gitdir: Optional[str] = None,
Gavin Makea2e3302023-03-11 06:46:20 +0000179 objdir: Optional[str] = None,
Sam Sacconed6863652022-11-15 23:57:22 +0000180):
Gavin Makea2e3302023-03-11 06:46:20 +0000181 """Constucts an env dict for command execution."""
Sam Sacconed6863652022-11-15 23:57:22 +0000182
Gavin Makea2e3302023-03-11 06:46:20 +0000183 assert _kwargs_only == (), "_build_env only accepts keyword arguments."
Sam Sacconed6863652022-11-15 23:57:22 +0000184
Gavin Makea2e3302023-03-11 06:46:20 +0000185 env = GitCommand._GetBasicEnv()
Sam Sacconed6863652022-11-15 23:57:22 +0000186
Gavin Makea2e3302023-03-11 06:46:20 +0000187 if disable_editor:
188 env["GIT_EDITOR"] = ":"
189 if ssh_proxy:
190 env["REPO_SSH_SOCK"] = ssh_proxy.sock()
191 env["GIT_SSH"] = ssh_proxy.proxy
192 env["GIT_SSH_VARIANT"] = "ssh"
193 if "http_proxy" in env and "darwin" == sys.platform:
194 s = "'http.proxy=%s'" % (env["http_proxy"],)
195 p = env.get("GIT_CONFIG_PARAMETERS")
196 if p is not None:
197 s = p + " " + s
198 env["GIT_CONFIG_PARAMETERS"] = s
199 if "GIT_ALLOW_PROTOCOL" not in env:
200 env[
201 "GIT_ALLOW_PROTOCOL"
202 ] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
203 env["GIT_HTTP_USER_AGENT"] = user_agent.git
Sam Sacconed6863652022-11-15 23:57:22 +0000204
Gavin Makea2e3302023-03-11 06:46:20 +0000205 if objdir:
206 # Set to the place we want to save the objects.
207 env["GIT_OBJECT_DIRECTORY"] = objdir
Sam Sacconed6863652022-11-15 23:57:22 +0000208
Gavin Makea2e3302023-03-11 06:46:20 +0000209 alt_objects = os.path.join(gitdir, "objects") if gitdir else None
210 if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(
211 objdir
212 ):
213 # Allow git to search the original place in case of local or unique
214 # refs that git will attempt to resolve even if we aren't fetching
215 # them.
216 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = alt_objects
217 if bare and gitdir is not None:
218 env[GIT_DIR] = gitdir
Sam Sacconed6863652022-11-15 23:57:22 +0000219
Gavin Makea2e3302023-03-11 06:46:20 +0000220 return env
Sam Sacconed6863652022-11-15 23:57:22 +0000221
222
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700223class GitCommand(object):
Gavin Makea2e3302023-03-11 06:46:20 +0000224 """Wrapper around a single git invocation."""
Mike Frysinger790f4ce2020-12-07 22:04:55 -0500225
Gavin Makea2e3302023-03-11 06:46:20 +0000226 def __init__(
227 self,
228 project,
229 cmdv,
230 bare=False,
231 input=None,
232 capture_stdout=False,
233 capture_stderr=False,
234 merge_output=False,
235 disable_editor=False,
236 ssh_proxy=None,
237 cwd=None,
238 gitdir=None,
239 objdir=None,
240 ):
241 if project:
242 if not cwd:
243 cwd = project.worktree
244 if not gitdir:
245 gitdir = project.gitdir
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700246
Gavin Makea2e3302023-03-11 06:46:20 +0000247 # Git on Windows wants its paths only using / for reliability.
248 if platform_utils.isWindows():
249 if objdir:
250 objdir = objdir.replace("\\", "/")
251 if gitdir:
252 gitdir = gitdir.replace("\\", "/")
Sam Sacconed6863652022-11-15 23:57:22 +0000253
Gavin Makea2e3302023-03-11 06:46:20 +0000254 env = _build_env(
255 disable_editor=disable_editor,
256 ssh_proxy=ssh_proxy,
257 objdir=objdir,
258 gitdir=gitdir,
259 bare=bare,
260 )
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500261
Gavin Makea2e3302023-03-11 06:46:20 +0000262 command = [GIT]
263 if bare:
264 cwd = None
265 command.append(cmdv[0])
266 # Need to use the --progress flag for fetch/clone so output will be
267 # displayed as by default git only does progress output if stderr is a
268 # TTY.
269 if sys.stderr.isatty() and cmdv[0] in ("fetch", "clone"):
270 if "--progress" not in cmdv and "--quiet" not in cmdv:
271 command.append("--progress")
272 command.extend(cmdv[1:])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700273
Gavin Makea2e3302023-03-11 06:46:20 +0000274 stdin = subprocess.PIPE if input else None
275 stdout = subprocess.PIPE if capture_stdout else None
276 stderr = (
277 subprocess.STDOUT
278 if merge_output
279 else (subprocess.PIPE if capture_stderr else None)
280 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700281
Gavin Makea2e3302023-03-11 06:46:20 +0000282 dbg = ""
283 if IsTrace():
284 global LAST_CWD
285 global LAST_GITDIR
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700286
Gavin Makea2e3302023-03-11 06:46:20 +0000287 if cwd and LAST_CWD != cwd:
288 if LAST_GITDIR or LAST_CWD:
289 dbg += "\n"
290 dbg += ": cd %s\n" % cwd
291 LAST_CWD = cwd
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700292
Gavin Makea2e3302023-03-11 06:46:20 +0000293 if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
294 if LAST_GITDIR or LAST_CWD:
295 dbg += "\n"
296 dbg += ": export GIT_DIR=%s\n" % env[GIT_DIR]
297 LAST_GITDIR = env[GIT_DIR]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700298
Gavin Makea2e3302023-03-11 06:46:20 +0000299 if "GIT_OBJECT_DIRECTORY" in env:
300 dbg += (
301 ": export GIT_OBJECT_DIRECTORY=%s\n"
302 % env["GIT_OBJECT_DIRECTORY"]
303 )
304 if "GIT_ALTERNATE_OBJECT_DIRECTORIES" in env:
305 dbg += ": export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n" % (
306 env["GIT_ALTERNATE_OBJECT_DIRECTORIES"]
307 )
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700308
Gavin Makea2e3302023-03-11 06:46:20 +0000309 dbg += ": "
310 dbg += " ".join(command)
311 if stdin == subprocess.PIPE:
312 dbg += " 0<|"
313 if stdout == subprocess.PIPE:
314 dbg += " 1>|"
315 if stderr == subprocess.PIPE:
316 dbg += " 2>|"
317 elif stderr == subprocess.STDOUT:
318 dbg += " 2>&1"
Mike Frysinger67d6cdf2021-12-23 17:36:09 -0500319
Gavin Makea2e3302023-03-11 06:46:20 +0000320 with Trace(
321 "git command %s %s with debug: %s", LAST_GITDIR, command, dbg
322 ):
323 try:
324 p = subprocess.Popen(
325 command,
326 cwd=cwd,
327 env=env,
328 encoding="utf-8",
329 errors="backslashreplace",
330 stdin=stdin,
331 stdout=stdout,
332 stderr=stderr,
333 )
334 except Exception as e:
335 raise GitError("%s: %s" % (command[1], e))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700336
Gavin Makea2e3302023-03-11 06:46:20 +0000337 if ssh_proxy:
338 ssh_proxy.add_client(p)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700339
Gavin Makea2e3302023-03-11 06:46:20 +0000340 self.process = p
Joanna Wanga6c52f52022-11-03 16:51:19 -0400341
Gavin Makea2e3302023-03-11 06:46:20 +0000342 try:
343 self.stdout, self.stderr = p.communicate(input=input)
344 finally:
345 if ssh_proxy:
346 ssh_proxy.remove_client(p)
347 self.rc = p.wait()
Joanna Wanga6c52f52022-11-03 16:51:19 -0400348
Gavin Makea2e3302023-03-11 06:46:20 +0000349 @staticmethod
350 def _GetBasicEnv():
351 """Return a basic env for running git under.
Mike Frysingerc5bbea82021-02-16 15:45:19 -0500352
Gavin Makea2e3302023-03-11 06:46:20 +0000353 This is guaranteed to be side-effect free.
354 """
355 env = os.environ.copy()
356 for key in (
357 REPO_TRACE,
358 GIT_DIR,
359 "GIT_ALTERNATE_OBJECT_DIRECTORIES",
360 "GIT_OBJECT_DIRECTORY",
361 "GIT_WORK_TREE",
362 "GIT_GRAFT_FILE",
363 "GIT_INDEX_FILE",
364 ):
365 env.pop(key, None)
366 return env
Mike Frysinger71b0f312019-09-30 22:39:49 -0400367
Gavin Makea2e3302023-03-11 06:46:20 +0000368 def Wait(self):
369 return self.rc