| 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 json | 6 import json |
| 7 import logging | 7 import logging |
| 8 import os | 8 import os |
| 9 import shutil | 9 import shutil |
| 10 import sys | 10 import sys |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 42 cmd = ['git'] | 42 cmd = ['git'] |
| 43 | 43 |
| 44 if checkout_dir is not None: | 44 if checkout_dir is not None: |
| 45 cmd += ['-C', checkout_dir] | 45 cmd += ['-C', checkout_dir] |
| 46 cmd += list(args) | 46 cmd += list(args) |
| 47 | 47 |
| 48 logging.info('Running: %s', cmd) | 48 logging.info('Running: %s', cmd) |
| 49 return subprocess42.check_output(cmd) | 49 return subprocess42.check_output(cmd) |
| 50 | 50 |
| 51 | 51 |
| 52 def ensure_git_checkout(repo, revision, checkout_dir, allow_fetch): | 52 class Backend(object): |
| 53 """Fetches given |repo| at |revision| to |checkout_dir| using git. | 53 def checkout(self, repo, revision, checkout_dir, allow_fetch): |
| 54 """Checks out given |repo| at |revision| to |checkout_dir|. |
| 54 | 55 |
| 55 Network operations are performed only if |allow_fetch| is True. | 56 Network operations are performed only if |allow_fetch| is True. |
| 56 """ | 57 """ |
| 57 logging.info('Freshening repository %s in %s', repo, checkout_dir) | 58 raise NotImplementedError() |
| 58 | 59 |
| 59 if not os.path.isdir(checkout_dir): | 60 |
| 61 class GitBackend(Backend): |
| 62 """GitBackend uses a local git checkout.""" |
| 63 |
| 64 def checkout(self, repo, revision, checkout_dir, allow_fetch): |
| 65 logging.info('Freshening repository %s in %s', repo, checkout_dir) |
| 66 |
| 67 if not os.path.isdir(checkout_dir): |
| 68 if not allow_fetch: |
| 69 raise FetchNotAllowedError( |
| 70 'need to clone %s but fetch not allowed' % repo) |
| 71 _run_git(None, 'clone', '-q', repo, checkout_dir) |
| 72 elif not os.path.isdir(os.path.join(checkout_dir, '.git')): |
| 73 raise UncleanFilesystemError( |
| 74 '%s exists but is not a git repo' % checkout_dir) |
| 75 |
| 76 actual_origin = _run_git( |
| 77 checkout_dir, 'config', 'remote.origin.url').strip() |
| 78 if actual_origin != repo: |
| 79 raise UncleanFilesystemError( |
| 80 ('workdir %r exists but uses a different origin url %r ' |
| 81 'than requested %r') % (checkout_dir, actual_origin, repo)) |
| 82 |
| 83 try: |
| 84 _run_git(checkout_dir, 'rev-parse', '-q', '--verify', |
| 85 '%s^{commit}' % revision) |
| 86 except subprocess42.CalledProcessError: |
| 87 if not allow_fetch: |
| 88 raise FetchNotAllowedError( |
| 89 'need to fetch %s but fetch not allowed' % repo) |
| 90 _run_git(checkout_dir, 'fetch') |
| 91 _run_git(checkout_dir, 'reset', '-q', '--hard', revision) |
| 92 |
| 93 |
| 94 class GitilesBackend(Backend): |
| 95 """GitilesBackend uses a repo served by Gitiles.""" |
| 96 |
| 97 def checkout(self, repo, revision, checkout_dir, allow_fetch): |
| 98 logging.info('Freshening repository %s in %s', repo, checkout_dir) |
| 99 |
| 100 # TODO(phajdan.jr): implement caching. |
| 60 if not allow_fetch: | 101 if not allow_fetch: |
| 61 raise FetchNotAllowedError( | 102 raise FetchNotAllowedError( |
| 62 'need to clone %s but fetch not allowed' % repo) | 103 'need to download %s from gitiles but fetch not allowed' % repo) |
| 63 _run_git(None, 'clone', '-q', repo, checkout_dir) | |
| 64 elif not os.path.isdir(os.path.join(checkout_dir, '.git')): | |
| 65 raise UncleanFilesystemError( | |
| 66 '%s exists but is not a git repo' % checkout_dir) | |
| 67 | 104 |
| 68 actual_origin = _run_git(checkout_dir, 'config', 'remote.origin.url').strip() | 105 rev_url = '%s/+/%s?format=JSON' % (repo, requests.utils.quote(revision)) |
| 69 if actual_origin != repo: | 106 logging.info('fetching %s', rev_url) |
| 70 raise UncleanFilesystemError( | 107 rev_raw = requests.get(rev_url).text |
| 71 ('workdir %r exists but uses a different origin url %r ' | 108 if not rev_raw.startswith(')]}\'\n'): |
| 72 'than requested %r') % (checkout_dir, actual_origin, repo)) | 109 raise FetchError('Unexpected gitiles response: %s' % rev_raw) |
| 110 rev_json = json.loads(rev_raw.split('\n', 1)[1]) |
| 111 orig_revision = revision |
| 112 revision = rev_json['commit'] |
| 113 logging.info('resolved %s to %s', orig_revision, revision) |
| 73 | 114 |
| 74 try: | 115 shutil.rmtree(checkout_dir, ignore_errors=True) |
| 75 _run_git(checkout_dir, 'rev-parse', '-q', '--verify', | |
| 76 '%s^{commit}' % revision) | |
| 77 except subprocess42.CalledProcessError: | |
| 78 if not allow_fetch: | |
| 79 raise FetchNotAllowedError( | |
| 80 'need to fetch %s but fetch not allowed' % repo) | |
| 81 _run_git(checkout_dir, 'fetch') | |
| 82 _run_git(checkout_dir, 'reset', '-q', '--hard', revision) | |
| 83 | 116 |
| 117 recipes_cfg_url = '%s/+/%s/infra/config/recipes.cfg?format=TEXT' % ( |
| 118 repo, requests.utils.quote(revision)) |
| 119 logging.info('fetching %s' % recipes_cfg_url) |
| 120 recipes_cfg_request = requests.get(recipes_cfg_url) |
| 121 recipes_cfg_text = base64.b64decode(recipes_cfg_request.text) |
| 122 recipes_cfg_proto = package_pb2.Package() |
| 123 text_format.Merge(recipes_cfg_text, recipes_cfg_proto) |
| 124 recipes_path_rel = recipes_cfg_proto.recipes_path |
| 84 | 125 |
| 85 def ensure_gitiles_checkout(repo, revision, checkout_dir, allow_fetch): | 126 # Re-create recipes.cfg in |checkout_dir| so that the repo's recipes.py |
| 86 """Fetches given |repo| at |revision| to |checkout_dir| using gitiles. | 127 # can look it up. |
| 128 recipes_cfg_path = os.path.join( |
| 129 checkout_dir, 'infra', 'config', 'recipes.cfg') |
| 130 os.makedirs(os.path.dirname(recipes_cfg_path)) |
| 131 with open(recipes_cfg_path, 'w') as f: |
| 132 f.write(recipes_cfg_text) |
| 87 | 133 |
| 88 Network operations are performed only if |allow_fetch| is True. | 134 recipes_path = os.path.join(checkout_dir, recipes_path_rel) |
| 89 """ | 135 os.makedirs(recipes_path) |
| 90 logging.info('Freshening repository %s in %s', repo, checkout_dir) | |
| 91 | 136 |
| 92 # TODO(phajdan.jr): implement caching. | 137 archive_url = '%s/+archive/%s/%s.tar.gz' % ( |
| 93 if not allow_fetch: | 138 repo, requests.utils.quote(revision), recipes_path_rel) |
| 94 raise FetchNotAllowedError( | 139 logging.info('fetching %s' % archive_url) |
| 95 'need to download %s from gitiles but fetch not allowed' % repo) | 140 archive_request = requests.get(archive_url) |
| 96 | 141 with tempfile.NamedTemporaryFile() as f: |
| 97 rev_url = '%s/+/%s?format=JSON' % (repo, requests.utils.quote(revision)) | 142 f.write(archive_request.content) |
| 98 logging.info('fetching %s', rev_url) | 143 f.flush() |
| 99 rev_raw = requests.get(rev_url).text | 144 with tarfile.open(f.name) as archive_tarfile: |
| 100 if not rev_raw.startswith(')]}\'\n'): | 145 archive_tarfile.extractall(recipes_path) |
| 101 raise FetchError('Unexpected gitiles response: %s' % rev_raw) | |
| 102 rev_json = json.loads(rev_raw.split('\n', 1)[1]) | |
| 103 orig_revision = revision | |
| 104 revision = rev_json['commit'] | |
| 105 logging.info('resolved %s to %s', orig_revision, revision) | |
| 106 | |
| 107 shutil.rmtree(checkout_dir, ignore_errors=True) | |
| 108 | |
| 109 recipes_cfg_url = '%s/+/%s/infra/config/recipes.cfg?format=TEXT' % ( | |
| 110 repo, requests.utils.quote(revision)) | |
| 111 logging.info('fetching %s' % recipes_cfg_url) | |
| 112 recipes_cfg_request = requests.get(recipes_cfg_url) | |
| 113 recipes_cfg_text = base64.b64decode(recipes_cfg_request.text) | |
| 114 recipes_cfg_proto = package_pb2.Package() | |
| 115 text_format.Merge(recipes_cfg_text, recipes_cfg_proto) | |
| 116 recipes_path_rel = recipes_cfg_proto.recipes_path | |
| 117 | |
| 118 # Re-create recipes.cfg in |checkout_dir| so that the repo's recipes.py | |
| 119 # can look it up. | |
| 120 recipes_cfg_path = os.path.join( | |
| 121 checkout_dir, 'infra', 'config', 'recipes.cfg') | |
| 122 os.makedirs(os.path.dirname(recipes_cfg_path)) | |
| 123 with open(recipes_cfg_path, 'w') as f: | |
| 124 f.write(recipes_cfg_text) | |
| 125 | |
| 126 recipes_path = os.path.join(checkout_dir, recipes_path_rel) | |
| 127 os.makedirs(recipes_path) | |
| 128 | |
| 129 archive_url = '%s/+archive/%s/%s.tar.gz' % ( | |
| 130 repo, requests.utils.quote(revision), recipes_path_rel) | |
| 131 logging.info('fetching %s' % archive_url) | |
| 132 archive_request = requests.get(archive_url) | |
| 133 with tempfile.NamedTemporaryFile() as f: | |
| 134 f.write(archive_request.content) | |
| 135 f.flush() | |
| 136 with tarfile.open(f.name) as archive_tarfile: | |
| 137 archive_tarfile.extractall(recipes_path) | |
| OLD | NEW |