1 # Copyright (C) 2013 Google Inc. All rights reserved.
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
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
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.
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
37 _log = logging.getLogger(__name__)
40 class CommitAnnouncer(SingleServerIRCBot):
41 _commit_detail_format = "%H\n%cn\n%s\n%b" # commit-sha1, author, subject, body
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)
52 if not self._update():
54 self.last_commit = self.git.latest_git_commit()
55 SingleServerIRCBot.start(self)
57 def post_new_commits(self):
58 if not self.connection.is_connected():
60 if not self._update(force_clean=True):
61 self.stop("Failed to update repository!")
63 new_commits = self.git.git_commits_since(self.last_commit)
65 self.last_commit = new_commits[-1]
66 for commit in new_commits:
67 commit_detail = self._commit_detail(commit)
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)
73 _log.error('Malformed commit log for %s' % commit)
78 self._post('Commands available: %s' % ' '.join(self.commands.keys()))
80 def stop(self, message=""):
81 self.connection.execute_delayed(0, lambda: self.die(message))
85 def on_nicknameinuse(self, connection, event):
86 connection.nick('%s_' % connection.get_nickname())
88 def on_welcome(self, connection, event):
89 connection.join(channel)
91 def on_pubmsg(self, connection, event):
92 message = event.arguments()[0]
93 command = self._message_command(message)
97 def _update(self, force_clean=False):
98 if not self.git.is_cleanly_tracking_remote_master():
100 confirm = raw_input('This repository has local changes, continue? (uncommitted changes will be lost) y/n: ')
101 if not confirm.lower() == 'y':
104 self.git.ensure_cleanly_tracking_remote_master()
105 except ScriptError, e:
106 _log.error('Failed to clean repository: %s' % e)
110 while attempts <= retry_attempts:
112 # User may have sent a keyboard interrupt during the wait.
113 if not self.connection.is_connected():
115 wait = int(update_wait_seconds) << (attempts - 1)
117 _log.info('Waiting %s seconds' % wait)
119 _log.info('Waiting %s minutes' % (wait / 60))
121 _log.info('Pull attempt %s out of %s' % (attempts, retry_attempts))
125 except ScriptError, e:
126 _log.error('Error pulling from server: %s' % e)
127 _log.error('Output: %s' % e.output)
129 _log.error('Exceeded pull attempts')
130 _log.error('Aborting at time: %s' % self._time())
134 return time.strftime('[%x %X %Z]', time.localtime())
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]
144 def _commit_detail(self, commit):
145 return self._format_commit_detail(self.git.git_commit_detail(commit, self._commit_detail_format))
147 def _format_commit_detail(self, commit_detail):
148 if commit_detail.count('\n') < self._commit_detail_format.count('\n'):
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=']
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()
167 if not revision.isdigit():
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())
175 match = re.search(r'(?P<review_id>\d+)', review_url)
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]
180 red_flag_message = ' \x037%s\x03' % (' '.join(red_flags)) if red_flags else ''
182 return '%s committed "%s" %s %s%s' % (email, subject, first_url, svn_revision, red_flag_message)
184 def _post(self, message):
185 self.connection.execute_delayed(0, lambda: self.connection.privmsg(channel, self._sanitize_string(message)))
187 def _sanitize_string(self, message):
188 return message.encode('ascii', 'backslashreplace')
191 class CommitAnnouncerThread(threading.Thread):
192 def __init__(self, tool, irc_password):
193 threading.Thread.__init__(self)
194 self.bot = CommitAnnouncer(tool, irc_password)