Chromium Code Reviews| 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 httplib | 6 import httplib |
| 7 import json | 7 import json |
| 8 import logging | 8 import logging |
| 9 import os | 9 import os |
| 10 import re | |
| 10 import shutil | 11 import shutil |
| 11 import sys | 12 import sys |
| 12 import tarfile | 13 import tarfile |
| 13 import tempfile | 14 import tempfile |
| 14 | 15 |
| 15 # Add third party paths. | 16 # Add third party paths. |
| 16 from . import env | 17 from . import env |
| 17 from . import requests_ssl | 18 from . import requests_ssl |
| 18 from . import util | 19 from . import util |
| 19 from .requests_ssl import requests | 20 from .requests_ssl import requests |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 31 class FetchNotAllowedError(FetchError): | 32 class FetchNotAllowedError(FetchError): |
| 32 pass | 33 pass |
| 33 | 34 |
| 34 | 35 |
| 35 class Backend(object): | 36 class Backend(object): |
| 36 @property | 37 @property |
| 37 def repo_type(self): | 38 def repo_type(self): |
| 38 """Returns repo type (see package_pb2.DepSpec).""" | 39 """Returns repo type (see package_pb2.DepSpec).""" |
| 39 raise NotImplementedError() | 40 raise NotImplementedError() |
| 40 | 41 |
| 41 @staticmethod | |
| 42 def branch_spec(branch): | |
| 43 """Returns branch spec for given branch suitable for given git backend.""" | |
| 44 raise NotImplementedError() | |
| 45 | |
| 46 def checkout(self, repo, revision, checkout_dir, allow_fetch): | 42 def checkout(self, repo, revision, checkout_dir, allow_fetch): |
| 47 """Checks out given |repo| at |revision| to |checkout_dir|. | 43 """Checks out given |repo| at |revision| to |checkout_dir|. |
| 48 | 44 |
| 49 Network operations are performed only if |allow_fetch| is True. | 45 Network operations are performed only if |allow_fetch| is True. |
| 50 """ | 46 """ |
| 51 raise NotImplementedError() | 47 raise NotImplementedError() |
| 52 | 48 |
| 53 def updates(self, repo, revision, checkout_dir, allow_fetch, | 49 def updates(self, repo, revision, checkout_dir, allow_fetch, |
| 54 other_revision, paths): | 50 other_revision, paths): |
| 55 """Returns a list of revisions between |revision| and |other_revision|. | 51 """Returns a list of revisions between |revision| and |other_revision|. |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 112 subcommand = (args[0]) if args else ('') | 108 subcommand = (args[0]) if args else ('') |
| 113 is_remote = subcommand in self._REMOTE_SUBCOMMANDS | 109 is_remote = subcommand in self._REMOTE_SUBCOMMANDS |
| 114 raise GitError(is_remote, 'Git "%s" failed: %s' % ( | 110 raise GitError(is_remote, 'Git "%s" failed: %s' % ( |
| 115 subcommand, e.message,)) | 111 subcommand, e.message,)) |
| 116 | 112 |
| 117 def _execute(self, *args): | 113 def _execute(self, *args): |
| 118 """Runs a raw command. Separate so it's easily mockable.""" | 114 """Runs a raw command. Separate so it's easily mockable.""" |
| 119 logging.info('Running: %s', args) | 115 logging.info('Running: %s', args) |
| 120 return subprocess42.check_output(args) | 116 return subprocess42.check_output(args) |
| 121 | 117 |
| 122 | |
| 123 @property | 118 @property |
| 124 def repo_type(self): | 119 def repo_type(self): |
| 125 return package_pb2.DepSpec.GIT | 120 return package_pb2.DepSpec.GIT |
| 126 | 121 |
| 127 @staticmethod | 122 @util.exponential_retry(condition=GitError.is_remote_error) |
| 128 def branch_spec(branch): | 123 def checkout(self, repo_url, revision, checkout_dir, allow_fetch): |
| 129 return 'origin/%s' % branch | 124 """Checks out given |repo| at |revision| to |checkout_dir|. |
| 130 | 125 |
| 131 @util.exponential_retry(condition=GitError.is_remote_error) | 126 Network operations are performed only if |allow_fetch| is True. |
| 132 def checkout(self, repo, revision, checkout_dir, allow_fetch): | |
| 133 logging.info('Freshening repository %s in %s', repo, checkout_dir) | |
| 134 | 127 |
| 135 git = self.Git() | 128 Returns the commit hash, if any. |
| 136 if not os.path.isdir(checkout_dir): | 129 """ |
| 137 if not allow_fetch: | 130 logging.info('Freshening repository %s in %s', repo_url, checkout_dir) |
| 138 raise FetchNotAllowedError( | 131 |
| 139 'need to clone %s but fetch not allowed' % repo) | 132 if revision == 'origin/master': |
| 140 git('clone', '-q', repo, checkout_dir) | 133 # Some clients pass 'origin/master' when they actually mean |
| 141 elif not os.path.isdir(os.path.join(checkout_dir, '.git')): | 134 # 'refs/heads/master'. |
| 142 raise UncleanFilesystemError( | 135 # TODO(nodir): remove this if statement BUG(http://crbug.com/696704). |
| 143 '%s exists but is not a git repo' % checkout_dir) | 136 revision = 'refs/heads/master' |
|
Paweł Hajdan Jr.
2017/02/28 08:44:33
Consider a logging.warning call in this case.
| |
| 137 assert re.match('^([a-z0-9]{40}|refs/.+)$', revision), revision | |
| 138 | |
| 139 is_commit = re.match('^[a-z0-9]{40}$', revision) | |
| 144 | 140 |
| 145 git = self.Git(checkout_dir=checkout_dir) | 141 git = self.Git(checkout_dir=checkout_dir) |
| 146 git('config', 'remote.origin.url', repo) | 142 if not os.path.isdir(checkout_dir): |
| 147 try: | 143 os.makedirs(checkout_dir) |
| 148 git('rev-parse', '-q', '--verify', '%s^{commit}' % revision) | 144 else: |
| 149 except GitError as e: | 145 if not os.path.isdir(os.path.join(checkout_dir, '.git')): |
| 150 logging.warning('Revision %s is not available: %s', revision, e) | 146 raise UncleanFilesystemError( |
| 147 '%s exists but is not a git repo' % checkout_dir) | |
| 151 | 148 |
| 152 # Revision does not exist. If we can't fetch, then we fail here. | 149 if is_commit: |
| 153 if not allow_fetch: | 150 # Avoid doing sever roundtrips if we already have the commit. |
| 154 raise FetchNotAllowedError( | 151 # Note: rev-parse is only safe if revision is a commit. If the revision |
| 155 'need to fetch %s but fetch not allowed' % repo) | 152 # is a ref, they may differ locally and remotely. |
| 156 git('fetch') | 153 try: |
| 154 git('rev-parse', '-q', '--verify', '%s^{commit}' % revision) | |
| 155 except GitError as e: | |
| 156 logging.warning('Revision %s needs to be fetched: %s', revision, e) | |
| 157 else: | |
| 158 git('checkout', '-q', '-f', revision) | |
| 159 return revision | |
| 157 | 160 |
| 158 git('reset', '-q', '--hard', revision) | 161 if not allow_fetch: |
| 162 # If revision is a commit hash, it is not present in the existing repo, so | |
| 163 # we have to fetch. | |
| 164 # If revision is a ref, what we have locally may be stale, so we have to | |
| 165 # fetch. | |
| 166 # We have to fetch anyway. | |
| 167 raise FetchNotAllowedError( | |
| 168 'need to fetch %s but fetch not allowed' % repo_url) | |
| 169 | |
| 170 git('init') # Safe to call on an existing git repo. | |
| 171 | |
| 172 # Typically we cannot fetch a commit, so we assume that remote master branch | |
|
Paweł Hajdan Jr.
2017/02/28 08:44:33
recipes.cfg has a "branch" field, so that assumpti
| |
| 173 # contains the requested commit. We will checkout the commit afterwards. | |
| 174 # Do not name the git remote to minimize repo state and to avoid making it | |
| 175 # work with invalid value of "origin/master". | |
| 176 # Do not map the fetched commit to anything to avoid checking out possibly | |
| 177 # stale refs. | |
| 178 git('fetch', repo_url, 'refs/heads/master' if is_commit else revision) | |
| 179 | |
| 180 # FETCH_HEAD points to the fetched commit. | |
| 181 | |
| 182 git('checkout', '-q', '-f', revision if is_commit else 'FETCH_HEAD') | |
| 183 return git('rev-parse', 'HEAD').strip() | |
| 159 | 184 |
| 160 @util.exponential_retry(condition=GitError.is_remote_error) | 185 @util.exponential_retry(condition=GitError.is_remote_error) |
| 161 def updates(self, repo, revision, checkout_dir, allow_fetch, | 186 def updates(self, repo, revision, checkout_dir, allow_fetch, |
| 162 other_revision, paths): | 187 other_revision, paths): |
| 163 self.checkout(repo, revision, checkout_dir, allow_fetch) | 188 |
| 189 assert re.match('^[a-z0-9]{40}$', revision), revision | |
| 190 # Since we are asked for revision..other_revision, revision must be among | |
| 191 # ancestors of other_revision and by fetching other_revision we ensure that | |
| 192 # both revisions will be present in the repo. | |
| 193 other_revision = self.checkout( | |
| 194 repo, other_revision, checkout_dir, allow_fetch) | |
| 164 | 195 |
| 165 git = self.Git(checkout_dir=checkout_dir) | 196 git = self.Git(checkout_dir=checkout_dir) |
| 166 if allow_fetch: | |
| 167 git('fetch') | |
| 168 | |
| 169 args = [ | 197 args = [ |
| 170 'rev-list', | 198 'rev-list', |
| 171 '--reverse', | 199 '--reverse', |
| 172 '%s..%s' % (revision, other_revision), | 200 '%s..%s' % (revision, other_revision), |
| 173 ] | 201 ] |
| 174 if paths: | 202 if paths: |
| 175 args.extend(['--'] + paths) | 203 args.extend(['--'] + paths) |
| 176 return filter(bool, git(*args).strip().split('\n')) | 204 return filter(bool, git(*args).strip().split('\n')) |
| 177 | 205 |
| 178 def commit_metadata(self, repo, revision, checkout_dir, allow_fetch): | 206 def commit_metadata(self, repo, revision, checkout_dir, allow_fetch): |
| (...skipping 25 matching lines...) Expand all Loading... | |
| 204 class GitilesBackend(Backend): | 232 class GitilesBackend(Backend): |
| 205 """GitilesBackend uses a repo served by Gitiles.""" | 233 """GitilesBackend uses a repo served by Gitiles.""" |
| 206 | 234 |
| 207 # Header at the beginning of Gerrit/Gitiles JSON API responses. | 235 # Header at the beginning of Gerrit/Gitiles JSON API responses. |
| 208 _GERRIT_XSRF_HEADER = ')]}\'\n' | 236 _GERRIT_XSRF_HEADER = ')]}\'\n' |
| 209 | 237 |
| 210 @property | 238 @property |
| 211 def repo_type(self): | 239 def repo_type(self): |
| 212 return package_pb2.DepSpec.GITILES | 240 return package_pb2.DepSpec.GITILES |
| 213 | 241 |
| 214 @staticmethod | |
| 215 def branch_spec(branch): | |
| 216 return branch | |
| 217 | |
| 218 def checkout(self, repo, revision, checkout_dir, allow_fetch): | 242 def checkout(self, repo, revision, checkout_dir, allow_fetch): |
| 219 requests_ssl.check_requests_ssl() | 243 requests_ssl.check_requests_ssl() |
| 220 logging.info('Freshening repository %s in %s', repo, checkout_dir) | 244 logging.info('Freshening repository %s in %s', repo, checkout_dir) |
| 221 | 245 |
| 222 # TODO(phajdan.jr): implement caching. | 246 # TODO(phajdan.jr): implement caching. |
| 223 if not allow_fetch: | 247 if not allow_fetch: |
| 224 raise FetchNotAllowedError( | 248 raise FetchNotAllowedError( |
| 225 'need to download %s from gitiles but fetch not allowed' % repo) | 249 'need to download %s from gitiles but fetch not allowed' % repo) |
| 226 | 250 |
| 227 revision = self._resolve_revision(repo, revision) | 251 revision = self._resolve_revision(repo, revision) |
| (...skipping 103 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 331 logging.info('fetching %s', url) | 355 logging.info('fetching %s', url) |
| 332 | 356 |
| 333 resp = requests.get(url) | 357 resp = requests.get(url) |
| 334 if resp.status_code != httplib.OK: | 358 if resp.status_code != httplib.OK: |
| 335 raise GitilesFetchError(resp.status_code, resp.text) | 359 raise GitilesFetchError(resp.status_code, resp.text) |
| 336 | 360 |
| 337 if not resp.text.startswith(cls._GERRIT_XSRF_HEADER): | 361 if not resp.text.startswith(cls._GERRIT_XSRF_HEADER): |
| 338 raise GitilesFetchError(resp.status_code, 'Missing XSRF header') | 362 raise GitilesFetchError(resp.status_code, 'Missing XSRF header') |
| 339 | 363 |
| 340 return json.loads(resp.text[len(cls._GERRIT_XSRF_HEADER):]) | 364 return json.loads(resp.text[len(cls._GERRIT_XSRF_HEADER):]) |
| OLD | NEW |