blob: 3376429af9777f86df1bebb4850548ffa9c2bba8 [file] [log] [blame]
# Copyright (C) 2013 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
import re
import threading
import time
from webkitpy.common.checkout.scm.git import Git
from webkitpy.common.config.irc import server, port, channel, nickname
from webkitpy.common.config.irc import update_wait_seconds, retry_attempts
from webkitpy.common.system.executive import ScriptError
from webkitpy.thirdparty.irc.ircbot import SingleServerIRCBot
_log = logging.getLogger(__name__)
class CommitAnnouncer(SingleServerIRCBot):
_commit_detail_format = "%H\n%cn\n%s\n%b" # commit-sha1, author, subject, body
def __init__(self, tool, irc_password):
SingleServerIRCBot.__init__(self, [(server, port, irc_password)], nickname, nickname)
self.git = Git(cwd=tool.scm().checkout_root, filesystem=tool.filesystem, executive=tool.executive)
self.commands = {
'help': self.help,
'quit': self.stop,
}
def start(self):
if not self._update():
return
self.last_commit = self.git.latest_git_commit()
SingleServerIRCBot.start(self)
def post_new_commits(self):
if not self.connection.is_connected():
return
if not self._update(force_clean=True):
self.stop("Failed to update repository!")
return
new_commits = self.git.git_commits_since(self.last_commit)
if new_commits:
self.last_commit = new_commits[-1]
for commit in new_commits:
commit_detail = self._commit_detail(commit)
if commit_detail:
_log.info('%s Posting commit %s' % (self._time(), commit))
_log.info('%s Posted message: %s' % (self._time(), repr(commit_detail)))
self._post(commit_detail)
else:
_log.error('Malformed commit log for %s' % commit)
# Bot commands.
def help(self):
self._post('Commands available: %s' % ' '.join(self.commands.keys()))
def stop(self, message=""):
self.connection.execute_delayed(0, lambda: self.die(message))
# IRC event handlers.
def on_nicknameinuse(self, connection, event):
connection.nick('%s_' % connection.get_nickname())
def on_welcome(self, connection, event):
connection.join(channel)
def on_pubmsg(self, connection, event):
message = event.arguments()[0]
command = self._message_command(message)
if command:
command()
def _update(self, force_clean=False):
if not self.git.is_cleanly_tracking_remote_master():
if not force_clean:
confirm = raw_input('This repository has local changes, continue? (uncommitted changes will be lost) y/n: ')
if not confirm.lower() == 'y':
return False
try:
self.git.ensure_cleanly_tracking_remote_master()
except ScriptError, e:
_log.error('Failed to clean repository: %s' % e)
return False
attempts = 1
while attempts <= retry_attempts:
if attempts > 1:
# User may have sent a keyboard interrupt during the wait.
if not self.connection.is_connected():
return False
wait = int(update_wait_seconds) << (attempts - 1)
if wait < 120:
_log.info('Waiting %s seconds' % wait)
else:
_log.info('Waiting %s minutes' % (wait / 60))
time.sleep(wait)
_log.info('Pull attempt %s out of %s' % (attempts, retry_attempts))
try:
self.git.pull()
return True
except ScriptError, e:
_log.error('Error pulling from server: %s' % e)
_log.error('Output: %s' % e.output)
attempts += 1
_log.error('Exceeded pull attempts')
_log.error('Aborting at time: %s' % self._time())
return False
def _time(self):
return time.strftime('[%x %X %Z]', time.localtime())
def _message_command(self, message):
prefix = '%s:' % self.connection.get_nickname()
if message.startswith(prefix):
command_name = message[len(prefix):].strip()
if command_name in self.commands:
return self.commands[command_name]
return None
def _commit_detail(self, commit):
return self._format_commit_detail(self.git.git_commit_detail(commit, self._commit_detail_format))
def _format_commit_detail(self, commit_detail):
if commit_detail.count('\n') < self._commit_detail_format.count('\n'):
return ''
commit, email, subject, body = commit_detail.split('\n', 3)
review_string = 'Review URL: '
svn_string = 'git-svn-id: svn://svn.chromium.org/blink/trunk@'
red_flag_strings = ['NOTRY=true', 'TBR=']
review_url = ''
svn_revision = ''
red_flags = []
for line in body.split('\n'):
if line.startswith(review_string):
review_url = line[len(review_string):]
if line.startswith(svn_string):
tokens = line[len(svn_string):].split()
if not tokens:
continue
revision = tokens[0]
if not revision.isdigit():
continue
svn_revision = 'r%s' % revision
for red_flag_string in red_flag_strings:
if line.lower().startswith(red_flag_string.lower()):
red_flags.append(line.strip())
if review_url:
match = re.search(r'(?P<review_id>\d+)', review_url)
if match:
review_url = 'http://crrev.com/%s' % match.group('review_id')
first_url = review_url if review_url else 'https://chromium.googlesource.com/chromium/blink/+/%s' % commit[:8]
red_flag_message = '\x037%s\x03' % (' '.join(red_flags)) if red_flags else ''
return ('%s %s %s committed "%s" %s' % (svn_revision, first_url, email, subject, red_flag_message)).strip()
def _post(self, message):
self.connection.execute_delayed(0, lambda: self.connection.privmsg(channel, self._sanitize_string(message)))
def _sanitize_string(self, message):
return message.encode('ascii', 'backslashreplace')
class CommitAnnouncerThread(threading.Thread):
def __init__(self, tool, irc_password):
threading.Thread.__init__(self)
self.bot = CommitAnnouncer(tool, irc_password)
def run(self):
self.bot.start()
def stop(self):
self.bot.stop()
self.join()