| Index: commit-queue/commit_queue.py
|
| ===================================================================
|
| --- commit-queue/commit_queue.py (revision 249146)
|
| +++ commit-queue/commit_queue.py (working copy)
|
| @@ -1,397 +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.
|
| -"""Commit queue executable.
|
| -
|
| -Reuse Rietveld and the Chromium Try Server to process and automatically commit
|
| -patches.
|
| -"""
|
| -
|
| -import logging
|
| -import logging.handlers
|
| -import optparse
|
| -import os
|
| -import shutil
|
| -import signal
|
| -import socket
|
| -import sys
|
| -import tempfile
|
| -import time
|
| -
|
| -import find_depot_tools # pylint: disable=W0611
|
| -import checkout
|
| -import fix_encoding
|
| -import rietveld
|
| -import subprocess2
|
| -
|
| -import async_push
|
| -import cq_alerts
|
| -import creds
|
| -import errors
|
| -import projects
|
| -import sig_handler
|
| -
|
| -
|
| -ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| -
|
| -
|
| -class OnlyIssueRietveld(rietveld.Rietveld):
|
| - """Returns a single issue for end-to-end in prod testing."""
|
| - def __init__(self, url, email, password, extra_headers, only_issue):
|
| - super(OnlyIssueRietveld, self).__init__(url, email, password, extra_headers)
|
| - self._only_issue = only_issue
|
| -
|
| - def get_pending_issues(self):
|
| - """If it's set to return a single issue, only return this one."""
|
| - if self._only_issue:
|
| - return [self._only_issue]
|
| - return []
|
| -
|
| - def get_issue_properties(self, issue, messages):
|
| - """Hacks the result to fake that the issue has the commit bit set."""
|
| - data = super(OnlyIssueRietveld, self).get_issue_properties(issue, messages)
|
| - if issue == self._only_issue:
|
| - data['commit'] = True
|
| - return data
|
| -
|
| - def set_flag(self, issue, patchset, flag, value):
|
| - if issue == self._only_issue and flag == 'commit' and value == 'False':
|
| - self._only_issue = None
|
| - return super(OnlyIssueRietveld, self).set_flag(issue, patchset, flag, value)
|
| -
|
| -
|
| -class FakeCheckout(object):
|
| - def __init__(self):
|
| - self.project_path = os.getcwd()
|
| - self.project_name = os.path.basename(self.project_path)
|
| -
|
| - @staticmethod
|
| - def prepare(_revision):
|
| - logging.info('FakeCheckout is syncing')
|
| - return unicode('FAKE')
|
| -
|
| - @staticmethod
|
| - def apply_patch(*_args):
|
| - logging.info('FakeCheckout is applying a patch')
|
| -
|
| - @staticmethod
|
| - def commit(*_args):
|
| - logging.info('FakeCheckout is committing patch')
|
| - return 'FAKED'
|
| -
|
| - @staticmethod
|
| - def get_settings(_key):
|
| - return None
|
| -
|
| - @staticmethod
|
| - def revisions(*_args):
|
| - return None
|
| -
|
| -
|
| -def AlertOnUncleanCheckout():
|
| - """Sends an alert if the cq is running live with local edits."""
|
| - diff = subprocess2.capture(['gclient', 'diff'], cwd=ROOT_DIR).strip()
|
| - if diff:
|
| - cq_alerts.SendAlert(
|
| - 'CQ running with local diff.',
|
| - ('Ruh-roh! Commit queue was started with an unclean checkout.\n\n'
|
| - '$ gclient diff\n%s' % diff))
|
| -
|
| -
|
| -def SetupLogging(options):
|
| - """Configures the logging module."""
|
| - logging.getLogger().setLevel(logging.DEBUG)
|
| - if options.verbose:
|
| - level = logging.DEBUG
|
| - else:
|
| - level = logging.INFO
|
| - console_logging = logging.StreamHandler()
|
| - console_logging.setFormatter(logging.Formatter(
|
| - '%(asctime)s %(levelname)7s %(message)s'))
|
| - console_logging.setLevel(level)
|
| - logging.getLogger().addHandler(console_logging)
|
| -
|
| - log_directory = 'logs-' + options.project
|
| - if not os.path.exists(log_directory):
|
| - os.mkdir(log_directory)
|
| -
|
| - logging_rotating_file = logging.handlers.RotatingFileHandler(
|
| - filename=os.path.join(log_directory, 'commit_queue.log'),
|
| - maxBytes= 10*1024*1024,
|
| - backupCount=50)
|
| - logging_rotating_file.setLevel(logging.DEBUG)
|
| - logging_rotating_file.setFormatter(logging.Formatter(
|
| - '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)4d): %(message)s'))
|
| - logging.getLogger().addHandler(logging_rotating_file)
|
| -
|
| -
|
| -class SignalInterrupt(Exception):
|
| - """Exception that indicates being interrupted by a caught signal."""
|
| -
|
| - def __init__(self, signal_set=None, *args, **kwargs):
|
| - super(SignalInterrupt, self).__init__(*args, **kwargs)
|
| - self.signal_set = signal_set
|
| -
|
| -
|
| -def SaveDatabaseCopyForDebugging(db_path):
|
| - """Saves database file for debugging. Returns name of the saved file."""
|
| - with tempfile.NamedTemporaryFile(
|
| - dir=os.path.dirname(db_path),
|
| - prefix='db.debug.',
|
| - suffix='.json',
|
| - delete=False) as tmp_file:
|
| - with open(db_path) as db_file:
|
| - shutil.copyfileobj(db_file, tmp_file)
|
| - return tmp_file.name
|
| -
|
| -
|
| -def main():
|
| - # Set a default timeout for sockets. This is critical when talking to remote
|
| - # services like AppEngine and buildbot.
|
| - # TODO(phajdan.jr): This used to be 70s. Investigate lowering it again.
|
| - socket.setdefaulttimeout(60.0 * 15)
|
| -
|
| - parser = optparse.OptionParser(
|
| - description=sys.modules['__main__'].__doc__)
|
| - project_choices = projects.supported_projects()
|
| - parser.add_option('-v', '--verbose', action='store_true')
|
| - parser.add_option(
|
| - '--no-dry-run',
|
| - action='store_false',
|
| - dest='dry_run',
|
| - default=True,
|
| - help='Run for real instead of dry-run mode which is the default. '
|
| - 'WARNING: while the CQ won\'t touch rietveld in dry-run mode, the '
|
| - 'Try Server will. So it is recommended to use --only-issue')
|
| - parser.add_option(
|
| - '--only-issue',
|
| - type='int',
|
| - help='Limits to a single issue. Useful for live testing; WARNING: it '
|
| - 'will fake that the issue has the CQ bit set, so only try with an '
|
| - 'issue you don\'t mind about.')
|
| - parser.add_option(
|
| - '--fake',
|
| - action='store_true',
|
| - help='Run with a fake checkout to speed up testing')
|
| - parser.add_option(
|
| - '--no-try',
|
| - action='store_true',
|
| - help='Don\'t send try jobs.')
|
| - parser.add_option(
|
| - '-p',
|
| - '--poll-interval',
|
| - type='int',
|
| - default=10,
|
| - help='Minimum delay between each polling loop, default: %default')
|
| - parser.add_option(
|
| - '--query-only',
|
| - action='store_true',
|
| - help='Return internal state')
|
| - parser.add_option(
|
| - '--project',
|
| - choices=project_choices,
|
| - help='Project to run the commit queue against: %s' %
|
| - ', '.join(project_choices))
|
| - parser.add_option(
|
| - '-u',
|
| - '--user',
|
| - default='commit-bot@chromium.org',
|
| - help='User to use instead of %default')
|
| - parser.add_option(
|
| - '--rietveld',
|
| - default='https://codereview.chromium.org',
|
| - help='Rietveld server to use instead of %default')
|
| - options, args = parser.parse_args()
|
| - if args:
|
| - parser.error('Unsupported args: %s' % args)
|
| - if not options.project:
|
| - parser.error('Need to pass a valid project to --project.\nOptions are: %s' %
|
| - ', '.join(project_choices))
|
| -
|
| - SetupLogging(options)
|
| - try:
|
| - work_dir = os.path.join(ROOT_DIR, 'workdir')
|
| - # Use our specific subversion config.
|
| - checkout.SvnMixIn.svn_config = checkout.SvnConfig(
|
| - os.path.join(ROOT_DIR, 'subversion_config'))
|
| -
|
| - url = options.rietveld
|
| - gaia_creds = creds.Credentials(os.path.join(work_dir, '.gaia_pwd'))
|
| - if options.dry_run:
|
| - logging.debug('Dry run - skipping SCM check.')
|
| - if options.only_issue:
|
| - parser.error('--only-issue is not supported with dry run')
|
| - else:
|
| - print('Using read-only Rietveld')
|
| - # Make sure rietveld is not modified. Pass empty email and
|
| - # password to bypass authentication; this additionally
|
| - # guarantees rietveld will not allow any changes.
|
| - rietveld_obj = rietveld.ReadOnlyRietveld(url, email='', password='')
|
| - else:
|
| - AlertOnUncleanCheckout()
|
| - print('WARNING: The Commit Queue is going to commit stuff')
|
| - if options.only_issue:
|
| - print('Using only issue %d' % options.only_issue)
|
| - rietveld_obj = OnlyIssueRietveld(
|
| - url,
|
| - options.user,
|
| - gaia_creds.get(options.user),
|
| - None,
|
| - options.only_issue)
|
| - else:
|
| - rietveld_obj = rietveld.Rietveld(
|
| - url,
|
| - options.user,
|
| - gaia_creds.get(options.user),
|
| - None)
|
| -
|
| - pc = projects.load_project(
|
| - options.project,
|
| - options.user,
|
| - work_dir,
|
| - rietveld_obj,
|
| - options.no_try)
|
| -
|
| - if options.dry_run:
|
| - if options.fake:
|
| - # Disable the checkout.
|
| - print 'Using no checkout'
|
| - pc.context.checkout = FakeCheckout()
|
| - else:
|
| - print 'Using read-only checkout'
|
| - pc.context.checkout = checkout.ReadOnlyCheckout(pc.context.checkout)
|
| - # Save pushed events on disk.
|
| - print 'Using read-only chromium-status interface'
|
| - pc.context.status = async_push.AsyncPushStore()
|
| -
|
| - landmine_path = os.path.join(work_dir,
|
| - pc.context.checkout.project_name + '.landmine')
|
| - db_path = os.path.join(work_dir, pc.context.checkout.project_name + '.json')
|
| - if os.path.isfile(db_path):
|
| - if os.path.isfile(landmine_path):
|
| - debugging_path = SaveDatabaseCopyForDebugging(db_path)
|
| - os.remove(db_path)
|
| - logging.warning(('Deleting database because previous shutdown '
|
| - 'was unclean. The copy of the database is saved '
|
| - 'as %s.') % debugging_path)
|
| - else:
|
| - try:
|
| - pc.load(db_path)
|
| - except ValueError as e:
|
| - debugging_path = SaveDatabaseCopyForDebugging(db_path)
|
| - os.remove(db_path)
|
| - logging.warning(('Failed to parse database (%r), deleting it. '
|
| - 'The copy of the database is saved as %s.') %
|
| - (e, debugging_path))
|
| - raise e
|
| -
|
| - # Create a file to indicate unclean shutdown.
|
| - with open(landmine_path, 'w'):
|
| - pass
|
| -
|
| - sig_handler.installHandlers(
|
| - signal.SIGINT,
|
| - signal.SIGHUP
|
| - )
|
| -
|
| - # Sync every 5 minutes.
|
| - SYNC_DELAY = 5*60
|
| - try:
|
| - if options.query_only:
|
| - pc.look_for_new_pending_commit()
|
| - pc.update_status()
|
| - print(str(pc.queue))
|
| - os.remove(landmine_path)
|
| - return 0
|
| -
|
| - now = time.time()
|
| - next_loop = now + options.poll_interval
|
| - # First sync is on second loop.
|
| - next_sync = now + options.poll_interval * 2
|
| - while True:
|
| - # In theory, we would gain in performance to parallelize these tasks. In
|
| - # practice I'm not sure it matters.
|
| - pc.look_for_new_pending_commit()
|
| - pc.process_new_pending_commit()
|
| - pc.update_status()
|
| - pc.scan_results()
|
| - if sig_handler.getTriggeredSignals():
|
| - raise SignalInterrupt(signal_set=sig_handler.getTriggeredSignals())
|
| - # Save the db at each loop. The db can easily be in the 1mb range so
|
| - # it's slowing down the CQ a tad but it in the 100ms range even for that
|
| - # size.
|
| - pc.save(db_path)
|
| -
|
| - # More than a second to wait and due to sync.
|
| - now = time.time()
|
| - if (next_loop - now) >= 1 and (next_sync - now) <= 0:
|
| - if sys.stdout.isatty():
|
| - sys.stdout.write('Syncing while waiting \r')
|
| - sys.stdout.flush()
|
| - try:
|
| - pc.context.checkout.prepare(None)
|
| - except subprocess2.CalledProcessError as e:
|
| - # Don't crash, most of the time it's the svn server that is dead.
|
| - # How fun. Send a stack trace to annoy the maintainer.
|
| - errors.send_stack(e)
|
| - next_sync = time.time() + SYNC_DELAY
|
| -
|
| - now = time.time()
|
| - next_loop = max(now, next_loop)
|
| - while True:
|
| - # Abort if any signals are set
|
| - if sig_handler.getTriggeredSignals():
|
| - raise SignalInterrupt(signal_set=sig_handler.getTriggeredSignals())
|
| - delay = next_loop - now
|
| - if delay <= 0:
|
| - break
|
| - if sys.stdout.isatty():
|
| - sys.stdout.write('Sleeping for %1.1f seconds \r' % delay)
|
| - sys.stdout.flush()
|
| - time.sleep(min(delay, 0.1))
|
| - now = time.time()
|
| - if sys.stdout.isatty():
|
| - sys.stdout.write('Running (please do not interrupt) \r')
|
| - sys.stdout.flush()
|
| - next_loop = time.time() + options.poll_interval
|
| - except: # Catch all fatal exit conditions.
|
| - logging.exception('CQ loop terminating')
|
| - raise
|
| - finally:
|
| - logging.warning('Saving db...')
|
| - pc.save(db_path)
|
| - pc.close()
|
| - logging.warning('db save successful.')
|
| - except SignalInterrupt:
|
| - # This is considered a clean shutdown: we only throw this exception
|
| - # from selected places in the code where the database should be
|
| - # in a known and consistent state.
|
| - os.remove(landmine_path)
|
| -
|
| - print 'Bye bye (SignalInterrupt)'
|
| - # 23 is an arbitrary value to signal loop.sh that it must stop looping.
|
| - return 23
|
| - except KeyboardInterrupt:
|
| - # This is actually an unclean shutdown. Do not remove the landmine file.
|
| - # One example of this is user hitting ctrl-c twice at an arbitrary point
|
| - # inside the CQ loop. There are no guarantees about consistent state
|
| - # of the database then.
|
| -
|
| - print 'Bye bye (KeyboardInterrupt - this is considered unclean shutdown)'
|
| - # 23 is an arbitrary value to signal loop.sh that it must stop looping.
|
| - return 23
|
| - except errors.ConfigurationError as e:
|
| - parser.error(str(e))
|
| - return 1
|
| -
|
| - # CQ generally doesn't exit by itself, but if we ever get here, it looks
|
| - # like a clean shutdown so remove the landmine file.
|
| - # TODO(phajdan.jr): Do we ever get here?
|
| - os.remove(landmine_path)
|
| - return 0
|
| -
|
| -
|
| -if __name__ == '__main__':
|
| - fix_encoding.fix_encoding()
|
| - sys.exit(main())
|
|
|