Index: trychange.py |
diff --git a/trychange.py b/trychange.py |
deleted file mode 100755 |
index db9bd440fa593158b0e3b86bae78974acda1e604..0000000000000000000000000000000000000000 |
--- a/trychange.py |
+++ /dev/null |
@@ -1,1265 +0,0 @@ |
-#!/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 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. |
- build_properties: a dict of build properties. |
- """ |
- |
- 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 = {} |
- build_properties = {} |
- if options.testfilter: |
- build_properties['testfilter'] = options.testfilter |
- builderNames = [builder for builder, _ in bot_spec] |
- if builderNames: |
- job_def['builderNames'] = builderNames |
- if build_properties: |
- job_def['build_properties'] = build_properties |
- |
- # 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.chromium.org", |
- 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)) |