blob: 2b3975ec0cc526cf67c798f42ee4259fc8e59ce6 [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
15import os
Anders Björklund8e0fe192020-02-18 14:08:35 +010016import re
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070017import sys
18import subprocess
Shawn O. Pearcefb231612009-04-10 18:53:46 -070019import tempfile
Shawn O. Pearceca8c32c2010-05-11 18:21:33 -070020from signal import SIGTERM
Renaud Paquay2e702912016-11-01 11:23:38 -070021
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070022from error import GitError
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
28GIT = '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)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070040GIT_DIR = 'GIT_DIR'
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070041
42LAST_GITDIR = None
43LAST_CWD = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070044
Shawn O. Pearcefb231612009-04-10 18:53:46 -070045_ssh_proxy_path = None
46_ssh_sock_path = None
Shawn O. Pearceca8c32c2010-05-11 18:21:33 -070047_ssh_clients = []
Anders Björklund8e0fe192020-02-18 14:08:35 +010048_ssh_version = None
49
50
51def _run_ssh_version():
52 """run ssh -V to display the version number"""
53 return subprocess.check_output(['ssh', '-V'], stderr=subprocess.STDOUT).decode()
54
55
56def _parse_ssh_version(ver_str=None):
57 """parse a ssh version string into a tuple"""
58 if ver_str is None:
59 ver_str = _run_ssh_version()
60 m = re.match(r'^OpenSSH_([0-9.]+)(p[0-9]+)?\s', ver_str)
61 if m:
62 return tuple(int(x) for x in m.group(1).split('.'))
63 else:
64 return ()
65
66
67def ssh_version():
68 """return ssh version as a tuple"""
69 global _ssh_version
70 if _ssh_version is None:
71 try:
72 _ssh_version = _parse_ssh_version()
73 except subprocess.CalledProcessError:
74 print('fatal: unable to detect ssh version', file=sys.stderr)
75 sys.exit(1)
76 return _ssh_version
Shawn O. Pearcefb231612009-04-10 18:53:46 -070077
David Pursehouse819827a2020-02-12 15:20:19 +090078
Nico Sallembien1c85f4e2010-04-27 14:35:27 -070079def ssh_sock(create=True):
Shawn O. Pearcefb231612009-04-10 18:53:46 -070080 global _ssh_sock_path
81 if _ssh_sock_path is None:
82 if not create:
83 return None
Mickaël Salaün2f6ab7f2012-09-30 00:37:55 +020084 tmp_dir = '/tmp'
85 if not os.path.exists(tmp_dir):
86 tmp_dir = tempfile.gettempdir()
Anders Björklund8e0fe192020-02-18 14:08:35 +010087 if ssh_version() < (6, 7):
88 tokens = '%r@%h:%p'
89 else:
90 tokens = '%C' # hash of %l%h%p%r
Shawn O. Pearcefb231612009-04-10 18:53:46 -070091 _ssh_sock_path = os.path.join(
David Pursehouseabdf7502020-02-12 14:58:39 +090092 tempfile.mkdtemp('', 'ssh-', tmp_dir),
Anders Björklund8e0fe192020-02-18 14:08:35 +010093 'master-' + tokens)
Shawn O. Pearcefb231612009-04-10 18:53:46 -070094 return _ssh_sock_path
95
David Pursehouse819827a2020-02-12 15:20:19 +090096
Shawn O. Pearcefb231612009-04-10 18:53:46 -070097def _ssh_proxy():
98 global _ssh_proxy_path
99 if _ssh_proxy_path is None:
100 _ssh_proxy_path = os.path.join(
David Pursehouseabdf7502020-02-12 14:58:39 +0900101 os.path.dirname(__file__),
102 'git_ssh')
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700103 return _ssh_proxy_path
104
David Pursehouse819827a2020-02-12 15:20:19 +0900105
Shawn O. Pearceca8c32c2010-05-11 18:21:33 -0700106def _add_ssh_client(p):
107 _ssh_clients.append(p)
108
David Pursehouse819827a2020-02-12 15:20:19 +0900109
Shawn O. Pearceca8c32c2010-05-11 18:21:33 -0700110def _remove_ssh_client(p):
111 try:
112 _ssh_clients.remove(p)
113 except ValueError:
114 pass
115
David Pursehouse819827a2020-02-12 15:20:19 +0900116
Shawn O. Pearceca8c32c2010-05-11 18:21:33 -0700117def terminate_ssh_clients():
118 global _ssh_clients
119 for p in _ssh_clients:
120 try:
121 os.kill(p.pid, SIGTERM)
122 p.wait()
123 except OSError:
124 pass
125 _ssh_clients = []
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700126
David Pursehouse819827a2020-02-12 15:20:19 +0900127
Shawn O. Pearce334851e2011-09-19 08:05:31 -0700128_git_version = None
129
David Pursehouse819827a2020-02-12 15:20:19 +0900130
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700131class _GitCall(object):
Shawn O. Pearce334851e2011-09-19 08:05:31 -0700132 def version_tuple(self):
133 global _git_version
Shawn O. Pearce334851e2011-09-19 08:05:31 -0700134 if _git_version is None:
Mike Frysingerca540ae2019-07-10 15:42:30 -0400135 _git_version = Wrapper().ParseGitVersion()
Conley Owensff0a3c82014-01-30 14:46:03 -0800136 if _git_version is None:
Mike Frysingerca540ae2019-07-10 15:42:30 -0400137 print('fatal: unable to detect git version', file=sys.stderr)
Shawn O. Pearce334851e2011-09-19 08:05:31 -0700138 sys.exit(1)
139 return _git_version
140
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700141 def __getattr__(self, name):
David Pursehouse54a4e602020-02-12 14:31:05 +0900142 name = name.replace('_', '-')
David Pursehouse819827a2020-02-12 15:20:19 +0900143
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700144 def fun(*cmdv):
145 command = [name]
146 command.extend(cmdv)
147 return GitCommand(None, command).Wait() == 0
148 return fun
David Pursehouse819827a2020-02-12 15:20:19 +0900149
150
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700151git = _GitCall()
152
Mike Frysinger369814b2019-07-10 17:10:07 -0400153
Mike Frysinger71b0f312019-09-30 22:39:49 -0400154def RepoSourceVersion():
155 """Return the version of the repo.git tree."""
156 ver = getattr(RepoSourceVersion, 'version', None)
Mike Frysinger369814b2019-07-10 17:10:07 -0400157
Mike Frysinger71b0f312019-09-30 22:39:49 -0400158 # We avoid GitCommand so we don't run into circular deps -- GitCommand needs
159 # to initialize version info we provide.
160 if ver is None:
161 env = GitCommand._GetBasicEnv()
162
163 proj = os.path.dirname(os.path.abspath(__file__))
164 env[GIT_DIR] = os.path.join(proj, '.git')
165
166 p = subprocess.Popen([GIT, 'describe', HEAD], stdout=subprocess.PIPE,
167 env=env)
168 if p.wait() == 0:
169 ver = p.stdout.read().strip().decode('utf-8')
170 if ver.startswith('v'):
171 ver = ver[1:]
172 else:
173 ver = 'unknown'
174 setattr(RepoSourceVersion, 'version', ver)
175
176 return ver
177
178
179class UserAgent(object):
180 """Mange User-Agent settings when talking to external services
Mike Frysinger369814b2019-07-10 17:10:07 -0400181
182 We follow the style as documented here:
183 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
184 """
Mike Frysinger369814b2019-07-10 17:10:07 -0400185
Mike Frysinger71b0f312019-09-30 22:39:49 -0400186 _os = None
187 _repo_ua = None
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400188 _git_ua = None
Mike Frysinger369814b2019-07-10 17:10:07 -0400189
Mike Frysinger71b0f312019-09-30 22:39:49 -0400190 @property
191 def os(self):
192 """The operating system name."""
193 if self._os is None:
194 os_name = sys.platform
195 if os_name.lower().startswith('linux'):
196 os_name = 'Linux'
197 elif os_name == 'win32':
198 os_name = 'Win32'
199 elif os_name == 'cygwin':
200 os_name = 'Cygwin'
201 elif os_name == 'darwin':
202 os_name = 'Darwin'
203 self._os = os_name
Mike Frysinger369814b2019-07-10 17:10:07 -0400204
Mike Frysinger71b0f312019-09-30 22:39:49 -0400205 return self._os
Mike Frysinger369814b2019-07-10 17:10:07 -0400206
Mike Frysinger71b0f312019-09-30 22:39:49 -0400207 @property
208 def repo(self):
209 """The UA when connecting directly from repo."""
210 if self._repo_ua is None:
211 py_version = sys.version_info
212 self._repo_ua = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % (
213 RepoSourceVersion(),
214 self.os,
215 git.version_tuple().full,
216 py_version.major, py_version.minor, py_version.micro)
Mike Frysinger369814b2019-07-10 17:10:07 -0400217
Mike Frysinger71b0f312019-09-30 22:39:49 -0400218 return self._repo_ua
Mike Frysinger369814b2019-07-10 17:10:07 -0400219
Mike Frysinger2f0951b2019-07-10 17:13:46 -0400220 @property
221 def git(self):
222 """The UA when running git."""
223 if self._git_ua is None:
224 self._git_ua = 'git/%s (%s) git-repo/%s' % (
225 git.version_tuple().full,
226 self.os,
227 RepoSourceVersion())
228
229 return self._git_ua
230
David Pursehouse819827a2020-02-12 15:20:19 +0900231
Mike Frysinger71b0f312019-09-30 22:39:49 -0400232user_agent = UserAgent()
Mike Frysinger369814b2019-07-10 17:10:07 -0400233
David Pursehouse819827a2020-02-12 15:20:19 +0900234
Xin Li745be2e2019-06-03 11:24:30 -0700235def git_require(min_version, fail=False, msg=''):
Shawn O. Pearce334851e2011-09-19 08:05:31 -0700236 git_version = git.version_tuple()
237 if min_version <= git_version:
Shawn O. Pearce2ec00b92009-06-12 09:32:50 -0700238 return True
239 if fail:
David Pursehouse7e6dd2d2012-10-25 12:40:51 +0900240 need = '.'.join(map(str, min_version))
Xin Li745be2e2019-06-03 11:24:30 -0700241 if msg:
242 msg = ' for ' + msg
243 print('fatal: git %s or later required%s' % (need, msg), file=sys.stderr)
Shawn O. Pearce2ec00b92009-06-12 09:32:50 -0700244 sys.exit(1)
245 return False
246
David Pursehouse819827a2020-02-12 15:20:19 +0900247
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700248class GitCommand(object):
249 def __init__(self,
250 project,
251 cmdv,
David Pursehousee5913ae2020-02-12 13:56:59 +0900252 bare=False,
253 provide_stdin=False,
254 capture_stdout=False,
255 capture_stderr=False,
Mike Frysinger31990f02020-02-17 01:35:18 -0500256 merge_output=False,
David Pursehousee5913ae2020-02-12 13:56:59 +0900257 disable_editor=False,
258 ssh_proxy=False,
259 cwd=None,
260 gitdir=None):
Mike Frysinger71b0f312019-09-30 22:39:49 -0400261 env = self._GetBasicEnv()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700262
John L. Villalovos9c76f672015-03-16 20:49:10 -0700263 # If we are not capturing std* then need to print it.
264 self.tee = {'stdout': not capture_stdout, 'stderr': not capture_stderr}
265
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700266 if disable_editor:
Mike Frysinger56ce3462019-12-04 19:30:48 -0500267 env['GIT_EDITOR'] = ':'
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700268 if ssh_proxy:
Mike Frysinger56ce3462019-12-04 19:30:48 -0500269 env['REPO_SSH_SOCK'] = ssh_sock()
270 env['GIT_SSH'] = _ssh_proxy()
271 env['GIT_SSH_VARIANT'] = 'ssh'
Shawn O. Pearce62d0b102012-06-05 15:11:15 -0700272 if 'http_proxy' in env and 'darwin' == sys.platform:
Shawn O. Pearce337aee02012-06-13 10:40:46 -0700273 s = "'http.proxy=%s'" % (env['http_proxy'],)
Shawn O. Pearce62d0b102012-06-05 15:11:15 -0700274 p = env.get('GIT_CONFIG_PARAMETERS')
275 if p is not None:
276 s = p + ' ' + s
Mike Frysinger56ce3462019-12-04 19:30:48 -0500277 env['GIT_CONFIG_PARAMETERS'] = s
Dan Willemsen466b8c42015-11-25 13:26:39 -0800278 if 'GIT_ALLOW_PROTOCOL' not in env:
Mike Frysinger56ce3462019-12-04 19:30:48 -0500279 env['GIT_ALLOW_PROTOCOL'] = (
280 'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc')
281 env['GIT_HTTP_USER_AGENT'] = user_agent.git
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700282
283 if project:
284 if not cwd:
285 cwd = project.worktree
286 if not gitdir:
287 gitdir = project.gitdir
288
289 command = [GIT]
290 if bare:
291 if gitdir:
Mike Frysinger56ce3462019-12-04 19:30:48 -0500292 env[GIT_DIR] = gitdir
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700293 cwd = None
John L. Villalovos9c76f672015-03-16 20:49:10 -0700294 command.append(cmdv[0])
295 # Need to use the --progress flag for fetch/clone so output will be
296 # displayed as by default git only does progress output if stderr is a TTY.
297 if sys.stderr.isatty() and cmdv[0] in ('fetch', 'clone'):
298 if '--progress' not in cmdv and '--quiet' not in cmdv:
299 command.append('--progress')
300 command.extend(cmdv[1:])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700301
302 if provide_stdin:
303 stdin = subprocess.PIPE
304 else:
305 stdin = None
306
John L. Villalovos9c76f672015-03-16 20:49:10 -0700307 stdout = subprocess.PIPE
Mike Frysinger31990f02020-02-17 01:35:18 -0500308 stderr = subprocess.STDOUT if merge_output else subprocess.PIPE
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700309
Shawn O. Pearcead3193a2009-04-18 09:54:51 -0700310 if IsTrace():
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700311 global LAST_CWD
312 global LAST_GITDIR
313
314 dbg = ''
315
316 if cwd and LAST_CWD != cwd:
317 if LAST_GITDIR or LAST_CWD:
318 dbg += '\n'
319 dbg += ': cd %s\n' % cwd
320 LAST_CWD = cwd
321
322 if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
323 if LAST_GITDIR or LAST_CWD:
324 dbg += '\n'
325 dbg += ': export GIT_DIR=%s\n' % env[GIT_DIR]
326 LAST_GITDIR = env[GIT_DIR]
327
328 dbg += ': '
329 dbg += ' '.join(command)
330 if stdin == subprocess.PIPE:
331 dbg += ' 0<|'
332 if stdout == subprocess.PIPE:
333 dbg += ' 1>|'
334 if stderr == subprocess.PIPE:
335 dbg += ' 2>|'
Mike Frysinger31990f02020-02-17 01:35:18 -0500336 elif stderr == subprocess.STDOUT:
337 dbg += ' 2>&1'
Shawn O. Pearcead3193a2009-04-18 09:54:51 -0700338 Trace('%s', dbg)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700339
340 try:
341 p = subprocess.Popen(command,
David Pursehousee5913ae2020-02-12 13:56:59 +0900342 cwd=cwd,
343 env=env,
344 stdin=stdin,
345 stdout=stdout,
346 stderr=stderr)
Sarah Owensa5be53f2012-09-09 15:37:57 -0700347 except Exception as e:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700348 raise GitError('%s: %s' % (command[1], e))
349
Shawn O. Pearceca8c32c2010-05-11 18:21:33 -0700350 if ssh_proxy:
351 _add_ssh_client(p)
352
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700353 self.process = p
354 self.stdin = p.stdin
355
Mike Frysinger71b0f312019-09-30 22:39:49 -0400356 @staticmethod
357 def _GetBasicEnv():
358 """Return a basic env for running git under.
359
360 This is guaranteed to be side-effect free.
361 """
362 env = os.environ.copy()
363 for key in (REPO_TRACE,
364 GIT_DIR,
365 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
366 'GIT_OBJECT_DIRECTORY',
367 'GIT_WORK_TREE',
368 'GIT_GRAFT_FILE',
369 'GIT_INDEX_FILE'):
370 env.pop(key, None)
371 return env
372
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700373 def Wait(self):
Shawn O. Pearceca8c32c2010-05-11 18:21:33 -0700374 try:
Ulrik Sjölin498fe902011-09-11 22:59:37 +0200375 p = self.process
John L. Villalovos9c76f672015-03-16 20:49:10 -0700376 rc = self._CaptureOutput()
Shawn O. Pearceca8c32c2010-05-11 18:21:33 -0700377 finally:
378 _remove_ssh_client(p)
379 return rc
John L. Villalovos9c76f672015-03-16 20:49:10 -0700380
381 def _CaptureOutput(self):
382 p = self.process
Renaud Paquay2e702912016-11-01 11:23:38 -0700383 s_in = platform_utils.FileDescriptorStreams.create()
384 s_in.add(p.stdout, sys.stdout, 'stdout')
Mike Frysinger31990f02020-02-17 01:35:18 -0500385 if p.stderr is not None:
386 s_in.add(p.stderr, sys.stderr, 'stderr')
John L. Villalovos9c76f672015-03-16 20:49:10 -0700387 self.stdout = ''
388 self.stderr = ''
389
Renaud Paquay2e702912016-11-01 11:23:38 -0700390 while not s_in.is_done:
391 in_ready = s_in.select()
John L. Villalovos9c76f672015-03-16 20:49:10 -0700392 for s in in_ready:
Renaud Paquay2e702912016-11-01 11:23:38 -0700393 buf = s.read()
John L. Villalovos9c76f672015-03-16 20:49:10 -0700394 if not buf:
395 s_in.remove(s)
396 continue
Anthony King6cfc68e2015-06-03 16:39:32 +0100397 if not hasattr(buf, 'encode'):
Gaurav Pathakdb3128f2021-01-26 18:10:43 +0530398 buf = buf.decode('utf-8', 'backslashreplace')
John L. Villalovos9c76f672015-03-16 20:49:10 -0700399 if s.std_name == 'stdout':
400 self.stdout += buf
401 else:
402 self.stderr += buf
403 if self.tee[s.std_name]:
404 s.dest.write(buf)
405 s.dest.flush()
406 return p.wait()