rietveld_patcher.py revision ca12bfac764ba476d6cd062bf1dde12cc64c3f40
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 5import json 6import tarfile 7from StringIO import StringIO 8 9from file_system import FileNotFoundError, ToUnicode 10from future import Future 11from patcher import Patcher 12import svn_constants 13 14_CHROMIUM_REPO_BASEURLS = [ 15 'https://src.chromium.org/svn/trunk/src/', 16 'http://src.chromium.org/svn/trunk/src/', 17 'svn://svn.chromium.org/chrome/trunk/src', 18 'https://chromium.googlesource.com/chromium/src.git@master', 19 'http://git.chromium.org/chromium/src.git@master', 20] 21 22_DOCS_PATHS = [ 23 svn_constants.API_PATH, 24 svn_constants.TEMPLATE_PATH, 25 svn_constants.STATIC_PATH 26] 27 28class RietveldPatcherError(Exception): 29 def __init__(self, message): 30 self.message = message 31 32class _AsyncFetchFuture(object): 33 def __init__(self, 34 base_path, 35 issue, 36 patchset, 37 files, 38 binary, 39 fetcher): 40 self._base_path = base_path 41 self._issue = issue 42 self._patchset = patchset 43 self._files = files 44 self._binary = binary 45 self._tarball = fetcher.FetchAsync('tarball/%s/%s' % (issue, patchset)) 46 47 def Get(self): 48 tarball_result = self._tarball.Get() 49 if tarball_result.status_code != 200: 50 raise RietveldPatcherError( 51 'Failed to download tarball for issue %s patchset %s. Status: %s' % 52 (self._issue, self._patchset, tarball_result.status_code)) 53 54 try: 55 tar = tarfile.open(fileobj=StringIO(tarball_result.content)) 56 except tarfile.TarError as e: 57 raise RietveldPatcherError( 58 'Error loading tarball for issue %s patchset %s.' % (self._issue, 59 self._patchset)) 60 61 self._value = {} 62 for path in self._files: 63 if self._base_path: 64 tar_path = 'b/%s/%s' % (self._base_path, path) 65 else: 66 tar_path = 'b/%s' % path 67 68 patched_file = None 69 try: 70 patched_file = tar.extractfile(tar_path) 71 data = patched_file.read() 72 except tarfile.TarError as e: 73 # Show appropriate error message in the unlikely case that the tarball 74 # is corrupted. 75 raise RietveldPatcherError( 76 'Error extracting tarball for issue %s patchset %s file %s.' % 77 (self._issue, self._patchset, tar_path)) 78 except KeyError as e: 79 raise FileNotFoundError( 80 'File %s not found in the tarball for issue %s patchset %s' % 81 (tar_path, self._issue, self._patchset)) 82 finally: 83 if patched_file: 84 patched_file.close() 85 86 if self._binary: 87 self._value[path] = data 88 else: 89 self._value[path] = ToUnicode(data) 90 91 return self._value 92 93class RietveldPatcher(Patcher): 94 ''' Class to fetch resources from a patchset in Rietveld. 95 ''' 96 def __init__(self, 97 base_path, 98 issue, 99 fetcher): 100 self._base_path = base_path 101 self._issue = issue 102 self._fetcher = fetcher 103 self._cache = None 104 105 # In RietveldPatcher, the version is the latest patchset number. 106 def GetVersion(self): 107 try: 108 issue_json = json.loads(self._fetcher.Fetch( 109 'api/%s' % self._issue).content) 110 except Exception as e: 111 raise RietveldPatcherError( 112 'Failed to fetch information for issue %s.' % self._issue) 113 114 if issue_json.get('closed'): 115 raise RietveldPatcherError('Issue %s has been closed.' % self._issue) 116 117 patchsets = issue_json.get('patchsets') 118 if not isinstance(patchsets, list) or len(patchsets) == 0: 119 raise RietveldPatcherError('Cannot parse issue %s.' % self._issue) 120 121 if not issue_json.get('base_url') in _CHROMIUM_REPO_BASEURLS: 122 raise RietveldPatcherError('Issue %s\'s base url is unknown.' % 123 self._issue) 124 125 return str(patchsets[-1]) 126 127 def GetPatchedFiles(self, version=None): 128 if version is None: 129 patchset = self.GetVersion() 130 else: 131 patchset = version 132 try: 133 patchset_json = json.loads(self._fetcher.Fetch( 134 'api/%s/%s' % (self._issue, patchset)).content) 135 except Exception as e: 136 raise RietveldPatcherError( 137 'Failed to fetch details for issue %s patchset %s.' % (self._issue, 138 patchset)) 139 140 files = patchset_json.get('files') 141 if files is None or not isinstance(files, dict): 142 raise RietveldPatcherError('Failed to parse issue %s patchset %s.' % 143 (self._issue, patchset)) 144 145 added = [] 146 deleted = [] 147 modified = [] 148 for key in files: 149 if not key.startswith(self._base_path + '/'): 150 continue 151 152 f = key.split(self._base_path + '/', 1)[1] 153 if any(f.startswith(path) for path in _DOCS_PATHS): 154 status = (files[key].get('status') or 'M') 155 # status can be 'A ' or 'A + ' 156 if 'A' in status: 157 added.append(f) 158 elif 'D' in status: 159 deleted.append(f) 160 elif 'M' in status: 161 modified.append(f) 162 else: 163 raise RietveldPatcherError('Unknown file status for file %s: "%s."' % 164 (key, status)) 165 166 return (added, deleted, modified) 167 168 def Apply(self, paths, file_system, binary, version=None): 169 if version is None: 170 version = self.GetVersion() 171 return Future(delegate=_AsyncFetchFuture(self._base_path, 172 self._issue, 173 version, 174 paths, 175 binary, 176 self._fetcher)) 177 178 def GetIdentity(self): 179 return self._issue 180