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