# 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\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()