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

Side by Side Diff: recipe_engine/fetch.py

Issue 2372753002: Cleanup fetch, fix missing network flake instances (Closed)
Patch Set: Unit test for bug failure case. Created 4 years, 2 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/unittests/fetch_test.py » ('j') | no next file with comments »
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 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
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
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
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):])
OLDNEW
« no previous file with comments | « no previous file | recipe_engine/unittests/fetch_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698