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 |