| OLD | NEW |
| (Empty) |
| 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, b
ody | |
| 42 | |
| 43 def __init__(self, tool, irc_password): | |
| 44 SingleServerIRCBot.__init__(self, [(server, port, irc_password)], nickna
me, 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(comm
it_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_attempt
s)) | |
| 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, sel
f._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 el
se '' | |
| 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(chann
el, self._sanitize_string(message))) | |
| 186 | |
| 187 def _sanitize_string(self, message): | |
| 188 return message.encode('ascii', 'backslashreplace') | |
| 189 | |
| 190 | |
| 191 class 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() | |
| OLD | NEW |