git_command.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. # 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. import os
  15. import re
  16. import sys
  17. import subprocess
  18. import tempfile
  19. from signal import SIGTERM
  20. from error import GitError
  21. from git_refs import HEAD
  22. import platform_utils
  23. from repo_trace import REPO_TRACE, IsTrace, Trace
  24. from wrapper import Wrapper
  25. GIT = 'git'
  26. # NB: These do not need to be kept in sync with the repo launcher script.
  27. # These may be much newer as it allows the repo launcher to roll between
  28. # different repo releases while source versions might require a newer git.
  29. #
  30. # The soft version is when we start warning users that the version is old and
  31. # we'll be dropping support for it. We'll refuse to work with versions older
  32. # than the hard version.
  33. #
  34. # git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
  35. MIN_GIT_VERSION_SOFT = (1, 9, 1)
  36. MIN_GIT_VERSION_HARD = (1, 7, 2)
  37. GIT_DIR = 'GIT_DIR'
  38. LAST_GITDIR = None
  39. LAST_CWD = None
  40. _ssh_proxy_path = None
  41. _ssh_sock_path = None
  42. _ssh_clients = []
  43. _ssh_version = None
  44. def _run_ssh_version():
  45. """run ssh -V to display the version number"""
  46. return subprocess.check_output(['ssh', '-V'], stderr=subprocess.STDOUT).decode()
  47. def _parse_ssh_version(ver_str=None):
  48. """parse a ssh version string into a tuple"""
  49. if ver_str is None:
  50. ver_str = _run_ssh_version()
  51. m = re.match(r'^OpenSSH_([0-9.]+)(p[0-9]+)?\s', ver_str)
  52. if m:
  53. return tuple(int(x) for x in m.group(1).split('.'))
  54. else:
  55. return ()
  56. def ssh_version():
  57. """return ssh version as a tuple"""
  58. global _ssh_version
  59. if _ssh_version is None:
  60. try:
  61. _ssh_version = _parse_ssh_version()
  62. except subprocess.CalledProcessError:
  63. print('fatal: unable to detect ssh version', file=sys.stderr)
  64. sys.exit(1)
  65. return _ssh_version
  66. def ssh_sock(create=True):
  67. global _ssh_sock_path
  68. if _ssh_sock_path is None:
  69. if not create:
  70. return None
  71. tmp_dir = '/tmp'
  72. if not os.path.exists(tmp_dir):
  73. tmp_dir = tempfile.gettempdir()
  74. if ssh_version() < (6, 7):
  75. tokens = '%r@%h:%p'
  76. else:
  77. tokens = '%C' # hash of %l%h%p%r
  78. _ssh_sock_path = os.path.join(
  79. tempfile.mkdtemp('', 'ssh-', tmp_dir),
  80. 'master-' + tokens)
  81. return _ssh_sock_path
  82. def _ssh_proxy():
  83. global _ssh_proxy_path
  84. if _ssh_proxy_path is None:
  85. _ssh_proxy_path = os.path.join(
  86. os.path.dirname(__file__),
  87. 'git_ssh')
  88. return _ssh_proxy_path
  89. def _add_ssh_client(p):
  90. _ssh_clients.append(p)
  91. def _remove_ssh_client(p):
  92. try:
  93. _ssh_clients.remove(p)
  94. except ValueError:
  95. pass
  96. def terminate_ssh_clients():
  97. global _ssh_clients
  98. for p in _ssh_clients:
  99. try:
  100. os.kill(p.pid, SIGTERM)
  101. p.wait()
  102. except OSError:
  103. pass
  104. _ssh_clients = []
  105. _git_version = None
  106. class _GitCall(object):
  107. def version_tuple(self):
  108. global _git_version
  109. if _git_version is None:
  110. _git_version = Wrapper().ParseGitVersion()
  111. if _git_version is None:
  112. print('fatal: unable to detect git version', file=sys.stderr)
  113. sys.exit(1)
  114. return _git_version
  115. def __getattr__(self, name):
  116. name = name.replace('_', '-')
  117. def fun(*cmdv):
  118. command = [name]
  119. command.extend(cmdv)
  120. return GitCommand(None, command).Wait() == 0
  121. return fun
  122. git = _GitCall()
  123. def RepoSourceVersion():
  124. """Return the version of the repo.git tree."""
  125. ver = getattr(RepoSourceVersion, 'version', None)
  126. # We avoid GitCommand so we don't run into circular deps -- GitCommand needs
  127. # to initialize version info we provide.
  128. if ver is None:
  129. env = GitCommand._GetBasicEnv()
  130. proj = os.path.dirname(os.path.abspath(__file__))
  131. env[GIT_DIR] = os.path.join(proj, '.git')
  132. p = subprocess.Popen([GIT, 'describe', HEAD], stdout=subprocess.PIPE,
  133. env=env)
  134. if p.wait() == 0:
  135. ver = p.stdout.read().strip().decode('utf-8')
  136. if ver.startswith('v'):
  137. ver = ver[1:]
  138. else:
  139. ver = 'unknown'
  140. setattr(RepoSourceVersion, 'version', ver)
  141. return ver
  142. class UserAgent(object):
  143. """Mange User-Agent settings when talking to external services
  144. We follow the style as documented here:
  145. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
  146. """
  147. _os = None
  148. _repo_ua = None
  149. _git_ua = None
  150. @property
  151. def os(self):
  152. """The operating system name."""
  153. if self._os is None:
  154. os_name = sys.platform
  155. if os_name.lower().startswith('linux'):
  156. os_name = 'Linux'
  157. elif os_name == 'win32':
  158. os_name = 'Win32'
  159. elif os_name == 'cygwin':
  160. os_name = 'Cygwin'
  161. elif os_name == 'darwin':
  162. os_name = 'Darwin'
  163. self._os = os_name
  164. return self._os
  165. @property
  166. def repo(self):
  167. """The UA when connecting directly from repo."""
  168. if self._repo_ua is None:
  169. py_version = sys.version_info
  170. self._repo_ua = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % (
  171. RepoSourceVersion(),
  172. self.os,
  173. git.version_tuple().full,
  174. py_version.major, py_version.minor, py_version.micro)
  175. return self._repo_ua
  176. @property
  177. def git(self):
  178. """The UA when running git."""
  179. if self._git_ua is None:
  180. self._git_ua = 'git/%s (%s) git-repo/%s' % (
  181. git.version_tuple().full,
  182. self.os,
  183. RepoSourceVersion())
  184. return self._git_ua
  185. user_agent = UserAgent()
  186. def git_require(min_version, fail=False, msg=''):
  187. git_version = git.version_tuple()
  188. if min_version <= git_version:
  189. return True
  190. if fail:
  191. need = '.'.join(map(str, min_version))
  192. if msg:
  193. msg = ' for ' + msg
  194. print('fatal: git %s or later required%s' % (need, msg), file=sys.stderr)
  195. sys.exit(1)
  196. return False
  197. class GitCommand(object):
  198. def __init__(self,
  199. project,
  200. cmdv,
  201. bare=False,
  202. provide_stdin=False,
  203. capture_stdout=False,
  204. capture_stderr=False,
  205. merge_output=False,
  206. disable_editor=False,
  207. ssh_proxy=False,
  208. cwd=None,
  209. gitdir=None):
  210. env = self._GetBasicEnv()
  211. # If we are not capturing std* then need to print it.
  212. self.tee = {'stdout': not capture_stdout, 'stderr': not capture_stderr}
  213. if disable_editor:
  214. env['GIT_EDITOR'] = ':'
  215. if ssh_proxy:
  216. env['REPO_SSH_SOCK'] = ssh_sock()
  217. env['GIT_SSH'] = _ssh_proxy()
  218. env['GIT_SSH_VARIANT'] = 'ssh'
  219. if 'http_proxy' in env and 'darwin' == sys.platform:
  220. s = "'http.proxy=%s'" % (env['http_proxy'],)
  221. p = env.get('GIT_CONFIG_PARAMETERS')
  222. if p is not None:
  223. s = p + ' ' + s
  224. env['GIT_CONFIG_PARAMETERS'] = s
  225. if 'GIT_ALLOW_PROTOCOL' not in env:
  226. env['GIT_ALLOW_PROTOCOL'] = (
  227. 'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc')
  228. env['GIT_HTTP_USER_AGENT'] = user_agent.git
  229. if project:
  230. if not cwd:
  231. cwd = project.worktree
  232. if not gitdir:
  233. gitdir = project.gitdir
  234. command = [GIT]
  235. if bare:
  236. if gitdir:
  237. env[GIT_DIR] = gitdir
  238. cwd = None
  239. command.append(cmdv[0])
  240. # Need to use the --progress flag for fetch/clone so output will be
  241. # displayed as by default git only does progress output if stderr is a TTY.
  242. if sys.stderr.isatty() and cmdv[0] in ('fetch', 'clone'):
  243. if '--progress' not in cmdv and '--quiet' not in cmdv:
  244. command.append('--progress')
  245. command.extend(cmdv[1:])
  246. if provide_stdin:
  247. stdin = subprocess.PIPE
  248. else:
  249. stdin = None
  250. stdout = subprocess.PIPE
  251. stderr = subprocess.STDOUT if merge_output else subprocess.PIPE
  252. if IsTrace():
  253. global LAST_CWD
  254. global LAST_GITDIR
  255. dbg = ''
  256. if cwd and LAST_CWD != cwd:
  257. if LAST_GITDIR or LAST_CWD:
  258. dbg += '\n'
  259. dbg += ': cd %s\n' % cwd
  260. LAST_CWD = cwd
  261. if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
  262. if LAST_GITDIR or LAST_CWD:
  263. dbg += '\n'
  264. dbg += ': export GIT_DIR=%s\n' % env[GIT_DIR]
  265. LAST_GITDIR = env[GIT_DIR]
  266. dbg += ': '
  267. dbg += ' '.join(command)
  268. if stdin == subprocess.PIPE:
  269. dbg += ' 0<|'
  270. if stdout == subprocess.PIPE:
  271. dbg += ' 1>|'
  272. if stderr == subprocess.PIPE:
  273. dbg += ' 2>|'
  274. elif stderr == subprocess.STDOUT:
  275. dbg += ' 2>&1'
  276. Trace('%s', dbg)
  277. try:
  278. p = subprocess.Popen(command,
  279. cwd=cwd,
  280. env=env,
  281. stdin=stdin,
  282. stdout=stdout,
  283. stderr=stderr)
  284. except Exception as e:
  285. raise GitError('%s: %s' % (command[1], e))
  286. if ssh_proxy:
  287. _add_ssh_client(p)
  288. self.process = p
  289. self.stdin = p.stdin
  290. @staticmethod
  291. def _GetBasicEnv():
  292. """Return a basic env for running git under.
  293. This is guaranteed to be side-effect free.
  294. """
  295. env = os.environ.copy()
  296. for key in (REPO_TRACE,
  297. GIT_DIR,
  298. 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
  299. 'GIT_OBJECT_DIRECTORY',
  300. 'GIT_WORK_TREE',
  301. 'GIT_GRAFT_FILE',
  302. 'GIT_INDEX_FILE'):
  303. env.pop(key, None)
  304. return env
  305. def Wait(self):
  306. try:
  307. p = self.process
  308. rc = self._CaptureOutput()
  309. finally:
  310. _remove_ssh_client(p)
  311. return rc
  312. def _CaptureOutput(self):
  313. p = self.process
  314. s_in = platform_utils.FileDescriptorStreams.create()
  315. s_in.add(p.stdout, sys.stdout, 'stdout')
  316. if p.stderr is not None:
  317. s_in.add(p.stderr, sys.stderr, 'stderr')
  318. self.stdout = ''
  319. self.stderr = ''
  320. while not s_in.is_done:
  321. in_ready = s_in.select()
  322. for s in in_ready:
  323. buf = s.read()
  324. if not buf:
  325. s_in.remove(s)
  326. continue
  327. if not hasattr(buf, 'encode'):
  328. buf = buf.decode()
  329. if s.std_name == 'stdout':
  330. self.stdout += buf
  331. else:
  332. self.stderr += buf
  333. if self.tee[s.std_name]:
  334. s.dest.write(buf)
  335. s.dest.flush()
  336. return p.wait()