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 26import logging 27import re 28import threading 29import time 30 31from webkitpy.common.checkout.scm.git import Git 32from webkitpy.common.config.irc import server, port, channel, nickname 33from webkitpy.common.config.irc import update_wait_seconds, retry_attempts 34from webkitpy.common.system.executive import ScriptError 35from webkitpy.thirdparty.irc.ircbot import SingleServerIRCBot 36 37_log = logging.getLogger(__name__) 38 39 40class 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_revision = '' 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_revision = 'r%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 %s %s committed "%s" %s' % (svn_revision, first_url, email, subject, red_flag_message)).strip() 183 184 def _post(self, message): 185 self.connection.execute_delayed(0, lambda: self.connection.privmsg(channel, self._sanitize_string(message))) 186 187 def _sanitize_string(self, message): 188 return message.encode('ascii', 'backslashreplace') 189 190 191class CommitAnnouncerThread(threading.Thread): 192 def __init__(self, tool, irc_password): 193 threading.Thread.__init__(self) 194 self.bot = CommitAnnouncer(tool, irc_password) 195 196 def run(self): 197 self.bot.start() 198 199 def stop(self): 200 self.bot.stop() 201 self.join() 202