diff options
Diffstat (limited to 'git_command.py')
| -rw-r--r-- | git_command.py | 209 |
1 files changed, 77 insertions, 132 deletions
diff --git a/git_command.py b/git_command.py index dc542c36..95db91f2 100644 --- a/git_command.py +++ b/git_command.py | |||
| @@ -1,5 +1,3 @@ | |||
| 1 | # -*- coding:utf-8 -*- | ||
| 2 | # | ||
| 3 | # Copyright (C) 2008 The Android Open Source Project | 1 | # Copyright (C) 2008 The Android Open Source Project |
| 4 | # | 2 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| @@ -14,12 +12,10 @@ | |||
| 14 | # See the License for the specific language governing permissions and | 12 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. | 13 | # limitations under the License. |
| 16 | 14 | ||
| 17 | from __future__ import print_function | 15 | import functools |
| 18 | import os | 16 | import os |
| 19 | import sys | 17 | import sys |
| 20 | import subprocess | 18 | import subprocess |
| 21 | import tempfile | ||
| 22 | from signal import SIGTERM | ||
| 23 | 19 | ||
| 24 | from error import GitError | 20 | from error import GitError |
| 25 | from git_refs import HEAD | 21 | from git_refs import HEAD |
| @@ -28,75 +24,42 @@ from repo_trace import REPO_TRACE, IsTrace, Trace | |||
| 28 | from wrapper import Wrapper | 24 | from wrapper import Wrapper |
| 29 | 25 | ||
| 30 | GIT = 'git' | 26 | GIT = 'git' |
| 31 | MIN_GIT_VERSION = (1, 5, 4) | 27 | # NB: These do not need to be kept in sync with the repo launcher script. |
| 28 | # These may be much newer as it allows the repo launcher to roll between | ||
| 29 | # different repo releases while source versions might require a newer git. | ||
| 30 | # | ||
| 31 | # The soft version is when we start warning users that the version is old and | ||
| 32 | # we'll be dropping support for it. We'll refuse to work with versions older | ||
| 33 | # than the hard version. | ||
| 34 | # | ||
| 35 | # git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty. | ||
| 36 | MIN_GIT_VERSION_SOFT = (1, 9, 1) | ||
| 37 | MIN_GIT_VERSION_HARD = (1, 7, 2) | ||
| 32 | GIT_DIR = 'GIT_DIR' | 38 | GIT_DIR = 'GIT_DIR' |
| 33 | 39 | ||
| 34 | LAST_GITDIR = None | 40 | LAST_GITDIR = None |
| 35 | LAST_CWD = None | 41 | LAST_CWD = None |
| 36 | 42 | ||
| 37 | _ssh_proxy_path = None | ||
| 38 | _ssh_sock_path = None | ||
| 39 | _ssh_clients = [] | ||
| 40 | |||
| 41 | def ssh_sock(create=True): | ||
| 42 | global _ssh_sock_path | ||
| 43 | if _ssh_sock_path is None: | ||
| 44 | if not create: | ||
| 45 | return None | ||
| 46 | tmp_dir = '/tmp' | ||
| 47 | if not os.path.exists(tmp_dir): | ||
| 48 | tmp_dir = tempfile.gettempdir() | ||
| 49 | _ssh_sock_path = os.path.join( | ||
| 50 | tempfile.mkdtemp('', 'ssh-', tmp_dir), | ||
| 51 | 'master-%r@%h:%p') | ||
| 52 | return _ssh_sock_path | ||
| 53 | |||
| 54 | def _ssh_proxy(): | ||
| 55 | global _ssh_proxy_path | ||
| 56 | if _ssh_proxy_path is None: | ||
| 57 | _ssh_proxy_path = os.path.join( | ||
| 58 | os.path.dirname(__file__), | ||
| 59 | 'git_ssh') | ||
| 60 | return _ssh_proxy_path | ||
| 61 | |||
| 62 | def _add_ssh_client(p): | ||
| 63 | _ssh_clients.append(p) | ||
| 64 | |||
| 65 | def _remove_ssh_client(p): | ||
| 66 | try: | ||
| 67 | _ssh_clients.remove(p) | ||
| 68 | except ValueError: | ||
| 69 | pass | ||
| 70 | |||
| 71 | def terminate_ssh_clients(): | ||
| 72 | global _ssh_clients | ||
| 73 | for p in _ssh_clients: | ||
| 74 | try: | ||
| 75 | os.kill(p.pid, SIGTERM) | ||
| 76 | p.wait() | ||
| 77 | except OSError: | ||
| 78 | pass | ||
| 79 | _ssh_clients = [] | ||
| 80 | |||
| 81 | _git_version = None | ||
| 82 | 43 | ||
| 83 | class _GitCall(object): | 44 | class _GitCall(object): |
| 45 | @functools.lru_cache(maxsize=None) | ||
| 84 | def version_tuple(self): | 46 | def version_tuple(self): |
| 85 | global _git_version | 47 | ret = Wrapper().ParseGitVersion() |
| 86 | if _git_version is None: | 48 | if ret is None: |
| 87 | _git_version = Wrapper().ParseGitVersion() | 49 | print('fatal: unable to detect git version', file=sys.stderr) |
| 88 | if _git_version is None: | 50 | sys.exit(1) |
| 89 | print('fatal: unable to detect git version', file=sys.stderr) | 51 | return ret |
| 90 | sys.exit(1) | ||
| 91 | return _git_version | ||
| 92 | 52 | ||
| 93 | def __getattr__(self, name): | 53 | def __getattr__(self, name): |
| 94 | name = name.replace('_','-') | 54 | name = name.replace('_', '-') |
| 55 | |||
| 95 | def fun(*cmdv): | 56 | def fun(*cmdv): |
| 96 | command = [name] | 57 | command = [name] |
| 97 | command.extend(cmdv) | 58 | command.extend(cmdv) |
| 98 | return GitCommand(None, command).Wait() == 0 | 59 | return GitCommand(None, command).Wait() == 0 |
| 99 | return fun | 60 | return fun |
| 61 | |||
| 62 | |||
| 100 | git = _GitCall() | 63 | git = _GitCall() |
| 101 | 64 | ||
| 102 | 65 | ||
| @@ -111,11 +74,11 @@ def RepoSourceVersion(): | |||
| 111 | 74 | ||
| 112 | proj = os.path.dirname(os.path.abspath(__file__)) | 75 | proj = os.path.dirname(os.path.abspath(__file__)) |
| 113 | env[GIT_DIR] = os.path.join(proj, '.git') | 76 | env[GIT_DIR] = os.path.join(proj, '.git') |
| 114 | 77 | result = subprocess.run([GIT, 'describe', HEAD], stdout=subprocess.PIPE, | |
| 115 | p = subprocess.Popen([GIT, 'describe', HEAD], stdout=subprocess.PIPE, | 78 | stderr=subprocess.DEVNULL, encoding='utf-8', |
| 116 | env=env) | 79 | env=env, check=False) |
| 117 | if p.wait() == 0: | 80 | if result.returncode == 0: |
| 118 | ver = p.stdout.read().strip().decode('utf-8') | 81 | ver = result.stdout.strip() |
| 119 | if ver.startswith('v'): | 82 | if ver.startswith('v'): |
| 120 | ver = ver[1:] | 83 | ver = ver[1:] |
| 121 | else: | 84 | else: |
| @@ -177,8 +140,10 @@ class UserAgent(object): | |||
| 177 | 140 | ||
| 178 | return self._git_ua | 141 | return self._git_ua |
| 179 | 142 | ||
| 143 | |||
| 180 | user_agent = UserAgent() | 144 | user_agent = UserAgent() |
| 181 | 145 | ||
| 146 | |||
| 182 | def git_require(min_version, fail=False, msg=''): | 147 | def git_require(min_version, fail=False, msg=''): |
| 183 | git_version = git.version_tuple() | 148 | git_version = git.version_tuple() |
| 184 | if min_version <= git_version: | 149 | if min_version <= git_version: |
| @@ -191,42 +156,38 @@ def git_require(min_version, fail=False, msg=''): | |||
| 191 | sys.exit(1) | 156 | sys.exit(1) |
| 192 | return False | 157 | return False |
| 193 | 158 | ||
| 194 | def _setenv(env, name, value): | ||
| 195 | env[name] = value.encode() | ||
| 196 | 159 | ||
| 197 | class GitCommand(object): | 160 | class GitCommand(object): |
| 198 | def __init__(self, | 161 | def __init__(self, |
| 199 | project, | 162 | project, |
| 200 | cmdv, | 163 | cmdv, |
| 201 | bare = False, | 164 | bare=False, |
| 202 | provide_stdin = False, | 165 | input=None, |
| 203 | capture_stdout = False, | 166 | capture_stdout=False, |
| 204 | capture_stderr = False, | 167 | capture_stderr=False, |
| 205 | disable_editor = False, | 168 | merge_output=False, |
| 206 | ssh_proxy = False, | 169 | disable_editor=False, |
| 207 | cwd = None, | 170 | ssh_proxy=None, |
| 208 | gitdir = None): | 171 | cwd=None, |
| 172 | gitdir=None): | ||
| 209 | env = self._GetBasicEnv() | 173 | env = self._GetBasicEnv() |
| 210 | 174 | ||
| 211 | # If we are not capturing std* then need to print it. | ||
| 212 | self.tee = {'stdout': not capture_stdout, 'stderr': not capture_stderr} | ||
| 213 | |||
| 214 | if disable_editor: | 175 | if disable_editor: |
| 215 | _setenv(env, 'GIT_EDITOR', ':') | 176 | env['GIT_EDITOR'] = ':' |
| 216 | if ssh_proxy: | 177 | if ssh_proxy: |
| 217 | _setenv(env, 'REPO_SSH_SOCK', ssh_sock()) | 178 | env['REPO_SSH_SOCK'] = ssh_proxy.sock() |
| 218 | _setenv(env, 'GIT_SSH', _ssh_proxy()) | 179 | env['GIT_SSH'] = ssh_proxy.proxy |
| 219 | _setenv(env, 'GIT_SSH_VARIANT', 'ssh') | 180 | env['GIT_SSH_VARIANT'] = 'ssh' |
| 220 | if 'http_proxy' in env and 'darwin' == sys.platform: | 181 | if 'http_proxy' in env and 'darwin' == sys.platform: |
| 221 | s = "'http.proxy=%s'" % (env['http_proxy'],) | 182 | s = "'http.proxy=%s'" % (env['http_proxy'],) |
| 222 | p = env.get('GIT_CONFIG_PARAMETERS') | 183 | p = env.get('GIT_CONFIG_PARAMETERS') |
| 223 | if p is not None: | 184 | if p is not None: |
| 224 | s = p + ' ' + s | 185 | s = p + ' ' + s |
| 225 | _setenv(env, 'GIT_CONFIG_PARAMETERS', s) | 186 | env['GIT_CONFIG_PARAMETERS'] = s |
| 226 | if 'GIT_ALLOW_PROTOCOL' not in env: | 187 | if 'GIT_ALLOW_PROTOCOL' not in env: |
| 227 | _setenv(env, 'GIT_ALLOW_PROTOCOL', | 188 | env['GIT_ALLOW_PROTOCOL'] = ( |
| 228 | 'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc') | 189 | 'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc') |
| 229 | _setenv(env, 'GIT_HTTP_USER_AGENT', user_agent.git) | 190 | env['GIT_HTTP_USER_AGENT'] = user_agent.git |
| 230 | 191 | ||
| 231 | if project: | 192 | if project: |
| 232 | if not cwd: | 193 | if not cwd: |
| @@ -237,7 +198,10 @@ class GitCommand(object): | |||
| 237 | command = [GIT] | 198 | command = [GIT] |
| 238 | if bare: | 199 | if bare: |
| 239 | if gitdir: | 200 | if gitdir: |
| 240 | _setenv(env, GIT_DIR, gitdir) | 201 | # Git on Windows wants its paths only using / for reliability. |
| 202 | if platform_utils.isWindows(): | ||
| 203 | gitdir = gitdir.replace('\\', '/') | ||
| 204 | env[GIT_DIR] = gitdir | ||
| 241 | cwd = None | 205 | cwd = None |
| 242 | command.append(cmdv[0]) | 206 | command.append(cmdv[0]) |
| 243 | # Need to use the --progress flag for fetch/clone so output will be | 207 | # Need to use the --progress flag for fetch/clone so output will be |
| @@ -247,13 +211,10 @@ class GitCommand(object): | |||
| 247 | command.append('--progress') | 211 | command.append('--progress') |
| 248 | command.extend(cmdv[1:]) | 212 | command.extend(cmdv[1:]) |
| 249 | 213 | ||
| 250 | if provide_stdin: | 214 | stdin = subprocess.PIPE if input else None |
| 251 | stdin = subprocess.PIPE | 215 | stdout = subprocess.PIPE if capture_stdout else None |
| 252 | else: | 216 | stderr = (subprocess.STDOUT if merge_output else |
| 253 | stdin = None | 217 | (subprocess.PIPE if capture_stderr else None)) |
| 254 | |||
| 255 | stdout = subprocess.PIPE | ||
| 256 | stderr = subprocess.PIPE | ||
| 257 | 218 | ||
| 258 | if IsTrace(): | 219 | if IsTrace(): |
| 259 | global LAST_CWD | 220 | global LAST_CWD |
| @@ -281,23 +242,38 @@ class GitCommand(object): | |||
| 281 | dbg += ' 1>|' | 242 | dbg += ' 1>|' |
| 282 | if stderr == subprocess.PIPE: | 243 | if stderr == subprocess.PIPE: |
| 283 | dbg += ' 2>|' | 244 | dbg += ' 2>|' |
| 245 | elif stderr == subprocess.STDOUT: | ||
| 246 | dbg += ' 2>&1' | ||
| 284 | Trace('%s', dbg) | 247 | Trace('%s', dbg) |
| 285 | 248 | ||
| 286 | try: | 249 | try: |
| 287 | p = subprocess.Popen(command, | 250 | p = subprocess.Popen(command, |
| 288 | cwd = cwd, | 251 | cwd=cwd, |
| 289 | env = env, | 252 | env=env, |
| 290 | stdin = stdin, | 253 | encoding='utf-8', |
| 291 | stdout = stdout, | 254 | errors='backslashreplace', |
| 292 | stderr = stderr) | 255 | stdin=stdin, |
| 256 | stdout=stdout, | ||
| 257 | stderr=stderr) | ||
| 293 | except Exception as e: | 258 | except Exception as e: |
| 294 | raise GitError('%s: %s' % (command[1], e)) | 259 | raise GitError('%s: %s' % (command[1], e)) |
| 295 | 260 | ||
| 296 | if ssh_proxy: | 261 | if ssh_proxy: |
| 297 | _add_ssh_client(p) | 262 | ssh_proxy.add_client(p) |
| 298 | 263 | ||
| 299 | self.process = p | 264 | self.process = p |
| 300 | self.stdin = p.stdin | 265 | if input: |
| 266 | if isinstance(input, str): | ||
| 267 | input = input.encode('utf-8') | ||
| 268 | p.stdin.write(input) | ||
| 269 | p.stdin.close() | ||
| 270 | |||
| 271 | try: | ||
| 272 | self.stdout, self.stderr = p.communicate() | ||
| 273 | finally: | ||
| 274 | if ssh_proxy: | ||
| 275 | ssh_proxy.remove_client(p) | ||
| 276 | self.rc = p.wait() | ||
| 301 | 277 | ||
| 302 | @staticmethod | 278 | @staticmethod |
| 303 | def _GetBasicEnv(): | 279 | def _GetBasicEnv(): |
| @@ -317,35 +293,4 @@ class GitCommand(object): | |||
| 317 | return env | 293 | return env |
| 318 | 294 | ||
| 319 | def Wait(self): | 295 | def Wait(self): |
| 320 | try: | 296 | return self.rc |
| 321 | p = self.process | ||
| 322 | rc = self._CaptureOutput() | ||
| 323 | finally: | ||
| 324 | _remove_ssh_client(p) | ||
| 325 | return rc | ||
| 326 | |||
| 327 | def _CaptureOutput(self): | ||
| 328 | p = self.process | ||
| 329 | s_in = platform_utils.FileDescriptorStreams.create() | ||
| 330 | s_in.add(p.stdout, sys.stdout, 'stdout') | ||
| 331 | s_in.add(p.stderr, sys.stderr, 'stderr') | ||
| 332 | self.stdout = '' | ||
| 333 | self.stderr = '' | ||
| 334 | |||
| 335 | while not s_in.is_done: | ||
| 336 | in_ready = s_in.select() | ||
| 337 | for s in in_ready: | ||
| 338 | buf = s.read() | ||
| 339 | if not buf: | ||
| 340 | s_in.remove(s) | ||
| 341 | continue | ||
| 342 | if not hasattr(buf, 'encode'): | ||
| 343 | buf = buf.decode() | ||
| 344 | if s.std_name == 'stdout': | ||
| 345 | self.stdout += buf | ||
| 346 | else: | ||
| 347 | self.stderr += buf | ||
| 348 | if self.tee[s.std_name]: | ||
| 349 | s.dest.write(buf) | ||
| 350 | s.dest.flush() | ||
| 351 | return p.wait() | ||
