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