3376429af9777f86df1bebb4850548ffa9c2bba8
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Tools / Scripts / webkitpy / tool / bot / commitannouncer.py
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, 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
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()