You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			1262 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			1262 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
#!/usr/bin/env python
 | 
						|
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
 | 
						|
# Use of this source code is governed by a BSD-style license that can be
 | 
						|
# found in the LICENSE file.
 | 
						|
 | 
						|
"""Client-side script to send a try job to the try server. It communicates to
 | 
						|
the try server by either writting to a svn/git repository or by directly
 | 
						|
connecting to the server by HTTP.
 | 
						|
"""
 | 
						|
 | 
						|
import contextlib
 | 
						|
import datetime
 | 
						|
import errno
 | 
						|
import getpass
 | 
						|
import itertools
 | 
						|
import json
 | 
						|
import logging
 | 
						|
import optparse
 | 
						|
import os
 | 
						|
import posixpath
 | 
						|
import re
 | 
						|
import shutil
 | 
						|
import sys
 | 
						|
import tempfile
 | 
						|
import urllib
 | 
						|
import urllib2
 | 
						|
import urlparse
 | 
						|
 | 
						|
import breakpad  # pylint: disable=W0611
 | 
						|
 | 
						|
import fix_encoding
 | 
						|
import gcl
 | 
						|
import gclient_utils
 | 
						|
import gerrit_util
 | 
						|
import scm
 | 
						|
import subprocess2
 | 
						|
 | 
						|
 | 
						|
__version__ = '1.2'
 | 
						|
 | 
						|
 | 
						|
# Constants
 | 
						|
HELP_STRING = "Sorry, Tryserver is not available."
 | 
						|
USAGE = r"""%prog [options]
 | 
						|
 | 
						|
Client-side script to send a try job to the try server. It communicates to
 | 
						|
the try server by either writting to a svn repository or by directly connecting
 | 
						|
to the server by HTTP."""
 | 
						|
 | 
						|
EPILOG = """
 | 
						|
Examples:
 | 
						|
  Send a patch directly from rietveld:
 | 
						|
    %(prog)s -R codereview.chromium.org/1337
 | 
						|
        --email recipient@example.com --root src
 | 
						|
 | 
						|
  Try a change against a particular revision:
 | 
						|
    %(prog)s -r 123
 | 
						|
 | 
						|
  Try a change including changes to a sub repository:
 | 
						|
    %(prog)s -s third_party/WebKit
 | 
						|
 | 
						|
  A git patch off a web site (git inserts a/ and b/) and fix the base dir:
 | 
						|
    %(prog)s --url http://url/to/patch.diff --patchlevel 1 --root src
 | 
						|
 | 
						|
  Use svn to store the try job, specify an alternate email address and use a
 | 
						|
  premade diff file on the local drive:
 | 
						|
    %(prog)s --email user@example.com
 | 
						|
            --svn_repo svn://svn.chromium.org/chrome-try/try --diff foo.diff
 | 
						|
 | 
						|
  Running only on a 'mac' slave with revision 123 and clobber first; specify
 | 
						|
  manually the 3 source files to use for the try job:
 | 
						|
    %(prog)s --bot mac --revision 123 --clobber -f src/a.cc -f src/a.h
 | 
						|
            -f include/b.h
 | 
						|
"""
 | 
						|
 | 
						|
GIT_PATCH_DIR_BASENAME = os.path.join('git-try', 'patches-git')
 | 
						|
GIT_BRANCH_FILE = 'ref'
 | 
						|
_GIT_PUSH_ATTEMPTS = 3
 | 
						|
 | 
						|
def DieWithError(message):
 | 
						|
  print >> sys.stderr, message
 | 
						|
  sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def RunCommand(args, error_ok=False, error_message=None, **kwargs):
 | 
						|
  try:
 | 
						|
    return subprocess2.check_output(args, shell=False, **kwargs)
 | 
						|
  except subprocess2.CalledProcessError, e:
 | 
						|
    if not error_ok:
 | 
						|
      DieWithError(
 | 
						|
          'Command "%s" failed.\n%s' % (
 | 
						|
            ' '.join(args), error_message or e.stdout or ''))
 | 
						|
    return e.stdout
 | 
						|
 | 
						|
 | 
						|
def RunGit(args, **kwargs):
 | 
						|
  """Returns stdout."""
 | 
						|
  return RunCommand(['git'] + args, **kwargs)
 | 
						|
 | 
						|
class Error(Exception):
 | 
						|
  """An error during a try job submission.
 | 
						|
 | 
						|
  For this error, trychange.py does not display stack trace, only message
 | 
						|
  """
 | 
						|
 | 
						|
class InvalidScript(Error):
 | 
						|
  def __str__(self):
 | 
						|
    return self.args[0] + '\n' + HELP_STRING
 | 
						|
 | 
						|
 | 
						|
class NoTryServerAccess(Error):
 | 
						|
  def __str__(self):
 | 
						|
    return self.args[0] + '\n' + HELP_STRING
 | 
						|
 | 
						|
def Escape(name):
 | 
						|
  """Escapes characters that could interfere with the file system or try job
 | 
						|
  parsing.
 | 
						|
  """
 | 
						|
  return re.sub(r'[^\w#-]', '_', name)
 | 
						|
 | 
						|
 | 
						|
class SCM(object):
 | 
						|
  """Simplistic base class to implement one function: ProcessOptions."""
 | 
						|
  def __init__(self, options, path, file_list):
 | 
						|
    items = path.split('@')
 | 
						|
    assert len(items) <= 2
 | 
						|
    self.checkout_root = os.path.abspath(items[0])
 | 
						|
    items.append(None)
 | 
						|
    self.diff_against = items[1]
 | 
						|
    self.options = options
 | 
						|
    # Lazy-load file list from the SCM unless files were specified in options.
 | 
						|
    self._files = None
 | 
						|
    self._file_tuples = None
 | 
						|
    if file_list:
 | 
						|
      self._files = file_list
 | 
						|
      self._file_tuples = [('M', f) for f in self.files]
 | 
						|
    self.options.files = None
 | 
						|
    self.codereview_settings = None
 | 
						|
    self.codereview_settings_file = 'codereview.settings'
 | 
						|
    self.toplevel_root = None
 | 
						|
 | 
						|
  def GetFileNames(self):
 | 
						|
    """Return the list of files in the diff."""
 | 
						|
    return self.files
 | 
						|
 | 
						|
  def GetCodeReviewSetting(self, key):
 | 
						|
    """Returns a value for the given key for this repository.
 | 
						|
 | 
						|
    Uses gcl-style settings from the repository.
 | 
						|
    """
 | 
						|
    if gcl:
 | 
						|
      gcl_setting = gcl.GetCodeReviewSetting(key)
 | 
						|
      if gcl_setting != '':
 | 
						|
        return gcl_setting
 | 
						|
    if self.codereview_settings is None:
 | 
						|
      self.codereview_settings = {}
 | 
						|
      settings_file = self.ReadRootFile(self.codereview_settings_file)
 | 
						|
      if settings_file:
 | 
						|
        for line in settings_file.splitlines():
 | 
						|
          if not line or line.lstrip().startswith('#'):
 | 
						|
            continue
 | 
						|
          k, v = line.split(":", 1)
 | 
						|
          self.codereview_settings[k.strip()] = v.strip()
 | 
						|
    return self.codereview_settings.get(key, '')
 | 
						|
 | 
						|
  def _GclStyleSettings(self):
 | 
						|
    """Set default settings based on the gcl-style settings from the repository.
 | 
						|
 | 
						|
    The settings in the self.options object will only be set if no previous
 | 
						|
    value exists (i.e. command line flags to the try command will override the
 | 
						|
    settings in codereview.settings).
 | 
						|
    """
 | 
						|
    settings = {
 | 
						|
      'port': self.GetCodeReviewSetting('TRYSERVER_HTTP_PORT'),
 | 
						|
      'host': self.GetCodeReviewSetting('TRYSERVER_HTTP_HOST'),
 | 
						|
      'svn_repo': self.GetCodeReviewSetting('TRYSERVER_SVN_URL'),
 | 
						|
      'gerrit_url': self.GetCodeReviewSetting('TRYSERVER_GERRIT_URL'),
 | 
						|
      'git_repo': self.GetCodeReviewSetting('TRYSERVER_GIT_URL'),
 | 
						|
      'project': self.GetCodeReviewSetting('TRYSERVER_PROJECT'),
 | 
						|
      # Primarily for revision=auto
 | 
						|
      'revision': self.GetCodeReviewSetting('TRYSERVER_REVISION'),
 | 
						|
      'root': self.GetCodeReviewSetting('TRYSERVER_ROOT'),
 | 
						|
      'patchlevel': self.GetCodeReviewSetting('TRYSERVER_PATCHLEVEL'),
 | 
						|
    }
 | 
						|
    logging.info('\n'.join(['%s: %s' % (k, v)
 | 
						|
                            for (k, v) in settings.iteritems() if v]))
 | 
						|
    for (k, v) in settings.iteritems():
 | 
						|
      # Avoid overwriting options already set using command line flags.
 | 
						|
      if v and getattr(self.options, k) is None:
 | 
						|
        setattr(self.options, k, v)
 | 
						|
 | 
						|
  def AutomagicalSettings(self):
 | 
						|
    """Determines settings based on supported code review and checkout tools.
 | 
						|
    """
 | 
						|
    # Try to find gclient or repo root first.
 | 
						|
    if not self.options.no_search:
 | 
						|
      self.toplevel_root = gclient_utils.FindGclientRoot(self.checkout_root)
 | 
						|
      if self.toplevel_root:
 | 
						|
        logging.info('Found .gclient at %s' % self.toplevel_root)
 | 
						|
      else:
 | 
						|
        self.toplevel_root = gclient_utils.FindFileUpwards(
 | 
						|
            os.path.join('..', '.repo'), self.checkout_root)
 | 
						|
        if self.toplevel_root:
 | 
						|
          logging.info('Found .repo dir at %s'
 | 
						|
                       % os.path.dirname(self.toplevel_root))
 | 
						|
 | 
						|
      # Parse TRYSERVER_* settings from codereview.settings before falling back
 | 
						|
      # on setting self.options.root manually further down. Otherwise
 | 
						|
      # TRYSERVER_ROOT would never be used in codereview.settings.
 | 
						|
      self._GclStyleSettings()
 | 
						|
 | 
						|
      if self.toplevel_root and not self.options.root:
 | 
						|
        assert os.path.abspath(self.toplevel_root) == self.toplevel_root
 | 
						|
        self.options.root = gclient_utils.PathDifference(self.toplevel_root,
 | 
						|
                                                         self.checkout_root)
 | 
						|
    else:
 | 
						|
      self._GclStyleSettings()
 | 
						|
 | 
						|
  def ReadRootFile(self, filename):
 | 
						|
    cur = self.checkout_root
 | 
						|
    root = self.toplevel_root or self.checkout_root
 | 
						|
 | 
						|
    assert cur.startswith(root), (root, cur)
 | 
						|
    while cur.startswith(root):
 | 
						|
      filepath = os.path.join(cur, filename)
 | 
						|
      if os.path.isfile(filepath):
 | 
						|
        logging.info('Found %s at %s' % (filename, cur))
 | 
						|
        return gclient_utils.FileRead(filepath)
 | 
						|
      cur = os.path.dirname(cur)
 | 
						|
    logging.warning('Didn\'t find %s' % filename)
 | 
						|
    return None
 | 
						|
 | 
						|
  def _SetFileTuples(self, file_tuples):
 | 
						|
    excluded = ['!', '?', 'X', ' ', '~']
 | 
						|
    def Excluded(f):
 | 
						|
      if f[0][0] in excluded:
 | 
						|
        return True
 | 
						|
      for r in self.options.exclude:
 | 
						|
        if re.search(r, f[1]):
 | 
						|
          logging.info('Ignoring "%s"' % f[1])
 | 
						|
          return True
 | 
						|
      return False
 | 
						|
 | 
						|
    self._file_tuples = [f for f in file_tuples if not Excluded(f)]
 | 
						|
    self._files = [f[1] for f in self._file_tuples]
 | 
						|
 | 
						|
  def CaptureStatus(self):
 | 
						|
    """Returns the 'svn status' emulated output as an array of (status, file)
 | 
						|
       tuples."""
 | 
						|
    raise NotImplementedError(
 | 
						|
        "abstract method -- subclass %s must override" % self.__class__)
 | 
						|
 | 
						|
  @property
 | 
						|
  def files(self):
 | 
						|
    if self._files is None:
 | 
						|
      self._SetFileTuples(self.CaptureStatus())
 | 
						|
    return self._files
 | 
						|
 | 
						|
  @property
 | 
						|
  def file_tuples(self):
 | 
						|
    if self._file_tuples is None:
 | 
						|
      self._SetFileTuples(self.CaptureStatus())
 | 
						|
    return self._file_tuples
 | 
						|
 | 
						|
 | 
						|
class SVN(SCM):
 | 
						|
  """Gathers the options and diff for a subversion checkout."""
 | 
						|
  def __init__(self, *args, **kwargs):
 | 
						|
    SCM.__init__(self, *args, **kwargs)
 | 
						|
    self.checkout_root = scm.SVN.GetCheckoutRoot(self.checkout_root)
 | 
						|
    if not self.options.email:
 | 
						|
      # Assumes the svn credential is an email address.
 | 
						|
      self.options.email = scm.SVN.GetEmail(self.checkout_root)
 | 
						|
    logging.info("SVN(%s)" % self.checkout_root)
 | 
						|
 | 
						|
  def ReadRootFile(self, filename):
 | 
						|
    data = SCM.ReadRootFile(self, filename)
 | 
						|
    if data:
 | 
						|
      return data
 | 
						|
 | 
						|
    # Try to search on the subversion repository for the file.
 | 
						|
    if not gcl:
 | 
						|
      return None
 | 
						|
    data = gcl.GetCachedFile(filename)
 | 
						|
    logging.debug('%s:\n%s' % (filename, data))
 | 
						|
    return data
 | 
						|
 | 
						|
  def CaptureStatus(self):
 | 
						|
    return scm.SVN.CaptureStatus(None, self.checkout_root)
 | 
						|
 | 
						|
  def GenerateDiff(self):
 | 
						|
    """Returns a string containing the diff for the given file list.
 | 
						|
 | 
						|
    The files in the list should either be absolute paths or relative to the
 | 
						|
    given root.
 | 
						|
    """
 | 
						|
    return scm.SVN.GenerateDiff(self.files, self.checkout_root, full_move=True,
 | 
						|
                                revision=self.diff_against)
 | 
						|
 | 
						|
 | 
						|
class GIT(SCM):
 | 
						|
  """Gathers the options and diff for a git checkout."""
 | 
						|
  def __init__(self, *args, **kwargs):
 | 
						|
    SCM.__init__(self, *args, **kwargs)
 | 
						|
    self.checkout_root = scm.GIT.GetCheckoutRoot(self.checkout_root)
 | 
						|
    if not self.options.name:
 | 
						|
      self.options.name = scm.GIT.GetPatchName(self.checkout_root)
 | 
						|
    if not self.options.email:
 | 
						|
      self.options.email = scm.GIT.GetEmail(self.checkout_root)
 | 
						|
    if not self.diff_against:
 | 
						|
      self.diff_against = scm.GIT.GetUpstreamBranch(self.checkout_root)
 | 
						|
      if not self.diff_against:
 | 
						|
        raise NoTryServerAccess(
 | 
						|
            "Unable to determine default branch to diff against. "
 | 
						|
            "Verify this branch is set up to track another"
 | 
						|
            "(via the --track argument to \"git checkout -b ...\"")
 | 
						|
    logging.info("GIT(%s)" % self.checkout_root)
 | 
						|
 | 
						|
  def CaptureStatus(self):
 | 
						|
    return scm.GIT.CaptureStatus(
 | 
						|
        [],
 | 
						|
        self.checkout_root.replace(os.sep, '/'),
 | 
						|
        self.diff_against)
 | 
						|
 | 
						|
  def GenerateDiff(self):
 | 
						|
    if RunGit(['diff-index', 'HEAD']):
 | 
						|
      print 'Cannot try with a dirty tree.  You must commit locally first.'
 | 
						|
      return None
 | 
						|
    return scm.GIT.GenerateDiff(
 | 
						|
        self.checkout_root,
 | 
						|
        files=self.files,
 | 
						|
        full_move=True,
 | 
						|
        branch=self.diff_against)
 | 
						|
 | 
						|
 | 
						|
def _ParseBotList(botlist, testfilter):
 | 
						|
  """Parses bot configurations from a list of strings."""
 | 
						|
  bots = []
 | 
						|
  if testfilter:
 | 
						|
    for bot in itertools.chain.from_iterable(botspec.split(',')
 | 
						|
                                             for botspec in botlist):
 | 
						|
      tests = set()
 | 
						|
      if ':' in bot:
 | 
						|
        if bot.endswith(':compile'):
 | 
						|
          tests |= set(['compile'])
 | 
						|
        else:
 | 
						|
          raise ValueError(
 | 
						|
              'Can\'t use both --testfilter and --bot builder:test formats '
 | 
						|
              'at the same time')
 | 
						|
 | 
						|
      bots.append((bot, tests))
 | 
						|
  else:
 | 
						|
    for botspec in botlist:
 | 
						|
      botname = botspec.split(':')[0]
 | 
						|
      tests = set()
 | 
						|
      if ':' in botspec:
 | 
						|
        tests |= set(filter(None, botspec.split(':')[1].split(',')))
 | 
						|
      bots.append((botname, tests))
 | 
						|
  return bots
 | 
						|
 | 
						|
 | 
						|
def _ApplyTestFilter(testfilter, bot_spec):
 | 
						|
  """Applies testfilter from CLI.
 | 
						|
 | 
						|
  Specifying a testfilter strips off any builder-specified tests (except for
 | 
						|
  compile).
 | 
						|
  """
 | 
						|
  if testfilter:
 | 
						|
    return [(botname, set(testfilter) | (tests & set(['compile'])))
 | 
						|
            for botname, tests in bot_spec]
 | 
						|
  else:
 | 
						|
    return bot_spec
 | 
						|
 | 
						|
 | 
						|
def _GenTSBotSpec(checkouts, change, changed_files, options):
 | 
						|
  bot_spec = []
 | 
						|
  # Get try slaves from PRESUBMIT.py files if not specified.
 | 
						|
  # Even if the diff comes from options.url, use the local checkout for bot
 | 
						|
  # selection.
 | 
						|
  try:
 | 
						|
    import presubmit_support
 | 
						|
    root_presubmit = checkouts[0].ReadRootFile('PRESUBMIT.py')
 | 
						|
    if not change:
 | 
						|
      if not changed_files:
 | 
						|
        changed_files = checkouts[0].file_tuples
 | 
						|
      change = presubmit_support.Change(options.name,
 | 
						|
                                        '',
 | 
						|
                                        checkouts[0].checkout_root,
 | 
						|
                                        changed_files,
 | 
						|
                                        options.issue,
 | 
						|
                                        options.patchset,
 | 
						|
                                        options.email)
 | 
						|
    masters = presubmit_support.DoGetTryMasters(
 | 
						|
        change,
 | 
						|
        checkouts[0].GetFileNames(),
 | 
						|
        checkouts[0].checkout_root,
 | 
						|
        root_presubmit,
 | 
						|
        options.project,
 | 
						|
        options.verbose,
 | 
						|
        sys.stdout)
 | 
						|
 | 
						|
    # Compatibility for old checkouts and bots that were on tryserver.chromium.
 | 
						|
    trybots = masters.get('tryserver.chromium', [])
 | 
						|
 | 
						|
    # Compatibility for checkouts that are not using tryserver.chromium
 | 
						|
    # but are stuck with git-try or gcl-try.
 | 
						|
    if not trybots and len(masters) == 1:
 | 
						|
      trybots = masters.values()[0]
 | 
						|
 | 
						|
    if trybots:
 | 
						|
      old_style = filter(lambda x: isinstance(x, basestring), trybots)
 | 
						|
      new_style = filter(lambda x: isinstance(x, tuple), trybots)
 | 
						|
 | 
						|
      # _ParseBotList's testfilter is set to None otherwise it will complain.
 | 
						|
      bot_spec = _ApplyTestFilter(options.testfilter,
 | 
						|
                                  _ParseBotList(old_style, None))
 | 
						|
 | 
						|
      bot_spec.extend(_ApplyTestFilter(options.testfilter, new_style))
 | 
						|
 | 
						|
  except ImportError:
 | 
						|
    pass
 | 
						|
 | 
						|
  return bot_spec
 | 
						|
 | 
						|
 | 
						|
def _ParseSendChangeOptions(bot_spec, options):
 | 
						|
  """Parse common options passed to _SendChangeHTTP, _SendChangeSVN and
 | 
						|
  _SendChangeGit.
 | 
						|
  """
 | 
						|
  values = [
 | 
						|
      ('user', options.user),
 | 
						|
      ('name', options.name),
 | 
						|
  ]
 | 
						|
  # A list of options to copy.
 | 
						|
  optional_values = (
 | 
						|
      'email',
 | 
						|
      'revision',
 | 
						|
      'root',
 | 
						|
      'patchlevel',
 | 
						|
      'issue',
 | 
						|
      'patchset',
 | 
						|
      'target',
 | 
						|
      'project',
 | 
						|
  )
 | 
						|
  for option_name in optional_values:
 | 
						|
    value = getattr(options, option_name)
 | 
						|
    if value:
 | 
						|
      values.append((option_name, value))
 | 
						|
 | 
						|
  # Not putting clobber to optional_names
 | 
						|
  # because it used to have lower-case 'true'.
 | 
						|
  if options.clobber:
 | 
						|
    values.append(('clobber', 'true'))
 | 
						|
 | 
						|
  for bot, tests in bot_spec:
 | 
						|
    values.append(('bot', ('%s:%s' % (bot, ','.join(tests)))))
 | 
						|
 | 
						|
  return values
 | 
						|
 | 
						|
 | 
						|
def _SendChangeHTTP(bot_spec, options):
 | 
						|
  """Send a change to the try server using the HTTP protocol."""
 | 
						|
  if not options.host:
 | 
						|
    raise NoTryServerAccess('Please use the --host option to specify the try '
 | 
						|
        'server host to connect to.')
 | 
						|
  if not options.port:
 | 
						|
    raise NoTryServerAccess('Please use the --port option to specify the try '
 | 
						|
        'server port to connect to.')
 | 
						|
 | 
						|
  values = _ParseSendChangeOptions(bot_spec, options)
 | 
						|
  values.append(('patch', options.diff))
 | 
						|
 | 
						|
  url = 'http://%s:%s/send_try_patch' % (options.host, options.port)
 | 
						|
 | 
						|
  logging.info('Sending by HTTP')
 | 
						|
  logging.info(''.join("%s=%s\n" % (k, v) for k, v in values))
 | 
						|
  logging.info(url)
 | 
						|
  logging.info(options.diff)
 | 
						|
  if options.dry_run:
 | 
						|
    return
 | 
						|
 | 
						|
  try:
 | 
						|
    logging.info('Opening connection...')
 | 
						|
    connection = urllib2.urlopen(url, urllib.urlencode(values))
 | 
						|
    logging.info('Done')
 | 
						|
  except IOError, e:
 | 
						|
    logging.info(str(e))
 | 
						|
    if bot_spec and len(e.args) > 2 and e.args[2] == 'got a bad status line':
 | 
						|
      raise NoTryServerAccess('%s is unaccessible. Bad --bot argument?' % url)
 | 
						|
    else:
 | 
						|
      raise NoTryServerAccess('%s is unaccessible. Reason: %s' % (url,
 | 
						|
                                                                  str(e.args)))
 | 
						|
  if not connection:
 | 
						|
    raise NoTryServerAccess('%s is unaccessible.' % url)
 | 
						|
  logging.info('Reading response...')
 | 
						|
  response = connection.read()
 | 
						|
  logging.info('Done')
 | 
						|
  if response != 'OK':
 | 
						|
    raise NoTryServerAccess('%s is unaccessible. Got:\n%s' % (url, response))
 | 
						|
 | 
						|
  PrintSuccess(bot_spec, options)
 | 
						|
 | 
						|
@contextlib.contextmanager
 | 
						|
def _TempFilename(name, contents=None):
 | 
						|
  """Create a temporary directory, append the specified name and yield.
 | 
						|
 | 
						|
  In contrast to NamedTemporaryFile, does not keep the file open.
 | 
						|
  Deletes the file on __exit__.
 | 
						|
  """
 | 
						|
  temp_dir = tempfile.mkdtemp(prefix=name)
 | 
						|
  try:
 | 
						|
    path = os.path.join(temp_dir, name)
 | 
						|
    if contents is not None:
 | 
						|
      with open(path, 'wb') as f:
 | 
						|
        f.write(contents)
 | 
						|
    yield path
 | 
						|
  finally:
 | 
						|
    shutil.rmtree(temp_dir, True)
 | 
						|
 | 
						|
 | 
						|
@contextlib.contextmanager
 | 
						|
def _PrepareDescriptionAndPatchFiles(description, options):
 | 
						|
  """Creates temporary files with description and patch.
 | 
						|
 | 
						|
  __enter__ called on the return value returns a tuple of patch_filename and
 | 
						|
  description_filename.
 | 
						|
 | 
						|
  Args:
 | 
						|
    description: contents of description file.
 | 
						|
    options: patchset options object. Must have attributes: user,
 | 
						|
        name (of patch) and diff (contents of patch).
 | 
						|
  """
 | 
						|
  current_time = str(datetime.datetime.now()).replace(':', '.')
 | 
						|
  patch_basename = '%s.%s.%s.diff' % (Escape(options.user),
 | 
						|
                                      Escape(options.name), current_time)
 | 
						|
  with _TempFilename('description', description) as description_filename:
 | 
						|
    with _TempFilename(patch_basename, options.diff) as patch_filename:
 | 
						|
      yield patch_filename, description_filename
 | 
						|
 | 
						|
 | 
						|
def _SendChangeSVN(bot_spec, options):
 | 
						|
  """Send a change to the try server by committing a diff file on a subversion
 | 
						|
  server."""
 | 
						|
  if not options.svn_repo:
 | 
						|
    raise NoTryServerAccess('Please use the --svn_repo option to specify the'
 | 
						|
                            ' try server svn repository to connect to.')
 | 
						|
 | 
						|
  values = _ParseSendChangeOptions(bot_spec, options)
 | 
						|
  description = ''.join("%s=%s\n" % (k, v) for k, v in values)
 | 
						|
  logging.info('Sending by SVN')
 | 
						|
  logging.info(description)
 | 
						|
  logging.info(options.svn_repo)
 | 
						|
  logging.info(options.diff)
 | 
						|
  if options.dry_run:
 | 
						|
    return
 | 
						|
 | 
						|
  with _PrepareDescriptionAndPatchFiles(description, options) as (
 | 
						|
       patch_filename, description_filename):
 | 
						|
    if sys.platform == "cygwin":
 | 
						|
      # Small chromium-specific issue here:
 | 
						|
      # git-try uses /usr/bin/python on cygwin but svn.bat will be used
 | 
						|
      # instead of /usr/bin/svn by default. That causes bad things(tm) since
 | 
						|
      # Windows' svn.exe has no clue about cygwin paths. Hence force to use
 | 
						|
      # the cygwin version in this particular context.
 | 
						|
      exe = "/usr/bin/svn"
 | 
						|
    else:
 | 
						|
      exe = "svn"
 | 
						|
    patch_dir = os.path.dirname(patch_filename)
 | 
						|
    command = [exe, 'import', '-q', patch_dir, options.svn_repo, '--file',
 | 
						|
               description_filename]
 | 
						|
    if scm.SVN.AssertVersion("1.5")[0]:
 | 
						|
      command.append('--no-ignore')
 | 
						|
 | 
						|
    try:
 | 
						|
      subprocess2.check_call(command)
 | 
						|
    except subprocess2.CalledProcessError, e:
 | 
						|
      raise NoTryServerAccess(str(e))
 | 
						|
 | 
						|
  PrintSuccess(bot_spec, options)
 | 
						|
 | 
						|
def _GetPatchGitRepo(git_url):
 | 
						|
  """Gets a path to a Git repo with patches.
 | 
						|
 | 
						|
  Stores patches in .git/git-try/patches-git directory, a git repo. If it
 | 
						|
  doesn't exist yet or its origin URL is different, cleans up and clones it.
 | 
						|
  If it existed before, then pulls changes.
 | 
						|
 | 
						|
  Does not support SVN repo.
 | 
						|
 | 
						|
  Returns a path to the directory with patches.
 | 
						|
  """
 | 
						|
  git_dir = scm.GIT.GetGitDir(os.getcwd())
 | 
						|
  patch_dir = os.path.join(git_dir, GIT_PATCH_DIR_BASENAME)
 | 
						|
 | 
						|
  logging.info('Looking for git repo for patches')
 | 
						|
  # Is there already a repo with the expected url or should we clone?
 | 
						|
  clone = True
 | 
						|
  if os.path.exists(patch_dir) and scm.GIT.IsInsideWorkTree(patch_dir):
 | 
						|
    existing_url = scm.GIT.Capture(
 | 
						|
        ['config', '--local', 'remote.origin.url'],
 | 
						|
        cwd=patch_dir)
 | 
						|
    clone = existing_url != git_url
 | 
						|
 | 
						|
  if clone:
 | 
						|
    if os.path.exists(patch_dir):
 | 
						|
      logging.info('Cleaning up')
 | 
						|
      shutil.rmtree(patch_dir, True)
 | 
						|
    logging.info('Cloning patch repo')
 | 
						|
    scm.GIT.Capture(['clone', git_url, GIT_PATCH_DIR_BASENAME], cwd=git_dir)
 | 
						|
    email = scm.GIT.GetEmail(cwd=os.getcwd())
 | 
						|
    scm.GIT.Capture(['config', '--local', 'user.email', email], cwd=patch_dir)
 | 
						|
  else:
 | 
						|
    if scm.GIT.IsWorkTreeDirty(patch_dir):
 | 
						|
      logging.info('Work dir is dirty: hard reset!')
 | 
						|
      scm.GIT.Capture(['reset', '--hard'], cwd=patch_dir)
 | 
						|
    logging.info('Updating patch repo')
 | 
						|
    scm.GIT.Capture(['pull', 'origin', 'master'], cwd=patch_dir)
 | 
						|
 | 
						|
  return os.path.abspath(patch_dir)
 | 
						|
 | 
						|
 | 
						|
def _SendChangeGit(bot_spec, options):
 | 
						|
  """Sends a change to the try server by committing a diff file to a GIT repo.
 | 
						|
 | 
						|
  Creates a temp orphan branch, commits patch.diff, creates a ref pointing to
 | 
						|
  that commit, deletes the temp branch, checks master out, adds 'ref' file
 | 
						|
  containing the name of the new ref, pushes master and the ref to the origin.
 | 
						|
 | 
						|
  TODO: instead of creating a temp branch, use git-commit-tree.
 | 
						|
  """
 | 
						|
 | 
						|
  if not options.git_repo:
 | 
						|
    raise NoTryServerAccess('Please use the --git_repo option to specify the '
 | 
						|
                            'try server git repository to connect to.')
 | 
						|
 | 
						|
  values = _ParseSendChangeOptions(bot_spec, options)
 | 
						|
  comment_subject = '%s.%s' % (options.user, options.name)
 | 
						|
  comment_body = ''.join("%s=%s\n" % (k, v) for k, v in values)
 | 
						|
  description = '%s\n\n%s' % (comment_subject, comment_body)
 | 
						|
  logging.info('Sending by GIT')
 | 
						|
  logging.info(description)
 | 
						|
  logging.info(options.git_repo)
 | 
						|
  logging.info(options.diff)
 | 
						|
  if options.dry_run:
 | 
						|
    return
 | 
						|
 | 
						|
  patch_dir = _GetPatchGitRepo(options.git_repo)
 | 
						|
  def patch_git(*args):
 | 
						|
    return scm.GIT.Capture(list(args), cwd=patch_dir)
 | 
						|
  def add_and_commit(filename, comment_filename):
 | 
						|
    patch_git('add', filename)
 | 
						|
    patch_git('commit', '-F', comment_filename)
 | 
						|
 | 
						|
  assert scm.GIT.IsInsideWorkTree(patch_dir)
 | 
						|
  assert not scm.GIT.IsWorkTreeDirty(patch_dir)
 | 
						|
 | 
						|
  with _PrepareDescriptionAndPatchFiles(description, options) as (
 | 
						|
       patch_filename, description_filename):
 | 
						|
    logging.info('Committing patch')
 | 
						|
 | 
						|
    temp_branch = 'tmp_patch'
 | 
						|
    target_ref = 'refs/patches/%s/%s' % (
 | 
						|
        Escape(options.user),
 | 
						|
        os.path.basename(patch_filename).replace(' ','_'))
 | 
						|
    target_filename = os.path.join(patch_dir, 'patch.diff')
 | 
						|
    branch_file = os.path.join(patch_dir, GIT_BRANCH_FILE)
 | 
						|
 | 
						|
    patch_git('checkout', 'master')
 | 
						|
    try:
 | 
						|
      # Try deleting an existing temp branch, if any.
 | 
						|
      try:
 | 
						|
        patch_git('branch', '-D', temp_branch)
 | 
						|
        logging.debug('Deleted an existing temp branch.')
 | 
						|
      except subprocess2.CalledProcessError:
 | 
						|
        pass
 | 
						|
      # Create a new branch and put the patch there.
 | 
						|
      patch_git('checkout', '--orphan', temp_branch)
 | 
						|
      patch_git('reset')
 | 
						|
      patch_git('clean', '-f')
 | 
						|
      shutil.copyfile(patch_filename, target_filename)
 | 
						|
      add_and_commit(target_filename, description_filename)
 | 
						|
      assert not scm.GIT.IsWorkTreeDirty(patch_dir)
 | 
						|
 | 
						|
      # Create a ref and point it to the commit referenced by temp_branch.
 | 
						|
      patch_git('update-ref', target_ref, temp_branch)
 | 
						|
 | 
						|
      # Delete the temp ref.
 | 
						|
      patch_git('checkout', 'master')
 | 
						|
      patch_git('branch', '-D', temp_branch)
 | 
						|
 | 
						|
      # Update the branch file in the master.
 | 
						|
      def update_branch():
 | 
						|
        with open(branch_file, 'w') as f:
 | 
						|
          f.write(target_ref)
 | 
						|
        add_and_commit(branch_file, description_filename)
 | 
						|
 | 
						|
      update_branch()
 | 
						|
 | 
						|
      # Push master and target_ref to origin.
 | 
						|
      logging.info('Pushing patch')
 | 
						|
      for attempt in xrange(_GIT_PUSH_ATTEMPTS):
 | 
						|
        try:
 | 
						|
          patch_git('push', 'origin', 'master', target_ref)
 | 
						|
        except subprocess2.CalledProcessError as e:
 | 
						|
          is_last = attempt == _GIT_PUSH_ATTEMPTS - 1
 | 
						|
          if is_last:
 | 
						|
            raise NoTryServerAccess(str(e))
 | 
						|
          # Fetch, reset, update branch file again.
 | 
						|
          patch_git('fetch', 'origin')
 | 
						|
          patch_git('reset', '--hard', 'origin/master')
 | 
						|
          update_branch()
 | 
						|
    except subprocess2.CalledProcessError, e:
 | 
						|
      # Restore state.
 | 
						|
      patch_git('checkout', 'master')
 | 
						|
      patch_git('reset', '--hard', 'origin/master')
 | 
						|
      raise
 | 
						|
 | 
						|
  PrintSuccess(bot_spec, options)
 | 
						|
 | 
						|
def _SendChangeGerrit(bot_spec, options):
 | 
						|
  """Posts a try job to a Gerrit change.
 | 
						|
 | 
						|
  Reads Change-Id from the HEAD commit, resolves the current revision, checks
 | 
						|
  that local revision matches the uploaded one, posts a try job in form of a
 | 
						|
  message, sets Tryjob-Request label to 1.
 | 
						|
 | 
						|
  Gerrit message format: starts with !tryjob, optionally followed by a tryjob
 | 
						|
  definition in JSON format:
 | 
						|
      buildNames: list of strings specifying build names.
 | 
						|
  """
 | 
						|
 | 
						|
  logging.info('Sending by Gerrit')
 | 
						|
  if not options.gerrit_url:
 | 
						|
    raise NoTryServerAccess('Please use --gerrit_url option to specify the '
 | 
						|
                            'Gerrit instance url to connect to')
 | 
						|
  gerrit_host = urlparse.urlparse(options.gerrit_url).hostname
 | 
						|
  logging.debug('Gerrit host: %s' % gerrit_host)
 | 
						|
 | 
						|
  def GetChangeId(commmitish):
 | 
						|
    """Finds Change-ID of the HEAD commit."""
 | 
						|
    CHANGE_ID_RGX = '^Change-Id: (I[a-f0-9]{10,})'
 | 
						|
    comment = scm.GIT.Capture(['log', '-1', commmitish, '--format=%b'],
 | 
						|
                              cwd=os.getcwd())
 | 
						|
    change_id_match = re.search(CHANGE_ID_RGX, comment, re.I | re.M)
 | 
						|
    if not change_id_match:
 | 
						|
      raise Error('Change-Id was not found in the HEAD commit. Make sure you '
 | 
						|
                  'have a Git hook installed that generates and inserts a '
 | 
						|
                  'Change-Id into a commit message automatically.')
 | 
						|
    change_id = change_id_match.group(1)
 | 
						|
    return change_id
 | 
						|
 | 
						|
  def FormatMessage():
 | 
						|
    # Build job definition.
 | 
						|
    job_def = {}
 | 
						|
    builderNames = [builder for builder, _ in bot_spec]
 | 
						|
    if builderNames:
 | 
						|
      job_def['builderNames'] = builderNames
 | 
						|
 | 
						|
    # Format message.
 | 
						|
    msg = '!tryjob'
 | 
						|
    if job_def:
 | 
						|
      msg = '%s %s' % (msg, json.dumps(job_def, sort_keys=True))
 | 
						|
    return msg
 | 
						|
 | 
						|
  def PostTryjob(message):
 | 
						|
    logging.info('Posting gerrit message: %s' % message)
 | 
						|
    if not options.dry_run:
 | 
						|
      # Post a message and set TryJob=1 label.
 | 
						|
      try:
 | 
						|
        gerrit_util.SetReview(gerrit_host, change_id, msg=message,
 | 
						|
                              labels={'Tryjob-Request': 1})
 | 
						|
      except gerrit_util.GerritError, e:
 | 
						|
        if e.http_status == 400:
 | 
						|
          raise Error(e.message)
 | 
						|
        else:
 | 
						|
          raise
 | 
						|
 | 
						|
  head_sha = scm.GIT.Capture(['log', '-1', '--format=%H'], cwd=os.getcwd())
 | 
						|
 | 
						|
  change_id = GetChangeId(head_sha)
 | 
						|
 | 
						|
  try:
 | 
						|
    # Check that the uploaded revision matches the local one.
 | 
						|
    changes = gerrit_util.GetChangeCurrentRevision(gerrit_host, change_id)
 | 
						|
  except gerrit_util.GerritAuthenticationError, e:
 | 
						|
    raise NoTryServerAccess(e.message)
 | 
						|
 | 
						|
  assert len(changes) <= 1, 'Multiple changes with id %s' % change_id
 | 
						|
  if not changes:
 | 
						|
    raise Error('A change %s was not found on the server. Was it uploaded?' %
 | 
						|
                change_id)
 | 
						|
  logging.debug('Found Gerrit change: %s' % changes[0])
 | 
						|
  if changes[0]['current_revision'] != head_sha:
 | 
						|
    raise Error('Please upload your latest local changes to Gerrit.')
 | 
						|
 | 
						|
  # Post a try job.
 | 
						|
  message = FormatMessage()
 | 
						|
  PostTryjob(message)
 | 
						|
  change_url = urlparse.urljoin(options.gerrit_url,
 | 
						|
                                '/#/c/%s' % changes[0]['_number'])
 | 
						|
  print('A tryjob was posted on change %s' % change_url)
 | 
						|
 | 
						|
def PrintSuccess(bot_spec, options):
 | 
						|
  if not options.dry_run:
 | 
						|
    text = 'Patch \'%s\' sent to try server' % options.name
 | 
						|
    if bot_spec:
 | 
						|
      text += ': %s' % ', '.join(
 | 
						|
          '%s:%s' % (b[0], ','.join(b[1])) for b in bot_spec)
 | 
						|
    print(text)
 | 
						|
 | 
						|
 | 
						|
def GuessVCS(options, path, file_list):
 | 
						|
  """Helper to guess the version control system.
 | 
						|
 | 
						|
  NOTE: Very similar to upload.GuessVCS. Doesn't look for hg since we don't
 | 
						|
  support it yet.
 | 
						|
 | 
						|
  This examines the path directory, guesses which SCM we're using, and
 | 
						|
  returns an instance of the appropriate class.  Exit with an error if we can't
 | 
						|
  figure it out.
 | 
						|
 | 
						|
  Returns:
 | 
						|
    A SCM instance. Exits if the SCM can't be guessed.
 | 
						|
  """
 | 
						|
  __pychecker__ = 'no-returnvalues'
 | 
						|
  real_path = path.split('@')[0]
 | 
						|
  logging.info("GuessVCS(%s)" % path)
 | 
						|
  # Subversion has a .svn in all working directories.
 | 
						|
  if os.path.isdir(os.path.join(real_path, '.svn')):
 | 
						|
    return SVN(options, path, file_list)
 | 
						|
 | 
						|
  # Git has a command to test if you're in a git tree.
 | 
						|
  # Try running it, but don't die if we don't have git installed.
 | 
						|
  try:
 | 
						|
    subprocess2.check_output(
 | 
						|
        ['git', 'rev-parse', '--is-inside-work-tree'], cwd=real_path,
 | 
						|
        stderr=subprocess2.VOID)
 | 
						|
    return GIT(options, path, file_list)
 | 
						|
  except OSError, e:
 | 
						|
    if e.errno != errno.ENOENT:
 | 
						|
      raise
 | 
						|
  except subprocess2.CalledProcessError, e:
 | 
						|
    if e.returncode != errno.ENOENT and e.returncode != 128:
 | 
						|
      # ENOENT == 2 = they don't have git installed.
 | 
						|
      # 128 = git error code when not in a repo.
 | 
						|
      logging.warning('Unexpected error code: %s' % e.returncode)
 | 
						|
      raise
 | 
						|
  raise NoTryServerAccess(
 | 
						|
      ( 'Could not guess version control system for %s.\n'
 | 
						|
        'Are you in a working copy directory?') % path)
 | 
						|
 | 
						|
 | 
						|
def GetMungedDiff(path_diff, diff):
 | 
						|
  # Munge paths to match svn.
 | 
						|
  changed_files = []
 | 
						|
  for i in range(len(diff)):
 | 
						|
    if diff[i].startswith('--- ') or diff[i].startswith('+++ '):
 | 
						|
      new_file = posixpath.join(path_diff, diff[i][4:]).replace('\\', '/')
 | 
						|
      if diff[i].startswith('--- '):
 | 
						|
        file_path = new_file.split('\t')[0].strip()
 | 
						|
        if file_path.startswith('a/'):
 | 
						|
          file_path = file_path[2:]
 | 
						|
        changed_files.append(('M', file_path))
 | 
						|
      diff[i] = diff[i][0:4] + new_file
 | 
						|
  return (diff, changed_files)
 | 
						|
 | 
						|
 | 
						|
class OptionParser(optparse.OptionParser):
 | 
						|
  def format_epilog(self, _):
 | 
						|
    """Removes epilog formatting."""
 | 
						|
    return self.epilog or ''
 | 
						|
 | 
						|
 | 
						|
def gen_parser(prog):
 | 
						|
  # Parse argv
 | 
						|
  parser = OptionParser(usage=USAGE, version=__version__, prog=prog)
 | 
						|
  parser.add_option("-v", "--verbose", action="count", default=0,
 | 
						|
                    help="Prints debugging infos")
 | 
						|
  group = optparse.OptionGroup(parser, "Result and status")
 | 
						|
  group.add_option("-u", "--user", default=getpass.getuser(),
 | 
						|
                   help="Owner user name [default: %default]")
 | 
						|
  group.add_option("-e", "--email",
 | 
						|
                   default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS',
 | 
						|
                        os.environ.get('EMAIL_ADDRESS')),
 | 
						|
                   help="Email address where to send the results. Use either "
 | 
						|
                        "the TRYBOT_RESULTS_EMAIL_ADDRESS environment "
 | 
						|
                        "variable or EMAIL_ADDRESS to set the email address "
 | 
						|
                        "the try bots report results to [default: %default]")
 | 
						|
  group.add_option("-n", "--name",
 | 
						|
                   help="Descriptive name of the try job")
 | 
						|
  group.add_option("--issue", type='int',
 | 
						|
                   help="Update rietveld issue try job status")
 | 
						|
  group.add_option("--patchset", type='int',
 | 
						|
                   help="Update rietveld issue try job status. This is "
 | 
						|
                        "optional if --issue is used, In that case, the "
 | 
						|
                        "latest patchset will be used.")
 | 
						|
  group.add_option("--dry_run", action='store_true',
 | 
						|
                   help="Don't send the try job. This implies --verbose, so "
 | 
						|
                        "it will print the diff.")
 | 
						|
  parser.add_option_group(group)
 | 
						|
 | 
						|
  group = optparse.OptionGroup(parser, "Try job options")
 | 
						|
  group.add_option(
 | 
						|
      "-b", "--bot", action="append",
 | 
						|
      help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
 | 
						|
            "times to specify multiple builders. ex: "
 | 
						|
            "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
 | 
						|
            "the try server waterfall for the builders name and the tests "
 | 
						|
            "available. Can also be used to specify gtest_filter, e.g. "
 | 
						|
            "-bwin_rel:base_unittests:ValuesTest.*Value"))
 | 
						|
  group.add_option("-B", "--print_bots", action="store_true",
 | 
						|
                    help="Print bots we would use (e.g. from PRESUBMIT.py)"
 | 
						|
                         " and exit.  Do not send patch.  Like --dry_run"
 | 
						|
                         " but less verbose.")
 | 
						|
  group.add_option("-r", "--revision",
 | 
						|
                    help="Revision to use for the try job. If 'auto' is "
 | 
						|
                         "specified, it is resolved to the revision a patch is "
 | 
						|
                         "generated against (Git only). Default: the "
 | 
						|
                         "revision will be determined by the try server; see "
 | 
						|
                         "its waterfall for more info")
 | 
						|
  group.add_option("-c", "--clobber", action="store_true",
 | 
						|
                    help="Force a clobber before building; e.g. don't do an "
 | 
						|
                         "incremental build")
 | 
						|
  # TODO(maruel): help="Select a specific configuration, usually 'debug' or "
 | 
						|
  #                    "'release'"
 | 
						|
  group.add_option("--target", help=optparse.SUPPRESS_HELP)
 | 
						|
 | 
						|
  group.add_option("--project",
 | 
						|
                   help="Override which project to use. Projects are defined "
 | 
						|
                        "server-side to define what default bot set to use")
 | 
						|
 | 
						|
  group.add_option(
 | 
						|
      "-t", "--testfilter", action="append", default=[],
 | 
						|
      help=("Apply a testfilter to all the selected builders. Unless the "
 | 
						|
            "builders configurations are similar, use multiple "
 | 
						|
            "--bot <builder>:<test> arguments."))
 | 
						|
 | 
						|
  parser.add_option_group(group)
 | 
						|
 | 
						|
  group = optparse.OptionGroup(parser, "Patch to run")
 | 
						|
  group.add_option("-f", "--file", default=[], dest="files",
 | 
						|
                   metavar="FILE", action="append",
 | 
						|
                   help="Use many times to list the files to include in the "
 | 
						|
                        "try, relative to the repository root")
 | 
						|
  group.add_option("--diff",
 | 
						|
                   help="File containing the diff to try")
 | 
						|
  group.add_option("--url",
 | 
						|
                   help="Url where to grab a patch, e.g. "
 | 
						|
                        "http://example.com/x.diff")
 | 
						|
  group.add_option("-R", "--rietveld_url", default="codereview.appspot.com",
 | 
						|
                   metavar="URL",
 | 
						|
                   help="Has 2 usages, both refer to the rietveld instance: "
 | 
						|
                        "Specify which code review patch to use as the try job "
 | 
						|
                        "or rietveld instance to update the try job results "
 | 
						|
                        "Default:%default")
 | 
						|
  group.add_option("--root",
 | 
						|
                   help="Root to use for the patch; base subdirectory for "
 | 
						|
                        "patch created in a subdirectory")
 | 
						|
  group.add_option("-p", "--patchlevel", type='int', metavar="LEVEL",
 | 
						|
                   help="Used as -pN parameter to patch")
 | 
						|
  group.add_option("-s", "--sub_rep", action="append", default=[],
 | 
						|
                   help="Subcheckout to use in addition. This is mainly "
 | 
						|
                        "useful for gclient-style checkouts. In git, checkout "
 | 
						|
                        "the branch with changes first. Use @rev or "
 | 
						|
                        "@branch to specify the "
 | 
						|
                        "revision/branch to diff against. If no @branch is "
 | 
						|
                        "given the diff will be against the upstream branch. "
 | 
						|
                        "If @branch then the diff is branch..HEAD. "
 | 
						|
                        "All edits must be checked in.")
 | 
						|
  group.add_option("--no_search", action="store_true",
 | 
						|
                   help=("Disable automatic search for gclient or repo "
 | 
						|
                        "checkout root."))
 | 
						|
  group.add_option("-E", "--exclude", action="append",
 | 
						|
                   default=['ChangeLog'], metavar='REGEXP',
 | 
						|
                   help="Regexp patterns to exclude files. Default: %default")
 | 
						|
  group.add_option("--upstream_branch", action="store",
 | 
						|
                   help="Specify the upstream branch to diff against in the "
 | 
						|
                        "main checkout")
 | 
						|
  parser.add_option_group(group)
 | 
						|
 | 
						|
  group = optparse.OptionGroup(parser, "Access the try server by HTTP")
 | 
						|
  group.add_option("--use_http",
 | 
						|
                   action="store_const",
 | 
						|
                   const=_SendChangeHTTP,
 | 
						|
                   dest="send_patch",
 | 
						|
                   help="Use HTTP to talk to the try server [default]")
 | 
						|
  group.add_option("-H", "--host",
 | 
						|
                   help="Host address")
 | 
						|
  group.add_option("-P", "--port", type="int",
 | 
						|
                   help="HTTP port")
 | 
						|
  parser.add_option_group(group)
 | 
						|
 | 
						|
  group = optparse.OptionGroup(parser, "Access the try server with SVN")
 | 
						|
  group.add_option("--use_svn",
 | 
						|
                   action="store_const",
 | 
						|
                   const=_SendChangeSVN,
 | 
						|
                   dest="send_patch",
 | 
						|
                   help="Use SVN to talk to the try server")
 | 
						|
  group.add_option("-S", "--svn_repo",
 | 
						|
                   metavar="SVN_URL",
 | 
						|
                   help="SVN url to use to write the changes in; --use_svn is "
 | 
						|
                        "implied when using --svn_repo")
 | 
						|
  parser.add_option_group(group)
 | 
						|
 | 
						|
  group = optparse.OptionGroup(parser, "Access the try server with Git")
 | 
						|
  group.add_option("--use_git",
 | 
						|
                   action="store_const",
 | 
						|
                   const=_SendChangeGit,
 | 
						|
                   dest="send_patch",
 | 
						|
                   help="Use GIT to talk to the try server")
 | 
						|
  group.add_option("-G", "--git_repo",
 | 
						|
                   metavar="GIT_URL",
 | 
						|
                   help="GIT url to use to write the changes in; --use_git is "
 | 
						|
                        "implied when using --git_repo")
 | 
						|
  parser.add_option_group(group)
 | 
						|
 | 
						|
  group = optparse.OptionGroup(parser, "Access the try server with Gerrit")
 | 
						|
  group.add_option("--use_gerrit",
 | 
						|
                   action="store_const",
 | 
						|
                   const=_SendChangeGerrit,
 | 
						|
                   dest="send_patch",
 | 
						|
                   help="Use Gerrit to talk to the try server")
 | 
						|
  group.add_option("--gerrit_url",
 | 
						|
                   metavar="GERRIT_URL",
 | 
						|
                   help="Gerrit url to post a tryjob to; --use_gerrit is "
 | 
						|
                        "implied when using --gerrit_url")
 | 
						|
  parser.add_option_group(group)
 | 
						|
 | 
						|
  return parser
 | 
						|
 | 
						|
 | 
						|
def TryChange(argv,
 | 
						|
              change,
 | 
						|
              swallow_exception,
 | 
						|
              prog=None,
 | 
						|
              extra_epilog=None):
 | 
						|
  """
 | 
						|
  Args:
 | 
						|
    argv: Arguments and options.
 | 
						|
    change: Change instance corresponding to the CL.
 | 
						|
    swallow_exception: Whether we raise or swallow exceptions.
 | 
						|
  """
 | 
						|
  parser = gen_parser(prog)
 | 
						|
  epilog = EPILOG % { 'prog': prog }
 | 
						|
  if extra_epilog:
 | 
						|
    epilog += extra_epilog
 | 
						|
  parser.epilog = epilog
 | 
						|
 | 
						|
  options, args = parser.parse_args(argv)
 | 
						|
 | 
						|
  # If they've asked for help, give it to them
 | 
						|
  if len(args) == 1 and args[0] == 'help':
 | 
						|
    parser.print_help()
 | 
						|
    return 0
 | 
						|
 | 
						|
  # If they've said something confusing, don't spawn a try job until you
 | 
						|
  # understand what they want.
 | 
						|
  if args:
 | 
						|
    parser.error('Extra argument(s) "%s" not understood' % ' '.join(args))
 | 
						|
 | 
						|
  if options.dry_run:
 | 
						|
    options.verbose += 1
 | 
						|
 | 
						|
  LOG_FORMAT = '%(levelname)s %(filename)s(%(lineno)d): %(message)s'
 | 
						|
  if not swallow_exception:
 | 
						|
    if options.verbose == 0:
 | 
						|
      logging.basicConfig(level=logging.WARNING, format=LOG_FORMAT)
 | 
						|
    elif options.verbose == 1:
 | 
						|
      logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
 | 
						|
    elif options.verbose > 1:
 | 
						|
      logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
 | 
						|
 | 
						|
  logging.debug(argv)
 | 
						|
 | 
						|
  if (options.patchlevel is not None and
 | 
						|
      (options.patchlevel < 0 or options.patchlevel > 10)):
 | 
						|
    parser.error(
 | 
						|
        'Have you tried --port instead? You probably confused -p and -P.')
 | 
						|
 | 
						|
  # Strip off any @ in the user, otherwise svn gets confused.
 | 
						|
  options.user = options.user.split('@', 1)[0]
 | 
						|
 | 
						|
  if options.rietveld_url:
 | 
						|
    # Try to extract the review number if possible and fix the protocol.
 | 
						|
    if not '://' in options.rietveld_url:
 | 
						|
      options.rietveld_url = 'http://' + options.rietveld_url
 | 
						|
    match = re.match(r'^(.*)/(\d+)/?$', options.rietveld_url)
 | 
						|
    if match:
 | 
						|
      if options.issue or options.patchset:
 | 
						|
        parser.error('Cannot use both --issue and use a review number url')
 | 
						|
      options.issue = int(match.group(2))
 | 
						|
      options.rietveld_url = match.group(1)
 | 
						|
 | 
						|
  try:
 | 
						|
    changed_files = None
 | 
						|
    # Always include os.getcwd() in the checkout settings.
 | 
						|
    path = os.getcwd()
 | 
						|
 | 
						|
    file_list = []
 | 
						|
    if options.files:
 | 
						|
      file_list = options.files
 | 
						|
    elif change:
 | 
						|
      file_list = [f.LocalPath() for f in change.AffectedFiles()]
 | 
						|
 | 
						|
    if options.upstream_branch:
 | 
						|
      path += '@' + options.upstream_branch
 | 
						|
      # Clear file list so that the correct list will be retrieved from the
 | 
						|
      # upstream branch.
 | 
						|
      file_list = []
 | 
						|
 | 
						|
    current_vcs = GuessVCS(options, path, file_list)
 | 
						|
    current_vcs.AutomagicalSettings()
 | 
						|
    options = current_vcs.options
 | 
						|
    vcs_is_git = type(current_vcs) is GIT
 | 
						|
 | 
						|
    # So far, git_repo doesn't work with SVN
 | 
						|
    if options.git_repo and not vcs_is_git:
 | 
						|
      parser.error('--git_repo option is supported only for GIT repositories')
 | 
						|
 | 
						|
    # If revision==auto, resolve it
 | 
						|
    if options.revision and options.revision.lower() == 'auto':
 | 
						|
      if not vcs_is_git:
 | 
						|
        parser.error('--revision=auto is supported only for GIT repositories')
 | 
						|
      options.revision = scm.GIT.Capture(
 | 
						|
          ['rev-parse', current_vcs.diff_against],
 | 
						|
          cwd=path)
 | 
						|
 | 
						|
    checkouts = [current_vcs]
 | 
						|
    for item in options.sub_rep:
 | 
						|
      # Pass file_list=None because we don't know the sub repo's file list.
 | 
						|
      checkout = GuessVCS(options,
 | 
						|
                          os.path.join(current_vcs.checkout_root, item),
 | 
						|
                          None)
 | 
						|
      if checkout.checkout_root in [c.checkout_root for c in checkouts]:
 | 
						|
        parser.error('Specified the root %s two times.' %
 | 
						|
                     checkout.checkout_root)
 | 
						|
      checkouts.append(checkout)
 | 
						|
 | 
						|
    can_http = options.port and options.host
 | 
						|
    can_svn = options.svn_repo
 | 
						|
    can_git = options.git_repo
 | 
						|
    can_gerrit = options.gerrit_url
 | 
						|
    can_something = can_http or can_svn or can_git or can_gerrit
 | 
						|
    # If there was no transport selected yet, now we must have enough data to
 | 
						|
    # select one.
 | 
						|
    if not options.send_patch and not can_something:
 | 
						|
      parser.error('Please specify an access method.')
 | 
						|
 | 
						|
    # Convert options.diff into the content of the diff.
 | 
						|
    if options.url:
 | 
						|
      if options.files:
 | 
						|
        parser.error('You cannot specify files and --url at the same time.')
 | 
						|
      options.diff = urllib2.urlopen(options.url).read()
 | 
						|
    elif options.diff:
 | 
						|
      if options.files:
 | 
						|
        parser.error('You cannot specify files and --diff at the same time.')
 | 
						|
      options.diff = gclient_utils.FileRead(options.diff, 'rb')
 | 
						|
    elif options.issue and options.patchset is None:
 | 
						|
      # Retrieve the patch from rietveld when the diff is not specified.
 | 
						|
      # When patchset is specified, it's because it's done by gcl/git-try.
 | 
						|
      api_url = '%s/api/%d' % (options.rietveld_url, options.issue)
 | 
						|
      logging.debug(api_url)
 | 
						|
      contents = json.loads(urllib2.urlopen(api_url).read())
 | 
						|
      options.patchset = contents['patchsets'][-1]
 | 
						|
      diff_url = ('%s/download/issue%d_%d.diff' %
 | 
						|
          (options.rietveld_url,  options.issue, options.patchset))
 | 
						|
      diff = GetMungedDiff('', urllib2.urlopen(diff_url).readlines())
 | 
						|
      options.diff = ''.join(diff[0])
 | 
						|
      changed_files = diff[1]
 | 
						|
    else:
 | 
						|
      # Use this as the base.
 | 
						|
      root = checkouts[0].checkout_root
 | 
						|
      diffs = []
 | 
						|
      for checkout in checkouts:
 | 
						|
        raw_diff = checkout.GenerateDiff()
 | 
						|
        if not raw_diff:
 | 
						|
          continue
 | 
						|
        diff = raw_diff.splitlines(True)
 | 
						|
        path_diff = gclient_utils.PathDifference(root, checkout.checkout_root)
 | 
						|
        # Munge it.
 | 
						|
        diffs.extend(GetMungedDiff(path_diff, diff)[0])
 | 
						|
      if not diffs:
 | 
						|
        logging.error('Empty or non-existant diff, exiting.')
 | 
						|
        return 1
 | 
						|
      options.diff = ''.join(diffs)
 | 
						|
 | 
						|
    if not options.name:
 | 
						|
      if options.issue:
 | 
						|
        options.name = 'Issue %s' % options.issue
 | 
						|
      else:
 | 
						|
        options.name = 'Unnamed'
 | 
						|
        print('Note: use --name NAME to change the try job name.')
 | 
						|
 | 
						|
    if not options.email:
 | 
						|
      parser.error('Using an anonymous checkout. Please use --email or set '
 | 
						|
                   'the TRYBOT_RESULTS_EMAIL_ADDRESS environment variable.')
 | 
						|
    print('Results will be emailed to: ' + options.email)
 | 
						|
 | 
						|
    if options.bot:
 | 
						|
      bot_spec = _ApplyTestFilter(
 | 
						|
          options.testfilter, _ParseBotList(options.bot, options.testfilter))
 | 
						|
    else:
 | 
						|
      bot_spec = _GenTSBotSpec(checkouts, change, changed_files, options)
 | 
						|
 | 
						|
    if options.testfilter:
 | 
						|
      bot_spec = _ApplyTestFilter(options.testfilter, bot_spec)
 | 
						|
 | 
						|
    if any('triggered' in b[0] for b in bot_spec):
 | 
						|
      print >> sys.stderr, (
 | 
						|
          'ERROR You are trying to send a job to a triggered bot.  This type of'
 | 
						|
          ' bot requires an\ninitial job from a parent (usually a builder).  '
 | 
						|
          'Instead send your job to the parent.\nBot list: %s' % bot_spec)
 | 
						|
      return 1
 | 
						|
 | 
						|
    if options.print_bots:
 | 
						|
      print 'Bots which would be used:'
 | 
						|
      for bot in bot_spec:
 | 
						|
        if bot[1]:
 | 
						|
          print '  %s:%s' % (bot[0], ','.join(bot[1]))
 | 
						|
        else:
 | 
						|
          print '  %s' % (bot[0])
 | 
						|
      return 0
 | 
						|
 | 
						|
    # Determine sending protocol
 | 
						|
    if options.send_patch:
 | 
						|
      # If forced.
 | 
						|
      senders = [options.send_patch]
 | 
						|
    else:
 | 
						|
      # Try sending patch using avaialble protocols
 | 
						|
      all_senders = [
 | 
						|
        (_SendChangeHTTP, can_http),
 | 
						|
        (_SendChangeSVN, can_svn),
 | 
						|
        (_SendChangeGerrit, can_gerrit),
 | 
						|
        (_SendChangeGit, can_git),
 | 
						|
      ]
 | 
						|
      senders = [sender for sender, can in all_senders if can]
 | 
						|
 | 
						|
    # Send the patch.
 | 
						|
    for sender in senders:
 | 
						|
      try:
 | 
						|
        sender(bot_spec, options)
 | 
						|
        return 0
 | 
						|
      except NoTryServerAccess:
 | 
						|
        is_last = sender == senders[-1]
 | 
						|
        if is_last:
 | 
						|
          raise
 | 
						|
    assert False, "Unreachable code"
 | 
						|
  except Error, e:
 | 
						|
    if swallow_exception:
 | 
						|
      return 1
 | 
						|
    print >> sys.stderr, e
 | 
						|
    return 1
 | 
						|
  except (gclient_utils.Error, subprocess2.CalledProcessError), e:
 | 
						|
    print >> sys.stderr, e
 | 
						|
    return 1
 | 
						|
  return 0
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
  fix_encoding.fix_encoding()
 | 
						|
  sys.exit(TryChange(None, None, False))
 |