Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(649)

Side by Side Diff: recipe_engine/fetch.py

Issue 2720853002: Fix GitBackend.checkout (Closed)
Patch Set: tests Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | recipe_engine/package.py » ('j') | recipe_engine/package.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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):])
OLDNEW
« no previous file with comments | « no previous file | recipe_engine/package.py » ('j') | recipe_engine/package.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698