Mon, 29 Sep 2014 03:26:45 +0300
- now is able to track zandronum mercurial repositories and react to 'fixes 0001234' trigger messages in them
cobalt.py | file | annotate | diff | comparison | revisions |
--- a/cobalt.py Sat Sep 06 14:03:38 2014 +0300 +++ b/cobalt.py Mon Sep 29 03:26:45 2014 +0300 @@ -103,7 +103,7 @@ if suds_active: try: sys.stdout.write ('Retrieving latest tracker ticket... ') - btannounce_id = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config ['trackerpassword'], 0) + btannounce_id = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config['trackerpassword'], 0) btannounce_active = True bt_updatechecktimeout() print btannounce_id @@ -123,7 +123,7 @@ bt_updatechecktimeout() newid = btannounce_id try: - newid = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config ['trackerpassword'], 0) + newid = suds_client.service.mc_issue_get_biggest_id (g_config['trackeruser'], g_config['trackerpassword'], 0) except Exception as e: pass @@ -206,6 +206,240 @@ python = sys.executable os.execl (python, python, * sys.argv) +' Retrieves hg incoming from zandronum repository ' +def get_incoming_data (zanrepo, rev, template): + try: + if rev != '': + return zanrepo.hg_command ('incoming', '../zanstablecopy', '--quiet', '-r', rev, '--template', template) + else: + return zanrepo.hg_command ('incoming', '../zanstablecopy', '--quiet', '--template', template) + #fi + except: + pass + return "" +#enddef + +' Check if a repository exists ' +def check_repo_exists (repo_name): + print 'Checking that %s exists...' % repo_name + repo_url = 'https://bitbucket.org/Torr_Samaho/' + repo_name + zanrepo = hgapi.Repo (repo_name) + + try: + zanrepo.hg_command ('id', '.') + except hgapi.hgapi.HgException: + # If the repo does not exist, clone it. After cloning, there obviously + # are no more updates, so we'll be done. + try: + print 'Cloning %s...' % repo_name + zanrepo.hg_clone (repo_url, repo_name) + print 'Cloning done. No update checking needed.' + except Exception as e: + print 'Unable to clone %s from %s: %s' % (repo_name, repo_url, str (`e`)) + quit(1) + #tried +#enddef + +check_repo_exists ('zandronum') +check_repo_exists ('zandronum-stable') + +repocheck_timeout = {'zandronum':(time.time()) + 15, 'zandronum-stable':(time.time() + 15)} + +' Retrieves and processes commits for zandronum repositories ' +' Ensure both repositories are OK before using this! ' +def process_zan_repo_updates (usestable): + global repocheck_timeout + global suds_client + global g_config + global g_clients + + repo_name = 'zandronum' if usestable == False else 'zandronum-stable' + repo_url = 'https://bitbucket.org/Torr_Samaho/' + repo_name + + if time.time() < repocheck_timeout[repo_name]: + return + + repocheck_timeout[repo_name] = time.time() + (cfg ('hg_checkinterval', 15) * 60) + zanrepo = hgapi.Repo (repo_name) + data = get_incoming_data (zanrepo, '', '{node|short} {desc}\n') + commits_to_pull = []; + + for line in data.split ('\n'): + if line == '': + continue + #fi + + rex = re.compile (r'^([^ ]+) (.+)$') + match = rex.match (line) + failed = False + + if not match: + chanlog ('malformed hg data: %s' % line) + continue + #fi + + try: + commit_node = match.group (1) + commit_message = match.group (2) + + rex = re.compile (r'^.*(fixes|resolves|addresses) ([0-9]+).*$') + match = rex.match (commit_message) + + if not match: + continue # no "fixes" message in the commit + #fi + + ticket_id = int (match.group (2)) + + # Acquire additional data + moredata = get_incoming_data (zanrepo, commit_node, + '{author|nonempty}\n{date(date, \'%A %d %B %Y %T\')}\n{diffstat|nonempty}').split('\n') + + if len (moredata) != 3: + chanlog ('commit %s: malformed hg data' % commit_node) + #fi + + commit_author = moredata[0] + commit_date = moredata[1] + commit_diffstat = moredata[2] + commit_email = "" + + ticket_data = suds_client.service.mc_issue_get (g_config['trackeruser'], g_config['trackerpassword'], ticket_id) + if not ticket_data: + chanlog ("error: commit %s: ticket %s not found" % (commit_node, ticket_id)) + continue + #fi + + # Remove the email address from the author if possible + rex = re.compile (r'^(.+) <([^>]+)>$.*') + match = rex.match (commit_author) + if match: + commit_author = match.group (1) + commit_email = match.group (2) + #fi + + rex = re.compile (r'([0-9]+): \+([0-9]+)/-([0-9]+)') + match = rex.match (commit_diffstat) + + if match: + modded = int (match.group (1)) + added = int (match.group (2)) + deleted = int (match.group (3)) + commit_diffstat = "%s file%s modified, %s line%s added, %s line%s removed" % \ + (modded if modded != 0 else 'no', + 's' if modded != 1 else '', + added if added != 0 else 'no', + 's' if added != 1 else '', + deleted if deleted != 0 else 'no', + 's' if deleted != 1 else '') + #fi + + files_added = filter (None, get_incoming_data (zanrepo, commit_node, '{file_adds}').split ('\n')) + files_removed = filter (None, get_incoming_data (zanrepo, commit_node, '{file_dels}').split ('\n')) + files_changed = filter (None, get_incoming_data (zanrepo, commit_node, '{file_mods}').split ('\n')) + + # Compare the email addresses against known developer usernames + commit_trackeruser = '' + + for developer, emails in g_config['developer_emails'].iteritems(): + for email in emails: + if commit_email == email: + commit_trackeruser = developer + break; + #fi + else: + continue + #done + break + #done + + if commit_trackeruser != '': + commit_author += ' [%s]' % commit_trackeruser + #fi + + message = 'Issue addressed by commit %s: [b][url="%s/commits/%s"]%s[/url][/b]' \ + % (commit_node, repo_url, commit_node, commit_message) + message += "\nCommitted by %s on %s\n\n%s" \ + % (commit_author, commit_date, commit_diffstat) + + if len (files_added) > 0: + message += "\nFiles added: %s" % ', '.join (files_added) + #fi + + if len (files_removed) > 0: + message += "\nFiles removed: %s" % ', '.join (files_removed) + #fi + + if len (files_changed) > 0: + message += "\nFiles changed: %s" % ', '.join (files_changed) + #fi + + need_update = False + + # If not already set, set handler + if not 'handler' in ticket_data: + ticket_data['handler'] = {'name': commit_trackeruser} + need_update = True + #fi + + # Find out the status level of the ticket + needs_testing_level = 70 + + if ticket_data['status']['id'] < needs_testing_level: + ticket_data.status['id'] = needs_testing_level + need_update = True + #fi + + # Announce on IRC + for irc_client in g_clients: + for channel in irc_client.cfg['channels']: + if 'btannounce' in channel and channel['btannounce'] == True: + irc_client.privmsg (channel['name'], + "%s: commit %s fixes issue %d: %s" + % (repo_name, commit_node, ticket_id, commit_message)) + irc_client.privmsg (channel['name'], + "Read all about it here: " + irc_client.get_ticket_url (ticket_id)) + #fi + #done + #done + + if need_update: + suds_client.service.mc_issue_update (g_config['trackeruser'], g_config['trackerpassword'], ticket_id, ticket_data) + #fi + + suds_client.service.mc_issue_note_add (g_config['trackeruser'], g_config['trackerpassword'], ticket_id, { 'text': message }) + except Exception as e: + chanlog ('Error: %s' % `e`) + failed = True + #tried + + if not failed: + commits_to_pull.append (commit_node) + #fi + #done + + if len (commits_to_pull) > 0: + pull_args = ['pull', '../zanstablecopy'] + + for commit in commits_to_pull: + pull_args.append ('-r'); + pull_args.append (commit); + #done + + try: + zanrepo.hg_command (*pull_args) + + # Also pull these commits to the zandronum main repository + if usestable: + devrepo = hgapi.Repo ('zandronum') + devrepo.hg_command (*pull_args) + #fi + except Exception as e: + chanlog ('Warning: unable to pull: %s' % `e`) + #tried + #fi +#enddef + # # Main IRC client class # @@ -330,6 +564,11 @@ # Check for new issues on the bugtracker bt_checklatest() + # Check for new commits in the repositories + # note: stable (i.e. True) goes first! + for n in [True, False]: + process_zan_repo_updates (n) + def channel_by_name (self, name): for channel in self.channels: if channel['name'].upper() == args[0].upper(): @@ -552,6 +791,71 @@ self.privmsg (replyto, '%s is now %s' % (key, channel[key])) save_config() + elif command == 'devemail': + check_admin (sender, ident, host, command) + + if len(args) < 2: + raise logical_exception ("usage: .%s <user> <email>" % command) + #fi + + if not 'developer_emails' in g_config: + g_config['developer_emails'] = {} + #fi + + user = ' '.join (args[0:-1]) + + if args[0] in g_config['developer_emails']: + g_config['developer_emails'][user].append (args[-1]) + else: + g_config['developer_emails'][user] = [args[-1]] + #fi + + self.privmsg (replyto, 'Developer emails for %s are now %s' % + (user, ', '.join (g_config['developer_emails'][user]))) + save_config() + elif command == 'deldevemail': + check_admin (sender, ident, host, command) + + if len(args) < 2: + raise logical_exception ("usage: .%s <user> <email>" % command) + #fi + + if not 'developer_emails' in g_config: + g_config['developer_emails'] = {} + #fi + + user = ' '.join (args[0:-1]) + + if user in g_config['developer_emails']: + try: + g_config['developer_emails'][user].remove (args[-1]) + except: + pass + #tried + + if len (g_config['developer_emails'][user]) == 0: + g_config['developer_emails'].pop (user) + self.privmsg (replyto, 'No more developer emails for %s' % user) + else: + self.privmsg (replyto, 'Developer emails for %s are now %s' % + (user, ', '.join (g_config['developer_emails'][user]))) + #fi + save_config() + else: + self.privmsg (replyto, 'There is no developer \'%s\'' % user) + #fi + elif command == 'listdevemails': + check_admin (sender, ident, host, command) + + for dev, emails in g_config['developer_emails'].iteritems(): + self.privmsg (replyto, 'Emails for %s: %s' % (dev, ', '.join (emails))) + #done + elif command == 'checkhg': + check_admin (sender, ident, host, command) + global repocheck_timeout + repocheck_timeout = {'zandronum':0, 'zandronum-stable':0} + process_zan_repo_updates (True) + process_zan_repo_updates (False) elif command == 'die': check_admin (sender, ident, host, command) quit() @@ -654,7 +958,7 @@ break else: raise logical_exception ("unknown autoconnect entry %s" % (aconn)) - + g_BotActive = True asyncore.loop() except KeyboardInterrupt: