| OLD | NEW |
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import base64 | 5 import base64 |
| 6 import functools | 6 import functools |
| 7 import httplib | 7 import httplib |
| 8 import json | 8 import json |
| 9 import logging | 9 import logging |
| 10 import os | 10 import os |
| (...skipping 16 matching lines...) Expand all Loading... |
| 27 | 27 |
| 28 | 28 |
| 29 class FetchError(Exception): | 29 class FetchError(Exception): |
| 30 pass | 30 pass |
| 31 | 31 |
| 32 | 32 |
| 33 class FetchNotAllowedError(FetchError): | 33 class FetchNotAllowedError(FetchError): |
| 34 pass | 34 pass |
| 35 | 35 |
| 36 | 36 |
| 37 def _run_git(checkout_dir, *args): | |
| 38 if sys.platform.startswith(('win', 'cygwin')): | |
| 39 cmd = ['git.bat'] | |
| 40 else: | |
| 41 cmd = ['git'] | |
| 42 | |
| 43 if checkout_dir is not None: | |
| 44 cmd += ['-C', checkout_dir] | |
| 45 cmd += list(args) | |
| 46 | |
| 47 logging.info('Running: %s', cmd) | |
| 48 return subprocess42.check_output(cmd) | |
| 49 | |
| 50 | |
| 51 class Backend(object): | 37 class Backend(object): |
| 52 @property | 38 @property |
| 53 def repo_type(self): | 39 def repo_type(self): |
| 54 """Returns repo type (see package_pb2.DepSpec).""" | 40 """Returns repo type (see package_pb2.DepSpec).""" |
| 55 raise NotImplementedError() | 41 raise NotImplementedError() |
| 56 | 42 |
| 57 @staticmethod | 43 @staticmethod |
| 58 def branch_spec(branch): | 44 def branch_spec(branch): |
| 59 """Returns branch spec for given branch suitable for given git backend.""" | 45 """Returns branch spec for given branch suitable for given git backend.""" |
| 60 raise NotImplementedError() | 46 raise NotImplementedError() |
| (...skipping 20 matching lines...) Expand all Loading... |
| 81 | 67 |
| 82 The dictionary contains the following keys: author, message. | 68 The dictionary contains the following keys: author, message. |
| 83 """ | 69 """ |
| 84 raise NotImplementedError() | 70 raise NotImplementedError() |
| 85 | 71 |
| 86 | 72 |
| 87 class UncleanFilesystemError(FetchError): | 73 class UncleanFilesystemError(FetchError): |
| 88 pass | 74 pass |
| 89 | 75 |
| 90 | 76 |
| 91 class GitFetchError(FetchError): | 77 class GitError(FetchError): |
| 92 pass | 78 |
| 79 def __init__(self, is_remote, message): |
| 80 super(GitError, self).__init__(message) |
| 81 self.is_remote = is_remote |
| 82 |
| 83 @staticmethod |
| 84 def is_remote_error(e): |
| 85 return isinstance(e, GitError) and e.is_remote |
| 93 | 86 |
| 94 | 87 |
| 95 class GitBackend(Backend): | 88 class GitBackend(Backend): |
| 96 """GitBackend uses a local git checkout.""" | 89 """GitBackend uses a local git checkout.""" |
| 97 | 90 |
| 91 class Git(object): |
| 92 |
| 93 # The set of Git subcommands that are considered network-touching |
| 94 # subcommands and, therefore, subject to flake and retriable. |
| 95 _REMOTE_SUBCOMMANDS = {'clone', 'fetch'} |
| 96 |
| 97 def __init__(self, checkout_dir=None): |
| 98 self._checkout_dir = checkout_dir |
| 99 |
| 100 @staticmethod |
| 101 def _resolve_git(): |
| 102 """Resolves the Git command to run based on current platform.""" |
| 103 return 'git.bat' if sys.platform.startswith(('win', 'cygwin')) else 'git' |
| 104 |
| 105 def __call__(self, *args): |
| 106 cmd = [self._resolve_git()] |
| 107 if self._checkout_dir is not None: |
| 108 cmd += ['-C', self._checkout_dir] |
| 109 cmd += list(args) |
| 110 |
| 111 try: |
| 112 return self._execute(*cmd) |
| 113 except subprocess42.CalledProcessError as e: |
| 114 subcommand = (args[0]) if args else ('') |
| 115 is_remote = subcommand in self._REMOTE_SUBCOMMANDS |
| 116 raise GitError(is_remote, 'Git "%s" failed: %s' % ( |
| 117 subcommand, e.message,)) |
| 118 |
| 119 def _execute(self, *args): |
| 120 """Runs a raw command. Separate so it's easily mockable.""" |
| 121 logging.info('Running: %s', args) |
| 122 return subprocess42.check_output(args) |
| 123 |
| 124 |
| 98 @property | 125 @property |
| 99 def repo_type(self): | 126 def repo_type(self): |
| 100 return package_pb2.DepSpec.GIT | 127 return package_pb2.DepSpec.GIT |
| 101 | 128 |
| 102 @staticmethod | 129 @staticmethod |
| 103 def branch_spec(branch): | 130 def branch_spec(branch): |
| 104 return 'origin/%s' % branch | 131 return 'origin/%s' % branch |
| 105 | 132 |
| 106 @util.exponential_retry(condition=lambda e: isinstance(e, GitFetchError)) | 133 @util.exponential_retry(condition=GitError.is_remote_error) |
| 107 def checkout(self, repo, revision, checkout_dir, allow_fetch): | 134 def checkout(self, repo, revision, checkout_dir, allow_fetch): |
| 108 logging.info('Freshening repository %s in %s', repo, checkout_dir) | 135 logging.info('Freshening repository %s in %s', repo, checkout_dir) |
| 109 | 136 |
| 137 git = self.Git() |
| 110 if not os.path.isdir(checkout_dir): | 138 if not os.path.isdir(checkout_dir): |
| 111 if not allow_fetch: | 139 if not allow_fetch: |
| 112 raise FetchNotAllowedError( | 140 raise FetchNotAllowedError( |
| 113 'need to clone %s but fetch not allowed' % repo) | 141 'need to clone %s but fetch not allowed' % repo) |
| 114 _run_git(None, 'clone', '-q', repo, checkout_dir) | 142 git('clone', '-q', repo, checkout_dir) |
| 115 elif not os.path.isdir(os.path.join(checkout_dir, '.git')): | 143 elif not os.path.isdir(os.path.join(checkout_dir, '.git')): |
| 116 raise UncleanFilesystemError( | 144 raise UncleanFilesystemError( |
| 117 '%s exists but is not a git repo' % checkout_dir) | 145 '%s exists but is not a git repo' % checkout_dir) |
| 118 | 146 |
| 119 _run_git(checkout_dir, 'config', 'remote.origin.url', repo) | 147 git = self.Git(checkout_dir=checkout_dir) |
| 148 git('config', 'remote.origin.url', repo) |
| 120 try: | 149 try: |
| 121 _run_git(checkout_dir, 'rev-parse', '-q', '--verify', | 150 git('rev-parse', '-q', '--verify', '%s^{commit}' % revision) |
| 122 '%s^{commit}' % revision) | 151 except GitError as e: |
| 123 except subprocess42.CalledProcessError: | 152 logging.warning('Revision %s is not available: %s', revision, e) |
| 153 |
| 154 # Revision does not exist. If we can't fetch, then we fail here. |
| 124 if not allow_fetch: | 155 if not allow_fetch: |
| 125 raise FetchNotAllowedError( | 156 raise FetchNotAllowedError( |
| 126 'need to fetch %s but fetch not allowed' % repo) | 157 'need to fetch %s but fetch not allowed' % repo) |
| 158 git('fetch') |
| 127 | 159 |
| 128 # Fetch from the remote Git repository. Wrap this in a GitFetchError | 160 git('reset', '-q', '--hard', revision) |
| 129 # for exponential retry on failure. | |
| 130 try: | |
| 131 _run_git(checkout_dir, 'fetch') | |
| 132 except subprocess42.CalledProcessError as e: | |
| 133 raise GitFetchError(e.message) | |
| 134 | 161 |
| 135 _run_git(checkout_dir, 'reset', '-q', '--hard', revision) | 162 @util.exponential_retry(condition=GitError.is_remote_error) |
| 136 | |
| 137 def updates(self, repo, revision, checkout_dir, allow_fetch, | 163 def updates(self, repo, revision, checkout_dir, allow_fetch, |
| 138 other_revision, paths): | 164 other_revision, paths): |
| 139 self.checkout(repo, revision, checkout_dir, allow_fetch) | 165 self.checkout(repo, revision, checkout_dir, allow_fetch) |
| 166 |
| 167 git = self.Git(checkout_dir=checkout_dir) |
| 140 if allow_fetch: | 168 if allow_fetch: |
| 141 _run_git(checkout_dir, 'fetch') | 169 git('fetch') |
| 170 |
| 142 args = [ | 171 args = [ |
| 143 'rev-list', | 172 'rev-list', |
| 144 '--reverse', | 173 '--reverse', |
| 145 '%s..%s' % (revision, other_revision), | 174 '%s..%s' % (revision, other_revision), |
| 146 ] | 175 ] |
| 147 if paths: | 176 if paths: |
| 148 args.extend(['--'] + paths) | 177 args.extend(['--'] + paths) |
| 149 return filter(bool, _run_git(checkout_dir, *args).strip().split('\n')) | 178 return filter(bool, git(*args).strip().split('\n')) |
| 150 | 179 |
| 151 def commit_metadata(self, repo, revision, checkout_dir, allow_fetch): | 180 def commit_metadata(self, repo, revision, checkout_dir, allow_fetch): |
| 181 git = self.Git(checkout_dir=checkout_dir) |
| 152 return { | 182 return { |
| 153 'author': _run_git(checkout_dir, 'show', '-s', '--pretty=%aE', | 183 'author': git('show', '-s', '--pretty=%aE', revision).strip(), |
| 154 revision).strip(), | 184 'message': git('show', '-s', '--pretty=%B', revision).strip(), |
| 155 'message': _run_git(checkout_dir, 'show', '-s', '--pretty=%B', | |
| 156 revision).strip(), | |
| 157 } | 185 } |
| 158 | 186 |
| 159 | 187 |
| 160 class GitilesFetchError(FetchError): | 188 class GitilesFetchError(FetchError): |
| 161 """An HTTP error that occurred during Gitiles fetching.""" | 189 """An HTTP error that occurred during Gitiles fetching.""" |
| 162 | 190 |
| 163 def __init__(self, status, message): | 191 def __init__(self, status, message): |
| 164 super(GitilesFetchError, self).__init__( | 192 super(GitilesFetchError, self).__init__( |
| 165 'Gitiles error code (%d): %s' % (status, message)) | 193 'Gitiles error code (%d): %s' % (status, message)) |
| 166 self.status = status | 194 self.status = status |
| (...skipping 138 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 305 logging.info('fetching %s', url) | 333 logging.info('fetching %s', url) |
| 306 | 334 |
| 307 resp = requests.get(url) | 335 resp = requests.get(url) |
| 308 if resp.status_code != httplib.OK: | 336 if resp.status_code != httplib.OK: |
| 309 raise GitilesFetchError(resp.status_code, resp.text) | 337 raise GitilesFetchError(resp.status_code, resp.text) |
| 310 | 338 |
| 311 if not resp.text.startswith(cls._GERRIT_XSRF_HEADER): | 339 if not resp.text.startswith(cls._GERRIT_XSRF_HEADER): |
| 312 raise GitilesFetchError(resp.status_code, 'Missing XSRF header') | 340 raise GitilesFetchError(resp.status_code, 'Missing XSRF header') |
| 313 | 341 |
| 314 return json.loads(resp.text[len(cls._GERRIT_XSRF_HEADER):]) | 342 return json.loads(resp.text[len(cls._GERRIT_XSRF_HEADER):]) |
| OLD | NEW |