Chromium Code Reviews
|
| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 import json | |
| 6 import tarfile | |
| 7 from datetime import datetime, timedelta | |
| 8 from StringIO import StringIO | |
| 9 | |
| 10 from file_system import FileNotFoundError, ToUnicode | |
| 11 from future import Future | |
| 12 from patcher import Patcher | |
| 13 import svn_constants | |
| 14 | |
| 15 # Use a special value other than None to represent a deleted file in the patch. | |
| 16 _FILE_NOT_FOUND_VALUE = (None,) | |
| 17 | |
| 18 _ISSUE_CACHE_MAXAGE = timedelta(seconds=5) | |
| 19 | |
| 20 _CHROMIUM_REPO_BASEURLS = [ | |
| 21 'https://src.chromium.org/svn/trunk/src/', | |
| 22 'http://src.chromium.org/svn/trunk/src/', | |
| 23 'svn://svn.chromium.org/chrome/trunk/src', | |
| 24 'https://chromium.googlesource.com/chromium/src.git@master', | |
| 25 'http://git.chromium.org/chromium/src.git@master', | |
| 26 ] | |
| 27 | |
| 28 _DOCS_PATHS = [ | |
| 29 svn_constants.API_PATH, | |
| 30 svn_constants.TEMPLATE_PATH, | |
| 31 svn_constants.STATIC_PATH | |
| 32 ] | |
| 33 | |
| 34 def _HandleBinary(data, binary): | |
| 35 return data if binary else ToUnicode(data) | |
| 36 | |
| 37 class RietveldPatcherError(Exception): | |
| 38 def __init__(self, message): | |
| 39 self.message = message | |
| 40 | |
| 41 class _AsyncUncachedFuture(object): | |
| 42 def __init__(self, | |
| 43 cache, | |
| 44 version, | |
| 45 paths, | |
| 46 binary, | |
| 47 cached_value, | |
| 48 missing_paths, | |
| 49 fetch_delegate): | |
| 50 self._cache = cache | |
| 51 self._version = version | |
| 52 self._paths = paths | |
| 53 self._binary = binary | |
| 54 self._cached_value = cached_value | |
| 55 self._missing_paths = missing_paths | |
| 56 self._fetch_delegate = fetch_delegate | |
| 57 | |
| 58 def Get(self): | |
| 59 uncached_raw_value = self._fetch_delegate.Get() | |
| 60 self._cache.CacheFiles(uncached_raw_value, self._version) | |
| 61 | |
| 62 for path in self._missing_paths: | |
| 63 if uncached_raw_value.get(path) is None: | |
| 64 raise FileNotFoundError('File %s was not found in the patch.' % path) | |
| 65 self._cached_value[path] = _HandleBinary(uncached_raw_value[path], | |
| 66 self._binary) | |
| 67 | |
| 68 # Make sure all paths exist before returning. | |
| 69 for path in self._paths: | |
| 70 if self._cached_value[path] == _FILE_NOT_FOUND_VALUE: | |
| 71 raise FileNotFoundError('File %s was deleted in the patch.' % path) | |
| 72 return self._cached_value | |
| 73 | |
| 74 class _AsyncFetchFuture(object): | |
| 75 def __init__(self, | |
| 76 base_path, | |
| 77 issue, | |
| 78 patchset, | |
| 79 patched_files, | |
| 80 paths_to_skip, | |
| 81 fetcher): | |
| 82 self._base_path = base_path | |
| 83 self._issue = issue | |
| 84 self._patchset = patchset | |
| 85 self._files = set() | |
| 86 for files in patched_files: | |
| 87 self._files |= set(files) - set(paths_to_skip) | |
| 88 self._tarball = fetcher.FetchAsync('tarball/%s/%s' % (issue, patchset)) | |
| 89 | |
| 90 def Get(self): | |
| 91 tarball_result = self._tarball.Get() | |
| 92 if tarball_result.status_code != 200: | |
| 93 raise RietveldPatcherError( | |
| 94 'Failed to download tarball for issue %s patchset %s. Status: %s' % | |
| 95 (self._issue, self._patchset, tarball_result.status_code)) | |
| 96 | |
| 97 try: | |
| 98 tar = tarfile.open(fileobj=StringIO(tarball_result.content)) | |
| 99 except tarfile.TarError as e: | |
| 100 raise RietveldPatcherError('Invalid tarball for issue %s patchset %s.' % | |
| 101 (self._issue, self._patchset)) | |
| 102 | |
| 103 self._value = {} | |
| 104 for path in self._files: | |
| 105 if self._base_path: | |
| 106 tar_path = 'b/%s/%s' % (self._base_path, path) | |
| 107 else: | |
| 108 tar_path = 'b/%s' % path | |
| 109 | |
| 110 patched_file = None | |
| 111 try: | |
| 112 patched_file = tar.extractfile(tar_path) | |
| 113 data = patched_file.read() | |
| 114 except tarfile.TarError as e: | |
| 115 # Show appropriate error message in the unlikely case that the tarball | |
| 116 # is corrupted. | |
| 117 raise RietveldPatcherError( | |
| 118 'Error extracting tarball for issue %s patchset %s file %s.' % | |
| 119 (self._issue, self._patchset, tar_path)) | |
| 120 finally: | |
| 121 if patched_file: | |
| 122 patched_file.close() | |
| 123 | |
| 124 # Deleted files still exist in the tarball, but they are empty. | |
| 125 if len(data) == 0: | |
| 126 # Mark it empty instead of throwing FileNotFoundError here to make sure | |
| 127 # this method completes and returns values to cache. | |
| 128 self._value[path] = _FILE_NOT_FOUND_VALUE | |
| 129 else: | |
| 130 self._value[path] = data | |
| 131 | |
| 132 return self._value | |
| 133 | |
| 134 class RietveldPatcher(Patcher): | |
| 135 ''' Class to fetch resources from a patchset in Rietveld. | |
| 136 ''' | |
| 137 class _Cache(object): | |
|
not at google - send to devlin
2013/05/07 05:45:40
the caching logic should all be pulled out into a
方觉(Fang Jue)
2013/05/07 06:03:55
I'll try to do that. But I don't think it's possib
not at google - send to devlin
2013/05/07 06:17:21
What do you mean "fetches all patched files at a t
| |
| 138 def __init__(self, object_store_creator_factory): | |
| 139 self._issue_object_store = object_store_creator_factory.Create( | |
| 140 RietveldPatcher).Create(category='issue') | |
| 141 self._file_object_store = object_store_creator_factory.Create( | |
| 142 RietveldPatcher).Create(category='file') | |
| 143 | |
| 144 def GetPatchset(self, fetch_function): | |
| 145 key = 'patchset' | |
| 146 value = self._issue_object_store.Get(key).Get() | |
| 147 if value is not None: | |
| 148 patchset, time = value | |
| 149 if datetime.now() - time < _ISSUE_CACHE_MAXAGE: | |
| 150 return patchset | |
| 151 | |
| 152 patchset = fetch_function() | |
| 153 self._issue_object_store.Set(key, (patchset, datetime.now())) | |
| 154 return patchset | |
| 155 | |
| 156 def GetPatchedFiles(self, version, fetch_function): | |
| 157 value = self._issue_object_store.Get(version).Get() | |
| 158 if value is not None: | |
| 159 return value | |
| 160 | |
| 161 value = fetch_function() | |
| 162 self._issue_object_store.Set(version, value) | |
| 163 return value | |
| 164 | |
| 165 ''' Append @version for keys to distinguish between different patchsets of | |
| 166 an issue. | |
| 167 ''' | |
| 168 def _MakeKey(self, path_or_paths, version): | |
| 169 if isinstance(path_or_paths, list) or isinstance(path_or_paths, set): | |
| 170 return ['%s@%s' % (p, version) for p in path_or_paths] | |
| 171 return self._MakeKey([path_or_paths], version)[0] | |
| 172 | |
| 173 def _ToObjectStoreValue(self, raw_value, version): | |
| 174 return {self._MakeKey(key, version): raw_value[key] for key in raw_value} | |
| 175 | |
| 176 def _FromObjectStoreValue(self, raw_value, binary): | |
| 177 return {key[0:key.find('@')]: _HandleBinary(raw_value[key], binary) | |
| 178 for key in raw_value} | |
| 179 | |
| 180 def Apply(self, version, paths, binary, fetch_future_function): | |
| 181 cached_value = self._FromObjectStoreValue(self._file_object_store. | |
| 182 GetMulti(self._MakeKey(paths, version)).Get(), binary) | |
| 183 missing_paths = list(set(paths) - set(cached_value.keys())) | |
| 184 if len(missing_paths) == 0: | |
| 185 return Future(value=cached_value) | |
| 186 | |
| 187 return _AsyncUncachedFuture(self, | |
| 188 version, | |
| 189 paths, | |
| 190 binary, | |
| 191 cached_value, | |
| 192 missing_paths, | |
| 193 fetch_future_function(cached_value.keys())) | |
| 194 | |
| 195 def CacheFiles(self, uncached_raw_value, version): | |
| 196 self._file_object_store.SetMulti(self._ToObjectStoreValue( | |
| 197 uncached_raw_value, version)) | |
| 198 | |
| 199 def __init__(self, | |
| 200 base_path, | |
| 201 issue, | |
| 202 fetcher, | |
| 203 object_store_creator_factory): | |
| 204 self._base_path = base_path | |
| 205 self._issue = issue | |
| 206 self._fetcher = fetcher | |
| 207 self._object_store = object_store_creator_factory.Create( | |
| 208 RietveldPatcher).Create() | |
| 209 self._cache = RietveldPatcher._Cache(object_store_creator_factory) | |
| 210 | |
| 211 def GetVersion(self): | |
| 212 return self._GetPatchset() | |
| 213 | |
| 214 def _GetPatchset(self): | |
| 215 return self._cache.GetPatchset(self._FetchPatchset) | |
| 216 | |
| 217 def _FetchPatchset(self): | |
| 218 try: | |
| 219 issue_json = json.loads(self._fetcher.Fetch( | |
| 220 'api/%s' % self._issue).content) | |
| 221 except Exception as e: | |
| 222 raise RietveldPatcherError( | |
| 223 'Failed to fetch information for issue %s.' % self._issue) | |
| 224 | |
| 225 if issue_json.get('closed'): | |
| 226 raise RietveldPatcherError('Issue %s has been closed.' % self._issue) | |
| 227 | |
| 228 patchsets = issue_json.get('patchsets') | |
| 229 if not isinstance(patchsets, list) or len(patchsets) == 0: | |
| 230 raise RietveldPatcherError('Cannot parse issue %s.' % self._issue) | |
| 231 | |
| 232 if not issue_json.get('base_url') in _CHROMIUM_REPO_BASEURLS: | |
| 233 raise RietveldPatcherError('Issue %s\'s base url is unknown.' % | |
| 234 self._issue) | |
| 235 | |
| 236 return str(patchsets[-1]) | |
| 237 | |
| 238 def GetPatchedFiles(self): | |
| 239 return self._cache.GetPatchedFiles(self.GetVersion(), | |
| 240 self._FetchPatchedFiles) | |
| 241 | |
| 242 def _FetchPatchedFiles(self): | |
| 243 patchset = self.GetVersion() | |
| 244 try: | |
| 245 patchset_json = json.loads(self._fetcher.Fetch( | |
| 246 'api/%s/%s' % (self._issue, patchset)).content) | |
| 247 except Exception as e: | |
| 248 raise RietveldPatcherError( | |
| 249 'Failed to fetch details for issue %s patchset %s.' % (self._issue, | |
| 250 patchset)) | |
| 251 | |
| 252 files = patchset_json.get('files') | |
| 253 if files is None or not isinstance(files, dict): | |
| 254 raise RietveldPatcherError('Failed to parse issue %s patchset %s.' % | |
| 255 (self._issue, patchset)) | |
| 256 | |
| 257 added = [] | |
| 258 deleted = [] | |
| 259 modified = [] | |
| 260 for key in files: | |
| 261 f = key.split(self._base_path + '/', 1)[1] | |
| 262 if any(f.startswith(path) for path in _DOCS_PATHS): | |
| 263 status = (files[key].get('status') or 'M') | |
| 264 # status can be 'A ' or 'A + ' | |
| 265 if 'A' in status: | |
| 266 added.append(f) | |
| 267 elif 'D' in status: | |
| 268 deleted.append(f) | |
| 269 else: | |
| 270 modified.append(f) | |
| 271 | |
| 272 return (added, deleted, modified) | |
| 273 | |
| 274 def Apply(self, paths, file_system, binary=False): | |
| 275 _, deleted, _ = self.GetPatchedFiles() | |
| 276 if set(deleted) & set(paths): | |
| 277 raise FileNotFoundError('File(s) %s are removed in the patch.' % | |
| 278 list(set(deleted) & set(paths))) | |
| 279 | |
| 280 return self._cache.Apply(self.GetVersion(), paths, binary, | |
| 281 self._CreateFetchFuture) | |
| 282 | |
| 283 def _CreateFetchFuture(self, paths_to_skip): | |
| 284 return Future(delegate=_AsyncFetchFuture(self._base_path, | |
| 285 self._issue, | |
| 286 self._GetPatchset(), | |
| 287 self.GetPatchedFiles(), | |
| 288 paths_to_skip, | |
| 289 self._fetcher)) | |
| OLD | NEW |