apply_issue.py 9.46 KB
Newer Older
1
#!/usr/bin/env python
2
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 4 5 6 7 8
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Applies an issue from Rietveld.
"""

9
import getpass
10
import json
11 12
import logging
import optparse
13
import os
14
import subprocess
15
import sys
16
import urllib2
17 18

import breakpad  # pylint: disable=W0611
19

20
import annotated_gclient
21
import checkout
22
import fix_encoding
23
import gclient_utils
24
import rietveld
25
import scm
26

27 28
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

29

30 31 32 33 34 35 36 37 38 39 40 41 42
class Unbuffered(object):
  """Disable buffering on a file object."""
  def __init__(self, stream):
    self.stream = stream

  def write(self, data):
    self.stream.write(data)
    self.stream.flush()

  def __getattr__(self, attr):
    return getattr(self.stream, attr)


43
def main():
44
  # TODO(pgervais): This function is way too long. Split.
45
  sys.stdout = Unbuffered(sys.stdout)
46 47
  parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
  parser.add_option(
48 49
      '-v', '--verbose', action='count', default=0,
      help='Prints debugging infos')
50
  parser.add_option(
51
      '-e', '--email',
52 53 54
      help='Email address to access rietveld.  If not specified, anonymous '
           'access will be used.')
  parser.add_option(
55 56 57 58 59 60 61 62 63 64 65
      '-E', '--email-file',
      help='File containing the email address to access rietveld. '
           'If not specified, anonymous access will be used.')
  parser.add_option(
      '-w', '--password',
      help='Password for email addressed. Use - to read password from stdin. '
           'if -k is provided, this is the private key file password.')
  parser.add_option(
      '-k', '--private-key-file',
      help='Path to file containing a private key in p12 format for OAuth2 '
           'authentication. Use -w to provide the decrypting password, if any.')
66 67 68 69 70 71 72
  parser.add_option(
      '-i', '--issue', type='int', help='Rietveld issue number')
  parser.add_option(
      '-p', '--patchset', type='int', help='Rietveld issue\'s patchset number')
  parser.add_option(
      '-r',
      '--root_dir',
73
      default=os.getcwd(),
74 75 76 77 78 79
      help='Root directory to apply the patch')
  parser.add_option(
      '-s',
      '--server',
      default='http://codereview.chromium.org',
      help='Rietveld server')
80 81
  parser.add_option('--no-auth', action='store_true',
                    help='Do not attempt authenticated requests.')
82 83 84
  parser.add_option('--revision-mapping', default='{}',
                    help='When running gclient, annotate the got_revisions '
                         'using the revision-mapping.')
85 86 87
  parser.add_option('-f', '--force', action='store_true',
                    help='Really run apply_issue, even if .update.flag '
                         'is detected.')
88
  parser.add_option('-b', '--base_ref', help='DEPRECATED do not use.')
89 90 91 92
  parser.add_option('--whitelist', action='append', default=[],
                    help='Patch only specified file(s).')
  parser.add_option('--blacklist', action='append', default=[],
                    help='Don\'t patch specified file(s).')
93 94
  parser.add_option('-d', '--ignore_deps', action='store_true',
                    help='Don\'t run gclient sync on DEPS changes.')
95
  options, args = parser.parse_args()
96

97 98 99
  if options.whitelist and options.blacklist:
    parser.error('Cannot specify both --whitelist and --blacklist')

100 101 102 103 104
  if options.password and options.private_key_file:
    parser.error('-k and -w options are incompatible')
  if options.email and options.email_file:
    parser.error('-e and -E options are incompatible')

105 106 107 108 109
  if (os.path.isfile(os.path.join(os.getcwd(), 'update.flag'))
      and not options.force):
    print 'update.flag file found: bot_update has run and checkout is already '
    print 'in a consistent state. No actions will be performed in this step.'
    return 0
110
  logging.basicConfig(
111
      format='%(levelname)5s %(module)11s(%(lineno)4d): %(message)s',
112 113
      level=[logging.WARNING, logging.INFO, logging.DEBUG][
          min(2, options.verbose)])
114 115 116 117
  if args:
    parser.error('Extra argument(s) "%s" not understood' % ' '.join(args))
  if not options.issue:
    parser.error('Require --issue')
118 119 120
  options.server = options.server.rstrip('/')
  if not options.server:
    parser.error('Require a valid server')
121

122 123
  options.revision_mapping = json.loads(options.revision_mapping)

124
  if options.password == '-':
125
    print('Reading password')
126 127
    options.password = sys.stdin.readline().strip()

128 129 130 131 132 133 134
  # read email if needed
  if options.email_file:
    if not os.path.exists(options.email_file):
      parser.error('file does not exist: %s' % options.email_file)
    with open(options.email_file, 'rb') as f:
      options.email = f.read().strip()

135
  print('Connecting to %s' % options.server)
136 137 138 139 140 141 142
  # Always try un-authenticated first, except for OAuth2
  if options.private_key_file:
    # OAuth2 authentication
    obj = rietveld.JwtOAuth2Rietveld(options.server,
                                     options.email,
                                     options.private_key_file,
                                     private_key_password=options.password)
143
    properties = obj.get_issue_properties(options.issue, False)
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
  else:
    obj = rietveld.Rietveld(options.server, '', None)
    properties = None
    # Bad except clauses order (HTTPError is an ancestor class of
    # ClientLoginError)
    # pylint: disable=E0701
    try:
      properties = obj.get_issue_properties(options.issue, False)
    except urllib2.HTTPError as e:
      if e.getcode() != 302:
        raise
      if options.no_auth:
        exit('FAIL: Login detected -- is issue private?')
      # TODO(maruel): A few 'Invalid username or password.' are printed first,
      # we should get rid of those.
    except rietveld.upload.ClientLoginError, e:
      # Fine, we'll do proper authentication.
      pass
    if properties is None:
      if options.email is not None:
        obj = rietveld.Rietveld(options.server, options.email, options.password)
        try:
          properties = obj.get_issue_properties(options.issue, False)
        except rietveld.upload.ClientLoginError, e:
          if sys.stdout.closed:
            print('Accessing the issue requires proper credentials.')
            return 1
      else:
        print('Accessing the issue requires login.')
        obj = rietveld.Rietveld(options.server, None, None)
        try:
          properties = obj.get_issue_properties(options.issue, False)
        except rietveld.upload.ClientLoginError, e:
177
          print('Accessing the issue requires proper credentials.')
178
          return 1
179 180

  if not options.patchset:
181
    options.patchset = properties['patchsets'][-1]
182 183 184
    print('No patchset specified. Using patchset %d' % options.patchset)

  print('Downloading the patch.')
185 186 187
  try:
    patchset = obj.get_patch(options.issue, options.patchset)
  except urllib2.HTTPError, e:
188
    print(
189 190 191 192 193
        'Failed to fetch the patch for issue %d, patchset %d.\n'
        'Try visiting %s/%d') % (
            options.issue, options.patchset,
            options.server, options.issue)
    return 1
194 195 196 197 198 199
  if options.whitelist:
    patchset.patches = [patch for patch in patchset.patches
                        if patch.filename in options.whitelist]
  if options.blacklist:
    patchset.patches = [patch for patch in patchset.patches
                        if patch.filename not in options.blacklist]
200
  for patch in patchset.patches:
201
    print(patch)
202 203
  full_dir = os.path.abspath(options.root_dir)
  scm_type = scm.determine_scm(full_dir)
204
  if scm_type == 'svn':
205
    scm_obj = checkout.SvnCheckout(full_dir, None, None, None, None)
206
  elif scm_type == 'git':
207
    scm_obj = checkout.GitCheckout(full_dir, None, None, None, None)
208
  elif scm_type == None:
209
    scm_obj = checkout.RawCheckout(full_dir, None, None)
210 211 212
  else:
    parser.error('Couldn\'t determine the scm')

213 214 215 216 217 218 219 220 221
  # TODO(maruel): HACK, remove me.
  # When run a build slave, make sure buildbot knows that the checkout was
  # modified.
  if options.root_dir == 'src' and getpass.getuser() == 'chrome-bot':
    # See sourcedirIsPatched() in:
    # http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/slave/
    #    chromium_commands.py?view=markup
    open('.buildbot-patched', 'w').close()

222
  print('\nApplying the patch.')
223
  try:
224
    scm_obj.apply_patch(patchset, verbose=True)
225
  except checkout.PatchApplicationFailed, e:
226 227 228
    print(str(e))
    print('CWD=%s' % os.getcwd())
    print('Checkout path=%s' % scm_obj.project_path)
229
    return 1
230

231 232
  if ('DEPS' in map(os.path.basename, patchset.filenames)
      and not options.ignore_deps):
233
    gclient_root = gclient_utils.FindGclientRoot(full_dir)
234 235 236 237
    if gclient_root and scm_type:
      print(
          'A DEPS file was updated inside a gclient checkout, running gclient '
          'sync.')
238 239 240
      gclient_path = os.path.join(BASE_DIR, 'gclient')
      if sys.platform == 'win32':
        gclient_path += '.bat'
241 242
      with annotated_gclient.temp_filename(suffix='gclient') as f:
        cmd = [
243 244 245
            gclient_path, 'sync',
            '--nohooks',
            '--delete_unversioned_trees',
246
            ]
247 248
        if scm_type == 'svn':
          cmd.extend(['--revision', 'BASE'])
249 250 251 252 253 254 255 256 257 258 259
        if options.revision_mapping:
          cmd.extend(['--output-json', f])

        retcode = subprocess.call(cmd, cwd=gclient_root)

        if retcode == 0 and options.revision_mapping:
          revisions = annotated_gclient.parse_got_revision(
              f, options.revision_mapping)
          annotated_gclient.emit_buildprops(revisions)

        return retcode
260 261 262 263 264 265
  return 0


if __name__ == "__main__":
  fix_encoding.fix_encoding()
  sys.exit(main())