Home | History | Annotate | Download | only in bot
      1 # Copyright (C) 2013 Google Inc. All rights reserved.
      2 #
      3 # Redistribution and use in source and binary forms, with or without
      4 # modification, are permitted provided that the following conditions are
      5 # met:
      6 #
      7 #    * Redistributions of source code must retain the above copyright
      8 # notice, this list of conditions and the following disclaimer.
      9 #    * Redistributions in binary form must reproduce the above
     10 # copyright notice, this list of conditions and the following disclaimer
     11 # in the documentation and/or other materials provided with the
     12 # distribution.
     13 #
     14 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     15 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     16 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     17 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     18 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     19 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     20 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     21 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     22 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     23 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     24 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     25 
     26 import logging
     27 import re
     28 import threading
     29 import time
     30 
     31 from webkitpy.common.checkout.scm.git import Git
     32 from webkitpy.common.config.irc import server, port, channel, nickname
     33 from webkitpy.common.config.irc import update_wait_seconds, retry_attempts
     34 from webkitpy.common.system.executive import ScriptError
     35 from webkitpy.thirdparty.irc.ircbot import SingleServerIRCBot
     36 
     37 _log = logging.getLogger(__name__)
     38 
     39 
     40 class CommitAnnouncer(SingleServerIRCBot):
     41     _commit_detail_format = "%H\n%cn\n%s\n%b"  # commit-sha1, author, subject, body
     42 
     43     def __init__(self, tool, irc_password):
     44         SingleServerIRCBot.__init__(self, [(server, port, irc_password)], nickname, nickname)
     45         self.git = Git(cwd=tool.scm().checkout_root, filesystem=tool.filesystem, executive=tool.executive)
     46         self.commands = {
     47             'help': self.help,
     48             'quit': self.stop,
     49         }
     50 
     51     def start(self):
     52         if not self._update():
     53             return
     54         self.last_commit = self.git.latest_git_commit()
     55         SingleServerIRCBot.start(self)
     56 
     57     def post_new_commits(self):
     58         if not self.connection.is_connected():
     59             return
     60         if not self._update(force_clean=True):
     61             self.stop("Failed to update repository!")
     62             return
     63         new_commits = self.git.git_commits_since(self.last_commit)
     64         if new_commits:
     65             self.last_commit = new_commits[-1]
     66             for commit in new_commits:
     67                 commit_detail = self._commit_detail(commit)
     68                 if commit_detail:
     69                     _log.info('%s Posting commit %s' % (self._time(), commit))
     70                     _log.info('%s Posted message: %s' % (self._time(), repr(commit_detail)))
     71                     self._post(commit_detail)
     72                 else:
     73                     _log.error('Malformed commit log for %s' % commit)
     74 
     75     # Bot commands.
     76 
     77     def help(self):
     78         self._post('Commands available: %s' % ' '.join(self.commands.keys()))
     79 
     80     def stop(self, message=""):
     81         self.connection.execute_delayed(0, lambda: self.die(message))
     82 
     83     # IRC event handlers.
     84 
     85     def on_nicknameinuse(self, connection, event):
     86         connection.nick('%s_' % connection.get_nickname())
     87 
     88     def on_welcome(self, connection, event):
     89         connection.join(channel)
     90 
     91     def on_pubmsg(self, connection, event):
     92         message = event.arguments()[0]
     93         command = self._message_command(message)
     94         if command:
     95             command()
     96 
     97     def _update(self, force_clean=False):
     98         if not self.git.is_cleanly_tracking_remote_master():
     99             if not force_clean:
    100                 confirm = raw_input('This repository has local changes, continue? (uncommitted changes will be lost) y/n: ')
    101                 if not confirm.lower() == 'y':
    102                     return False
    103             try:
    104                 self.git.ensure_cleanly_tracking_remote_master()
    105             except ScriptError, e:
    106                 _log.error('Failed to clean repository: %s' % e)
    107                 return False
    108 
    109         attempts = 1
    110         while attempts <= retry_attempts:
    111             if attempts > 1:
    112                 # User may have sent a keyboard interrupt during the wait.
    113                 if not self.connection.is_connected():
    114                     return False
    115                 wait = int(update_wait_seconds) << (attempts - 1)
    116                 if wait < 120:
    117                     _log.info('Waiting %s seconds' % wait)
    118                 else:
    119                     _log.info('Waiting %s minutes' % (wait / 60))
    120                 time.sleep(wait)
    121                 _log.info('Pull attempt %s out of %s' % (attempts, retry_attempts))
    122             try:
    123                 self.git.pull()
    124                 return True
    125             except ScriptError, e:
    126                 _log.error('Error pulling from server: %s' % e)
    127                 _log.error('Output: %s' % e.output)
    128             attempts += 1
    129         _log.error('Exceeded pull attempts')
    130         _log.error('Aborting at time: %s' % self._time())
    131         return False
    132 
    133     def _time(self):
    134         return time.strftime('[%x %X %Z]', time.localtime())
    135 
    136     def _message_command(self, message):
    137         prefix = '%s:' % self.connection.get_nickname()
    138         if message.startswith(prefix):
    139             command_name = message[len(prefix):].strip()
    140             if command_name in self.commands:
    141                 return self.commands[command_name]
    142         return None
    143 
    144     def _commit_detail(self, commit):
    145         return self._format_commit_detail(self.git.git_commit_detail(commit, self._commit_detail_format))
    146 
    147     def _format_commit_detail(self, commit_detail):
    148         if commit_detail.count('\n') < self._commit_detail_format.count('\n'):
    149             return ''
    150 
    151         commit, email, subject, body = commit_detail.split('\n', 3)
    152         review_string = 'Review URL: '
    153         svn_string = 'git-svn-id: svn://svn.chromium.org/blink/trunk@'
    154         red_flag_strings = ['NOTRY=true', 'TBR=']
    155         review_url = ''
    156         svn_url = ''
    157         red_flags = []
    158 
    159         for line in body.split('\n'):
    160             if line.startswith(review_string):
    161                 review_url = line[len(review_string):]
    162             if line.startswith(svn_string):
    163                 tokens = line[len(svn_string):].split()
    164                 if not tokens:
    165                     continue
    166                 revision = tokens[0]
    167                 if not revision.isdigit():
    168                     continue
    169                 svn_url = 'https://src.chromium.org/viewvc/blink?view=revision&revision=%s' % revision
    170             for red_flag_string in red_flag_strings:
    171                 if line.lower().startswith(red_flag_string.lower()):
    172                     red_flags.append(line.strip())
    173 
    174         if review_url:
    175             match = re.search(r'(?P<review_id>\d+)', review_url)
    176             if match:
    177                 review_url = 'http://crrev.com/%s' % match.group('review_id')
    178         first_url = review_url if review_url else 'https://chromium.googlesource.com/chromium/blink/+/%s' % commit[:8]
    179 
    180         red_flag_message = ' \x037%s\x03' % (' '.join(red_flags)) if red_flags else ''
    181 
    182         return '%s committed "%s" %s %s%s' % (email, subject, first_url, svn_url, red_flag_message)
    183 
    184     def _post(self, message):
    185         self.connection.execute_delayed(0, lambda: self.connection.privmsg(channel, message))
    186 
    187 
    188 class CommitAnnouncerThread(threading.Thread):
    189     def __init__(self, tool, irc_password):
    190         threading.Thread.__init__(self)
    191         self.bot = CommitAnnouncer(tool, irc_password)
    192 
    193     def run(self):
    194         self.bot.start()
    195 
    196     def stop(self):
    197         self.bot.stop()
    198         self.join()
    199