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