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