1a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)# Copyright 2013 The Chromium Authors. All rights reserved.
2a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
3a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)# found in the LICENSE file.
4a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
5a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)import json
6a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)import tarfile
7a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)from StringIO import StringIO
8a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
9a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)from file_system import FileNotFoundError
10a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)from future import Future
11a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)from patcher import Patcher
12f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)
13a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
14a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)_CHROMIUM_REPO_BASEURLS = [
15a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  'https://src.chromium.org/svn/trunk/src/',
16a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  'http://src.chromium.org/svn/trunk/src/',
17a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  'svn://svn.chromium.org/chrome/trunk/src',
18a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  'https://chromium.googlesource.com/chromium/src.git@master',
19a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  'http://git.chromium.org/chromium/src.git@master',
20a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)]
21a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
22a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
23a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)class RietveldPatcherError(Exception):
24a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  def __init__(self, message):
25a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    self.message = message
26a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
27a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
28a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)class RietveldPatcher(Patcher):
29a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  ''' Class to fetch resources from a patchset in Rietveld.
30a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  '''
31a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  def __init__(self,
32a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)               issue,
33a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)               fetcher):
34a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    self._issue = issue
35a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    self._fetcher = fetcher
36a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    self._cache = None
37a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
38a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  # In RietveldPatcher, the version is the latest patchset number.
39a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  def GetVersion(self):
40a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    try:
41a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      issue_json = json.loads(self._fetcher.Fetch(
42a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)          'api/%s' % self._issue).content)
43a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    except Exception as e:
44a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      raise RietveldPatcherError(
45a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)          'Failed to fetch information for issue %s.' % self._issue)
46a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
47a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    if issue_json.get('closed'):
48a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      raise RietveldPatcherError('Issue %s has been closed.' % self._issue)
49a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
50a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    patchsets = issue_json.get('patchsets')
51a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    if not isinstance(patchsets, list) or len(patchsets) == 0:
52a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      raise RietveldPatcherError('Cannot parse issue %s.' % self._issue)
53a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
54a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    if not issue_json.get('base_url') in _CHROMIUM_REPO_BASEURLS:
55a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      raise RietveldPatcherError('Issue %s\'s base url is unknown.' %
56a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)          self._issue)
57a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
58a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    return str(patchsets[-1])
59a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
60a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)  def GetPatchedFiles(self, version=None):
61a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    if version is None:
62a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      patchset = self.GetVersion()
63a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    else:
64a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      patchset = version
65a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    try:
66a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      patchset_json = json.loads(self._fetcher.Fetch(
67a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)          'api/%s/%s' % (self._issue, patchset)).content)
68a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    except Exception as e:
69a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      raise RietveldPatcherError(
70a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)          'Failed to fetch details for issue %s patchset %s.' % (self._issue,
71a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)                                                                 patchset))
72a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
73a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    files = patchset_json.get('files')
74a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    if files is None or not isinstance(files, dict):
75a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      raise RietveldPatcherError('Failed to parse issue %s patchset %s.' %
76a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)          (self._issue, patchset))
77a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
78a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    added = []
79a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    deleted = []
80a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    modified = []
81f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)    for f in files:
82f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)      status = (files[f].get('status') or 'M')
83f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)      # status can be 'A   ' or 'A + '
84f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)      if 'A' in status:
85f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)        added.append(f)
86f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)      elif 'D' in status:
87f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)        deleted.append(f)
88f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)      elif 'M' in status:
89f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)        modified.append(f)
90f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)      else:
91f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)        raise RietveldPatcherError('Unknown file status for file %s: "%s."' %
92f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)                                                                (key, status))
93a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
94a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    return (added, deleted, modified)
95a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)
96a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)  def Apply(self, paths, file_system, version=None):
97a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)    if version is None:
98a93a17c8d99d686bd4a1511e5504e5e6cc9fcadfTorne (Richard Coles)      version = self.GetVersion()
996e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
1006e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)    def apply_(tarball_result):
1016e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)      if tarball_result.status_code != 200:
1026e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        raise RietveldPatcherError(
1036e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)            'Failed to download tarball for issue %s patchset %s. Status: %s' %
1046e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)            (self._issue, version, tarball_result.status_code))
1056e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
1066e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)      try:
1076e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        tar = tarfile.open(fileobj=StringIO(tarball_result.content))
1086e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)      except tarfile.TarError as e:
1096e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        raise RietveldPatcherError(
1106e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)            'Error loading tarball for issue %s patchset %s.' % (self._issue,
1116e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)                                                                 version))
1126e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
1136e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)      value = {}
1146e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)      for path in paths:
1156e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        tar_path = 'b/%s' % path
1166e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
1176e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        patched_file = None
1186e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        try:
1196e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)          patched_file = tar.extractfile(tar_path)
1206e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)          data = patched_file.read()
1216e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        except tarfile.TarError as e:
1226e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)          # Show appropriate error message in the unlikely case that the tarball
1236e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)          # is corrupted.
1246e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)          raise RietveldPatcherError(
1256e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)              'Error extracting tarball for issue %s patchset %s file %s.' %
1266e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)              (self._issue, version, tar_path))
1276e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        except KeyError as e:
1286e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)          raise FileNotFoundError(
1296e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)              'File %s not found in the tarball for issue %s patchset %s' %
1306e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)              (tar_path, self._issue, version))
1316e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        finally:
1326e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)          if patched_file:
1336e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)            patched_file.close()
1346e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
1356e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)        value[path] = data
1366e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)
1376e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)      return value
1386e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)    return self._fetcher.FetchAsync('tarball/%s/%s' % (self._issue,
1396e8cce623b6e4fe0c9e4af605d675dd9d0338c38Torne (Richard Coles)                                                       version)).Then(apply_)
140ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch
141ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch  def GetIdentity(self):
142ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch    return self._issue
143