Mercurial handling major overhaul. Also get some stuff ready for Python 3

Tue, 04 Aug 2015 22:39:22 +0300

author
Teemu Piippo <tsapii@utu.fi>
date
Tue, 04 Aug 2015 22:39:22 +0300
changeset 146
c17b82b1f573
parent 145
588aff83bb87
child 147
4a72fb181a43

Mercurial handling major overhaul. Also get some stuff ready for Python 3

bt.py file | annotate | diff | comparison | revisions
calc.py file | annotate | diff | comparison | revisions
cobalt.py file | annotate | diff | comparison | revisions
configfile.py file | annotate | diff | comparison | revisions
hgdb.py file | annotate | diff | comparison | revisions
hgpoll.py file | annotate | diff | comparison | revisions
hgrepo.py file | annotate | diff | comparison | revisions
irc.py file | annotate | diff | comparison | revisions
mod_admin.py file | annotate | diff | comparison | revisions
mod_bridge.py file | annotate | diff | comparison | revisions
mod_bt.py file | annotate | diff | comparison | revisions
mod_config.py file | annotate | diff | comparison | revisions
mod_hg.py file | annotate | diff | comparison | revisions
mod_idgames.py file | annotate | diff | comparison | revisions
mod_util.py file | annotate | diff | comparison | revisions
modulecore.py file | annotate | diff | comparison | revisions
rest.py file | annotate | diff | comparison | revisions
utility.py file | annotate | diff | comparison | revisions
--- a/bt.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/bt.py	Tue Aug 04 22:39:22 2015 +0300
@@ -42,13 +42,12 @@
 
 def get_ticket_data (bot, replyto, ticket, withlink):
 	if suds_active == False:
-		print "suds is not active"
 		return
 
 	data = {}
 	try:
 		data = get_issue (ticket)
-	except Exception, e:
+	except Exception as e:
 		bot.privmsg (replyto, "Failed to get info for issue %s: %s" % (ticket, e))
 
 	if data:
@@ -90,13 +89,12 @@
 	global btannounce_id
 	
 	try:
-		print 'Initializing MantisBT connection...'
+		print ('Initializing MantisBT connection...')
 		suds_import = suds.xsd.doctor.Import ('http://schemas.xmlsoap.org/soap/encoding/', 'http://schemas.xmlsoap.org/soap/encoding/')
 		suds_client = suds.client.Client ('https://zandronum.com/tracker/api/soap/mantisconnect.php?wsdl', plugins=[suds.xsd.doctor.ImportDoctor (suds_import)])
 		suds_active = True
 	except Exception as e:
-		print 'Failed to establish MantisBT connection: ' + `e`
-		pass
+		print ('Failed to establish MantisBT connection: %s' % e)
 
 	if suds_active:
 		sys.stdout.write ('Retrieving latest tracker ticket... ')
@@ -104,7 +102,7 @@
 		btannounce_id = suds_client.service.mc_issue_get_biggest_id (user, password, 0)
 		btannounce_active = True
 		update_checktimeout()
-		print btannounce_id
+		print (btannounce_id)
 
 def update_checktimeout():
 	global btannounce_timeout
@@ -119,9 +117,8 @@
 def get_issue (ticket):
 	global suds_client
 	user, password = credentials()
-	print "get_issue: Retrieving issue data for %s" % ticket
+	print ("Retrieving issue data for %s..." % ticket)
 	result = suds_client.service.mc_issue_get (user, password, ticket)
-	print "Issue data recieved."
 	return result
 
 def poll():
--- a/calc.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/calc.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,6 +26,7 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 import re
 import cmath
 import math
@@ -146,7 +147,7 @@
 	'or':		{ 'symbol': '||', 'operands': 2, 'priority': 604, 'function': lambda x, y: x or y },
 }
 
-for name, data in OperatorData.iteritems():
+for name, data in OperatorData.items():
 	Operators.append (Operator (name=name, symbol=data['symbol'], operands=data['operands'],
 		priority=data['priority'], function=data['function']))
 
@@ -230,11 +231,11 @@
 SymbolTypes = {}
 Symbols = []
 
-for name, value in Constants.iteritems():
+for name, value in Constants.items():
 	Symbols.append (name)
 	SymbolTypes[name] = SymbolType.CONSTANT
 
-for name, data in Functions.iteritems():
+for name, data in Functions.items():
 	Symbols.append (name)
 	SymbolTypes[name] = SymbolType.FUNCTION
 
@@ -258,7 +259,7 @@
 	return (len(li) - 1) - li[::-1].index(value)
 
 def realPrint (x):
-	print x
+	print (x)
 
 Attributes = {
 	'hex': lambda self: self.set_preferred_base (0x10),
--- a/cobalt.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/cobalt.py	Tue Aug 04 22:39:22 2015 +0300
@@ -27,6 +27,7 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 import asyncore
 import sys
 import traceback
@@ -93,7 +94,7 @@
 		autoconnects = Config.get_value ('autoconnect', [])
 		
 		if len (autoconnects) == 0:
-			print "Nowhere to connect."
+			print ('Nowhere to connect.')
 			quit()
 		
 		for aconn in autoconnects:
@@ -102,7 +103,7 @@
 					Irc.irc_client (conndata, 0)
 					break
 			else:
-				raise ValueError ("unknown autoconnect entry %s" % (aconn))
+				raise ValueError ('unknown autoconnect entry "%s"' % (aconn))
 		
 		# Start the REST server
 		if Config.get_node ('rest').get_value ('active', True):
--- a/configfile.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/configfile.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,6 +26,7 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 import json, sys
 IsInitialized = False
 Config = None
@@ -95,10 +96,10 @@
 		with open (ConfigFileName, 'w') as fp:
 			json.dump (self.obj, fp, sort_keys = True, indent = 1)
 		
-		print "Config saved."
+		print ("""Config saved.""")
 	
 	def find_developer_by_email (self, commit_email):
-		for developer, emails in self.get_value ('developer_emails', default={}).iteritems():
+		for developer, emails in self.get_value ('developer_emails', default={}).items():
 			if commit_email in emails:
 				return developer
 		
@@ -115,13 +116,13 @@
 	ConfigFileName = filename
 
 	if not IsInitialized:
-		print """Loading configuration..."""
+		print ("""Loading configuration...""")
 
 		try:
 			with open (filename, 'r') as fp:
 				jsondata = json.loads (fp.read())
 		except IOError as e:
-			print ("""couldn't open %s: %s""" % (filename, e))
+			print ("""Couldn't open %s: %s""" % (filename, e))
 			quit()
 
 		global Config
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgdb.py	Tue Aug 04 22:39:22 2015 +0300
@@ -0,0 +1,117 @@
+'''
+	Copyright 2015 Teemu Piippo
+	All rights reserved.
+
+	Redistribution and use in source and binary forms, with or without
+	modification, are permitted provided that the following conditions
+	are met:
+
+	1. Redistributions of source code must retain the above copyright
+	   notice, this list of conditions and the following disclaimer.
+	2. Redistributions in binary form must reproduce the above copyright
+	   notice, this list of conditions and the following disclaimer in the
+	   documentation and/or other materials provided with the distribution.
+	3. The name of the author may not be used to endorse or promote products
+	   derived from this software without specific prior written permission.
+
+	THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+	IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+	OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+	IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+	INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+	NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+'''
+
+import os
+import sqlite3
+import hgpoll
+from datetime import datetime
+from configfile import Config
+
+class HgCommitsDatabase (object):
+	def __init__(self):
+		dbname = Config.get_node('hg').get_value('commits_db', 'commits.db')
+		needNew = not os.path.isfile (dbname)
+		self.db = sqlite3.connect (dbname)
+
+		if needNew:
+			self.create_new()
+
+	def create_new (self):
+		self.db.executescript ('''
+			DROP TABLE IF EXISTS COMMITS;
+			DROP TABLE IF EXISTS REPOS;
+			DROP TABLE IF EXISTS REPOCOMMITS;
+			CREATE TABLE IF NOT EXISTS COMMITS
+			(
+				Node        text NOT NULL,
+				Dateversion text NOT NULL,
+				PRIMARY KEY (Node)
+			);
+			CREATE TABLE IF NOT EXISTS REPOS
+			(
+				Name     text NOT NULL,
+				PRIMARY KEY (Name)
+			);
+			CREATE TABLE IF NOT EXISTS REPOCOMMITS
+			(
+				Reponame text,
+				Node     text,
+				FOREIGN KEY (Reponame) REFERENCES REPOS(Name),
+				FOREIGN KEY (Node) REFERENCES COMMITS(Node)
+			);
+		''')
+
+		print ('Building commits.db...')
+		for repo in hgpoll.Repositories:
+			print ('Adding commits from %s...' % repo.name)
+
+			for line in repo.hg ('log', '--template', '{node} {date|hgdate}\n').splitlines():
+				changeset, timestamp, tz = line.split(' ')
+				self.add_commit (repo, changeset, int (timestamp))
+
+		self.commit()
+
+	def add_commit (self, repo, changeset, timestamp):
+		dateversion = datetime.utcfromtimestamp (timestamp).strftime ('%y%m%d-%H%M')
+		self.db.execute ('''
+			INSERT OR IGNORE INTO REPOS
+			VALUES (?)
+		''', (repo.name,))
+
+		self.db.execute ('''
+			INSERT OR IGNORE INTO COMMITS
+			VALUES (?, ?)
+		''', (changeset, dateversion))
+
+		self.db.execute ('''
+			INSERT INTO REPOCOMMITS 
+			VALUES (?, ?)
+		''', (repo.name, changeset))
+
+	def get_commit_repos (self, node):
+		cursor = self.db.execute ('''
+			SELECT Reponame
+			FROM REPOCOMMITS
+			WHERE Node LIKE ?
+		''', (node + '%',))
+
+		names = cursor.fetchall()
+		names = set (zip (*names)[0]) if names else set()
+		return [hgpoll.RepositoriesByName[name] for name in names]
+
+	def find_commit_by_dateversion (self, dateversion):
+		cursor = self.db.execute ('''
+			SELECT Node
+			FROM COMMITS
+			WHERE Dateversion = ?
+		''', (dateversion,))
+		result = cursor.fetchone()
+		return result[0] if result else None
+
+	def commit(self):
+		self.db.commit()
\ No newline at end of file
--- a/hgpoll.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/hgpoll.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,118 +26,22 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 import time
 import re
 import bt as Bt
 import irc as Irc
-import os, sqlite3, subprocess
-from datetime import datetime
+import os
 from configfile import Config
 import utility
-import subprocess
 import random
-import math
-
-g_CommitsDb = None
-ZDoomRevNumber = 0
-
-def all_repo_names():
-	return Config.get_node ('hg').get_value ('repos', {}).keys()
-
-class CommitsDb (object):
-	def __init__(self):
-		dbname = Config.get_node('hg').get_value('commits_db', 'commits.db')
-		needNew = not os.path.isfile (dbname)
-		self.db = sqlite3.connect (dbname)
-
-		if needNew:
-			self.create_new()
-
-	def create_new (self):
-		self.db.executescript ('''
-			DROP TABLE IF EXISTS COMMITS;
-			DROP TABLE IF EXISTS REPOS;
-			DROP TABLE IF EXISTS REPOCOMMITS;
-			CREATE TABLE IF NOT EXISTS COMMITS
-			(
-				Node        text NOT NULL,
-				Dateversion text NOT NULL,
-				PRIMARY KEY (Node)
-			);
-			CREATE TABLE IF NOT EXISTS REPOS
-			(
-				Name     text NOT NULL,
-				PRIMARY KEY (Name)
-			);
-			CREATE TABLE IF NOT EXISTS REPOCOMMITS
-			(
-				Reponame text,
-				Node     text,
-				FOREIGN KEY (Reponame) REFERENCES REPOS(Name),
-				FOREIGN KEY (Node) REFERENCES COMMITS(Node)
-			);
-		''')
-
-		print 'Building commits.db...'
-		for repo in all_repo_names():
-			print 'Adding commits from %s...' % repo
-
-			data = subprocess.check_output (['hg', '--cwd', repo, 'log', '--template',
-				'{node} {date|hgdate}\n']).splitlines()
+from hgrepo import HgRepository
+from hgdb import HgCommitsDatabase
+import traceback
+import sys
 
-			for line in data:
-				changeset, timestamp, tz = line.split(' ')
-				self.add_commit (repo, changeset, int (timestamp))
-
-		self.commit()
-
-	def add_commit (self, repo, changeset, timestamp):
-		dateversion = datetime.utcfromtimestamp (timestamp).strftime ('%y%m%d-%H%M')
-		self.db.execute ('''
-			INSERT OR IGNORE INTO REPOS
-			VALUES (?)
-		''', (repo,))
-
-		self.db.execute ('''
-			INSERT OR IGNORE INTO COMMITS
-			VALUES (?, ?)
-		''', (changeset, dateversion))
-
-		self.db.execute ('''
-			INSERT INTO REPOCOMMITS 
-			VALUES (?, ?)
-		''', (repo, changeset))
-
-	def get_commit_repos (self, node):
-		cursor = self.db.execute ('''
-			SELECT Reponame
-			FROM REPOCOMMITS
-			WHERE Node LIKE ?
-		''', (node + '%',))
-
-		results = cursor.fetchall()
-		return list (set (zip (*results)[0])) if results else []
-
-	def find_commit_by_dateversion (self, dateversion):
-		cursor = self.db.execute ('''
-			SELECT Node
-			FROM COMMITS
-			WHERE Dateversion = ?
-		''', (dateversion,))
-		result = cursor.fetchone()
-		return result[0] if result else None
-
-	def commit(self):
-		self.db.commit()
-
-def color_for_repo (repo_name):
-	repo_name = repo_name.lower()
-	repoconfig = Config.get_node ('hg').get_node ('repos')
-
-	if repoconfig.has_node (repo_name):
-		return repoconfig.get_node(repo_name).get_value ('colorcode', 0)
-
-	return 0
+Repositories = []
+RepositoriesByName = {}
 
 def prettify_bookmarks (bookmarks):
 	if bookmarks:
@@ -145,112 +49,113 @@
 	else:
 		return ''
 
-def get_repo_info (reponame):
-	reponame = reponame.lower()
-	repoconfig = Config.get_node ('hg').get_node ('repos')
+def get_repo_by_name (name):
+	global Repositories, RepositoriesByName
+	name = name.lower()
 
-	if repoconfig.has_node (reponame):
-		return repoconfig.get_node (reponame)
+	if name not in RepositoriesByName:
+		repo = HgRepository (name)
+		Repositories.append (repo)
+		RepositoriesByName[name] = repo
+	else:
+		repo = RepositoriesByName[name]
 
-	return None
+	return repo
 
-def check_repo_exists (repo_name):
+def check_repo_exists (name):
 	' Check if a repository exists '
-	print ('Checking that %s exists...' % repo_name)
+	print ('Checking that %s exists...' % name)
 
-	if not os.path.exists (repo_name):
-		os.makedirs (repo_name)
+	repo = get_repo_by_name (name)
 
-	if not os.path.isfile (os.path.join (repo_name, '.hg', 'hgrc')):
+	if not os.path.exists (repo.name):
+		os.makedirs (repo.name)
+
+	if not repo.is_valid():
 		# If the repo does not exist, clone it.
-		repo_url = get_repo_info (repo_name).get_value ('url')
 		try:
-			print ('Cloning %s...' % repo_name)
-			subprocess.call (['hg', 'clone', repo_url, repo_name])
+			repo.clone()
 
 			# We need to un-alias a few things, they may be aliased on the host machine (e.g. mine)
 			comms=['log', 'incoming', 'pull', 'commit', 'push', 'outgoing', 'strip', 'transplant']
 			try:
-				with open (os.path.join (repo_name, '.hg', 'hgrc'), 'a') as fp:
+				with open (os.path.join (repo.name, '.hg', 'hgrc'), 'a') as fp:
 					fp.write ('\n[alias]\n' + ''.join(['%s=%s\n' % (x, x) for x in comms]))
 			except Exception as e:
-				print ('Warning: unable to alter hgrc of %s: %s' % repo_name, e)
+				print ('Warning: unable to alter hgrc of %s: %s' % repo.name, e)
 			print ('Cloning done.')
 		except Exception as e:
-			print ('Unable to clone %s from %s: %s' % (repo_name, repo_url, e))
+			raise HgProcessError ('Unable to clone %s from %s: %s' % (repo.name, repo.url, e))
 			quit (1)
 
+	if not repo.is_valid():
+		raise HgProcessError ('''%s is not a valid repository after cloning '''
+			'''(.hg is missing)''' % repo.name)
+
 class HgProcessError (Exception):
 	def __init__ (self, value):
 		self.message = value
 	def __str__ (self):
 		return self.message
 
-def contains_published_repositories (reponames):
-	for reponame in reponames:
-		if is_published (reponame):
+def contains_published_repositories (repos):
+	for repo in repos:
+		if repo.published:
 			return True
 
 	return False
 
-def is_published (reponame):
-	repoinfo = get_repo_info (reponame)
-	return (repoinfo and not repoinfo.get_value ('extrarepo', default=False))
-
-def announce_ticket_resolved (ticket_id, cset):
+def announce_ticket_resolved (ticket_id, cset, db):
 	ticket_id = int (ticket_id)
-	reponames = g_CommitsDb.get_commit_repos (cset)
+	repos = db.get_commit_repos (cset)
 
-	if not contains_published_repositories (reponames):
+	for repo in repos:
+		if repo.published:
+			break
+	else:
 		raise HgProcessError ('Changeset %s is only committed to non-published repositories: %s' %
-			(cset, ', '.join (reponames)))
-
-	repo_url = repoinfo.get_value ('url', default=None)
-
-	if not repo_url:
-		raise HgProcessError ('Repo %s has no url!' % reponame)
+			(cset, ', '.join (repos)))
 
 	# Acquire additional data
-	moredata = get_commit_data (reponame, cset,
-		r"{author|person}\n{date(date, '%A %d %B %Y %H:%M:%S')}\n{author|email}").split('\n')
+	commit = repo.get_commit_data (rev=cset,
+		author='author|person',
+		date='date(date, "%A %d %B %Y %H:%M:%S")',
+		email='author|email',
+		message='desc')
 
 	if len (moredata) != 2:
-		raise HgProcessError ('malformed hg data while processing %s' % cset)
+		raise HgProcessError ('Received invalid hg data while processing %s' % cset)
 
-	commit_author = moredata[0]
-	commit_date = moredata[1]
-	commit_email = moredata[2]
-	commit_message = subprocess.check_output (['hg', '--cwd', reponame, 'log', '--rev', cset, '--template', '{desc}'])
-	commit_diffstat = subprocess.check_output (['hg', '--cwd', reponame, 'diff', '--change', cset, '--stat'])
+	diffstat = repo.hg ('diff', '--change', cset, '--stat')
 
 	try:
 		ticket_data = Bt.get_issue (ticket_id)
 	except Exception as e:
 		raise HgProcessError ("error while processing %s: %s" % (cset, e))
 
-	if len(commit_diffstat) > 0:
-		commit_diffstat = 'Changes in files:\n[code]\n' + commit_diffstat + '\n[/code]'
+	if len(diffstat) > 0:
+		diffstat = 'Changes in files:\n[code]\n' + diffstat + '\n[/code]'
 	else:
-		commit_diffstat = 'No changes in files.'
+		diffstat = 'No changes in files.'
 
 	# Compare the email addresses against known developer usernames
-	commit_trackeruser = Config.find_developer_by_email (commit_email)
+	username = Config.find_developer_by_email (commit['email'])
 
-	if commit_trackeruser != '':
-		commit_author += ' [%s]' % commit_trackeruser
+	if username:
+		commit['author'] += ' [%s]' % username
 
-	commit_message = commit_message.replace ("\n", " ")
+	commit['message'] = commit['message'].replace ("\n", " ")
 
 	message = 'Issue addressed by commit %s: [b][url=%s/commits/%s]%s[/url][/b]' \
-		% (cset, repo_url, cset, commit_message)
+		% (cset, repo.url, cset, commit['message'])
 	message += "\nCommitted by %s on %s\n\n%s" \
-			% (commit_author, commit_date, commit_diffstat)
+			% (commit['author'], commit['date'], diffstat)
 
 	need_update = False
 
 	# If not already set, set handler
-	if not 'handler' in ticket_data:
-		ticket_data['handler'] = {'name': commit_trackeruser}
+	if username and not 'handler' in ticket_data:
+		ticket_data['handler'] = {'name': username}
 		need_update = True
 
 	# Find out the status level of the ticket
@@ -271,7 +176,7 @@
 			if channel.get_value ('btannounce', default=True):
 				irc_client.privmsg (channel.get_value ('name'),
 					"\003%d%s\003: commit\0035 %s\003 addresses issue\002\0032 %d\002" % \
-					(color_for_repo (reponame), reponame, cset, ticket_id))
+					(repo.color, repo.name, cset, ticket_id))
 				irc_client.privmsg (channel.get_value ('name'),
 					"Read all about it here: " + Bt.get_ticket_url (ticket_id))
 
@@ -285,159 +190,131 @@
 
 def init():
 	global repocheck_timeout
-	global g_CommitsDb
 
-	for reponame in all_repo_names():
-		check_repo_exists (reponame)
+	for name in Config.get_node ('hg').get_value ('repos', {}).keys():
+		check_repo_exists (name)
 
-	g_CommitsDb = CommitsDb()
-	repocheck_timeout = time.time() + 15
+	# Let the database check if commits.db needs to be built
+	HgCommitsDatabase()
 
-def get_commit_data (reponame, rev, template):
-	return subprocess.check_output (['hg', '--cwd', reponame, 'log', '--limit', '1', '--rev', rev, '--template', template])
+	repocheck_timeout = time.time() + 15
 
 def poll():
 	global repocheck_timeout
-	if time.time() < repocheck_timeout:
-		return
-
-	hgns = Config.get_node ('hg')
-	repocheck_timeout = time.time() + hgns.get_value ('checkinterval', default=15) * 60
-
-	for reponame in all_repo_names():
-		poll_one_repo (reponame)
-
-def poll_one_repo (repo_name):
-	global repocheck_timeout
-	hgns = Config.get_node ('hg')
-
-	if not hgns.get_value ('track', default=True):
-		return
-
-	commit_data = []
-	delimeter = '^^^^^^^^^^'
-	delimeter2 = '@@@@@@@@@@'
-	maxcommits = 15
-	numcommits = 0
-	print 'Checking %s for updates' % repo_name
 
 	try:
-		data = subprocess.check_output (['hg', '--cwd', repo_name, 'incoming',
-				'--limit', str(maxcommits), '--quiet', '--template',
-			delimeter.join (['{node|short}', '{desc}']) + delimeter2]).split (delimeter2)
-	except subprocess.CalledProcessError:
-		return
+		if time.time() < repocheck_timeout:
+			return
+
+		hgns = Config.get_node ('hg')
+		repocheck_timeout = time.time() + hgns.get_value ('checkinterval', default=15) * 60
+		maxcommits = 15
+		numcommits = 0
+
+		for repo in Repositories:
+			numcommits += poll_one_repo (repo, maxcommits=maxcommits - numcommits)
+
+			if numcommits >= maxcommits:
+				# There may be more coming so recheck sooner				
+				print ('Processed %d commits, checking for new commits in 1 minute...' % numcommits)
+				repocheck_timeout = time.time() + 60
+				return
 	except Exception as e:
-		Irc.broadcast (e.__class__.__name__ + ": " + str (e))
+		print (traceback.format_exception (*sys.exc_info()))
+		Irc.broadcast (str (e))
+
+def poll_one_repo (repo, maxcommits):
+	if not Config.get_node ('hg').get_value ('track', default=True):
 		return
 
-	for line in data:
-		if line:
-			numcommits += 1
-			commit_data.append (line.split (delimeter))
-
-	process_new_commits (repo_name, commit_data)
-
-	if numcommits == maxcommits:
-		# If we have 25 commits here, there may be more coming so recheck sooner
-		global repocheck_timeout
-		print ('Processed %d commits, checking for new commits in 1 minute...' % len(data))
-		repocheck_timeout = time.time() + 60
-
-def process_new_commits (repo_name, commit_data):
-	if len (commit_data) == 0:
-		return
+	print ('Checking %s for updates' % repo.name)
+	commits = repo.incoming (maxcommits=maxcommits, node='node|short', message='desc')
+	process_new_commits (repo, commits)
+	return len(commits)
 
-	repo_name = repo_name.lower()
-	repo_url = get_repo_info (repo_name).get_value ('url')
-	isExtraRepo = get_repo_info (repo_name).get_value ('extrarepo', False)
-	print ('%d new commits on %s' % (len (commit_data), repo_name))
-	pull_args = []
-	messages = [[], [], []]
-
-	for commit in commit_data:
-		pull_args.append ('-r');
-		pull_args.append (commit[0]);
-
-	print ('Pulling new commits...')
-	try:
-		subprocess.call (['hg', '--cwd', repo_name, 'pull'] + pull_args)
-	except Exception as e:
-		Irc.broadcast ('Warning: unable to pull: %s' % `e`)
+def process_new_commits (repo, commits):
+	if not commits:
 		return
 
-	LENGTH_MINIMUM, LENGTH_SHORT, LENGTH_FULL = range (0, 3)
+	print ('%d new commits on %s' % (len (commits), repo.name))
+	pull_args = ['pull']
+	messages = [[], [], []]
+
+	for commit in commits:
+		pull_args.append ('-r');
+		pull_args.append (commit['node']);
 
-	for commit in commit_data:
-		commit_node = commit[0]
-		commit_message = commit[1]
-		print ('Processing new commit %s...' % commit_node)
+	print ('Pulling new commits...')
+	repo.hg (*pull_args)
+	LENGTH_MINIMUM, LENGTH_SHORT, LENGTH_FULL = range (0, 3)
+	db = HgCommitsDatabase()
 
-		try:
-			existingrepos = g_CommitsDb.get_commit_repos (commit_node)
-			alreadyAdded = len (existingrepos) > 0
+	for commit in commits:
+		print ('Processing new commit %s...' % commit['node'])
 
-			delim = '@@@@@@@@@@'
-			data = get_commit_data (repo_name, commit_node, delim.join (['{node}', '{author|person}',
-				'{bookmarks}', '{date|hgdate}', '{author|email}'])).split (delim)
-			commit_full_node = data[0]
-			commit_author = data[1]
-			commit_bookmarks = prettify_bookmarks (data[2])
-			commit_time = int (data[3].split (' ')[0])
-			commit_url = '%s/commits/%s' % (repo_url, commit_node)
-			commit_email = data[4]
-			isMergeFromSandbox = False
+		existingrepos = db.get_commit_repos (commit['node'])
+		alreadyAdded = len (existingrepos) > 0
 
-			# If the commit was already in the commits database, it is not a new one and we should
-			# not react to it. Still add it to the db though so that the new repo name is added.
-			g_CommitsDb.add_commit (repo=repo_name, changeset=commit_full_node, timestamp=commit_time)
+		commit = dict (commit, **repo.get_commit_data (rev=commit['node'],
+			fullnode='node',
+			author='author|person',
+			bookmarks='bookmarks',
+			date='date|hgdate',
+			email='author|email'))
+		commit['bookmarks'] = prettify_bookmarks (commit['bookmarks'])
+		commit['time'] = int (commit['date'].split (' ')[0])
+		commit['url'] = '%s/commits/%s' % (repo.url, commit['node'])
+		isMergeFromSandbox = False
 
-			if alreadyAdded:
-				if not contains_published_repositories (existingrepos) and is_published (repo_name):
-					isMergeFromSandbox = True
-					print ('''%s appears to be a merge from sandbox (exists in %s)''' %
-						(commit_node, existingrepos))
-				else:
-					print ('''I already know of %s - they're in %s - not announcing.''' %
-						(commit_node, existingrepos))
-					continue
+		# If the commit was already in the commits database, it is not a new one and we should
+		# not react to it (unless a merge from sandbox). Still add it to the db though so that
+		# the new repo name is added.
+		db.add_commit (repo=repo, changeset=commit['fullnode'],
+			timestamp=commit['time'])
 
-			commit_trackeruser = Config.find_developer_by_email (commit_email)
-			committer = commit_trackeruser if commit_trackeruser else commit_author
-			commitDescriptor = """commit""" if int (random.random() * 100) != 0 else """KERMIT"""
-
-			if not isMergeFromSandbox:
-				commitMessage = """\003%d%s\003: new %s\0035 %s%s\003 by\0032 %s\003: %s""" % \
-					(color_for_repo (repo_name), repo_name, commitDescriptor, commit_node, commit_bookmarks,
-					committer, utility.shorten_link (commit_url))
-
-				for length in [LENGTH_MINIMUM, LENGTH_SHORT, LENGTH_FULL]:
-					messages[length].append (commitMessage)
-
-				messages[LENGTH_SHORT].append ('   ' + commit_message.splitlines()[0])
-
-				for line in commit_message.splitlines()[0:4]:
-					messages[LENGTH_FULL].append ('    ' + line)
+		if alreadyAdded:
+			if not contains_published_repositories (existingrepos) and repo.published:
+				isMergeFromSandbox = True
+				print ('''%s appears to be a merge from sandbox (exists in %s)''' %
+					(commit['node'], existingrepos))
 			else:
-				commitMessage = """\003%d%s\003: %s\0035 %s%s\003 by\0032 %s\003 was pulled: %s""" % \
-					(color_for_repo (repo_name), repo_name, commitDescriptor, commit_node, commit_bookmarks,
-					committer, utility.shorten_link (commit_url))
+				print ('''I already know of %s - they're in %s - not announcing.''' %
+					(commit['node'], existingrepos))
+				continue
+
+		username = Config.find_developer_by_email (commit['email'])
+		committer = username if username else commit['author']
+		descriptor = """commit""" if int (random.random() * 100) != 0 else """KERMIT"""
 
-				for length in [LENGTH_MINIMUM, LENGTH_SHORT, LENGTH_FULL]:
-					messages[length].append (commitMessage)
+		if not isMergeFromSandbox:
+			commitMessage = """\003%d%s\003: new %s\0035 %s%s\003 by\0032 %s\003: %s""" % \
+				(repo.color, repo.name, descriptor, commit['node'], commit['bookmarks'],
+				committer, utility.shorten_link (commit['url']))
 
-			if not isExtraRepo:
-				rex = re.compile (r'^.*(fixes|resolves|addresses|should fix) ([0-9]+).*$')
+			for length in [LENGTH_MINIMUM, LENGTH_SHORT, LENGTH_FULL]:
+				messages[length].append (commitMessage)
+
+			messages[LENGTH_SHORT].append ('   ' + commit['message'].splitlines()[0])
 
-				for line in commit_message.splitlines():
-					match = rex.match (line)
+			for line in commit['message'].splitlines()[:4]:
+				messages[LENGTH_FULL].append ('    ' + line)
+		else:
+			commitMessage = """\003%d%s\003: %s\0035 %s\003 by\0032 %s\003 was pulled: %s""" % \
+				(repo.color, repo.name, descriptor, commit['node'], committer,
+				utility.shorten_link (commit['url']))
+
+			for length in [LENGTH_MINIMUM, LENGTH_SHORT, LENGTH_FULL]:
+				messages[length].append (commitMessage)
 
-					if match:
-						announce_ticket_resolved (match.group (2), commit_node)
-						break
-		except Exception as e:
-			Irc.broadcast ('Error while processing %s: %s' % (commit_node, e))
-			continue
+		if repo.published:
+			rex = re.compile (r'^.*(fixes|resolves|addresses|should fix) ([0-9]+).*$')
+
+			for line in commit['message'].splitlines():
+				match = rex.match (line)
+
+				if match:
+					announce_ticket_resolved (match.group (2), commit['node'], db)
+					break
 
 	# Encode messages
 	for messagelist in messages:
@@ -460,19 +337,12 @@
 				if not channel.get_value ('btannounce', False):
 					continue
 
-				if isExtraRepo and not channel.get_value ('allpublishing', False):
+				if not repo.published and not channel.get_value ('allpublishing', False):
 					continue
 
 				irc_client.privmsg (channel.get_value ('name'), message)
 
-	g_CommitsDb.commit()
-
-def make_progress_bar (p, barLength, colored=True):
-	BoldChar, ColorChar = (Irc.BoldChar, Irc.ColorChar)
-	return BoldChar + '[' + BoldChar \
-	     + ColorChar + '2,2' + ('=' * int (round (p * barLength))) \
-	     + ColorChar + '1,1' + ('-' * int (barLength - round (p * barLength))) \
-	     + ColorChar + BoldChar + ']' + BoldChar
+	db.commit()
 
 def force_poll():
 	global repocheck_timeout
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgrepo.py	Tue Aug 04 22:39:22 2015 +0300
@@ -0,0 +1,109 @@
+'''
+	Copyright 2015 Teemu Piippo
+	All rights reserved.
+
+	Redistribution and use in source and binary forms, with or without
+	modification, are permitted provided that the following conditions
+	are met:
+
+	1. Redistributions of source code must retain the above copyright
+	   notice, this list of conditions and the following disclaimer.
+	2. Redistributions in binary form must reproduce the above copyright
+	   notice, this list of conditions and the following disclaimer in the
+	   documentation and/or other materials provided with the distribution.
+	3. The name of the author may not be used to endorse or promote products
+	   derived from this software without specific prior written permission.
+
+	THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+	IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+	OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+	IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+	INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+	NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+'''
+
+import subprocess
+import sys
+import os
+from configfile import Config
+
+class HgRepository (object):
+	def __init__ (self, reponame):
+		reponame = reponame.lower()
+		repoconfig = Config.get_node ('hg').get_node ('repos')
+		repoinfo = repoconfig.get_node (reponame)
+
+		if not repoinfo:
+			raise ValueError ('Unknown repository "%s"' % reponame)
+
+		self.name = reponame
+		self.published = not bool (repoinfo.get_value ('extrarepo', default=False))
+		self.url = repoinfo.get_value ('url')
+		self.color = int (repoinfo.get_value ('colorcode', default=0))
+
+		if not self.url:
+			raise ValueError ('Repository %s has no url!' % reponame)
+
+	def hg (self, *args):
+		output = subprocess.check_output (['hg', '--cwd', self.name] + list (args))
+
+		if sys.version_info >= (3, 0):
+			output = output.decode ('utf-8', 'ignore')
+
+		return output
+
+	def is_valid (self):
+		return os.path.isdir (os.path.join (self.name, '.hg'))
+
+	def split_template (self, kvargs, separator):
+		return separator.join (['{%s}' % x[1] for x in kvargs.items()])
+
+	def merge_template (self, data, args):
+		result = {}
+		items = list (args.items())
+		assert (len (items) == len (data)), '''Bad amount of items from hg log'''
+
+		for i in range (0, len (items)):
+			result[items[i][0]] = data[i]
+
+		return result
+
+	def get_commit_data (self, rev, **kvargs):
+		if not kvargs:
+			raise ValueError ('template arguments must be provided')
+
+		separator = '^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^'
+		template = self.split_template (kvargs, separator)
+		data = self.hg ('log', '--limit', '1', '--rev', rev, '--template', template)
+		data = data.split (separator)
+		return self.merge_template (data, kvargs)
+
+	def incoming (self, maxcommits=0, **kvargs):
+		if not kvargs:
+			raise ValueError ('template arguments must be provided')
+
+		commit_data = []
+		separator = '^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^'
+		separator2 = '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
+		template = self.split_template (kvargs, separator)
+		limit = ['--limit', str(maxcommits)] if maxcommits else []
+
+		try:
+			args = ['incoming'] + limit + ['--quiet', '--template', template + separator2]
+			data = self.hg (*args)
+			data = data.split (separator2)
+
+			if not data[-1]:
+				data = data[:-1]
+
+			return [self.merge_template (data=x.split (separator), args=kvargs) for x in data]
+		except subprocess.CalledProcessError:
+			return []
+
+	def clone():
+		print ('Cloning %s...' % repo.name)
+		subprocess.call (['hg', 'clone', repo.url, repo.name])
\ No newline at end of file
--- a/irc.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/irc.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,6 +26,7 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 import asyncore
 import socket
 import sys
@@ -60,7 +61,8 @@
 # Prints a line to log channel(s)
 #
 def broadcast (line):
-	print line
+	print (line)
+
 	for client in all_clients:
 		if not client.flags & CLIF_CONNECTED:
 			continue
@@ -104,7 +106,8 @@
 		all_clients.append (self)
 		asyncore.dispatcher.__init__ (self)
 		self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
-		print "[%s] Connecting to [%s] %s:%d..." % (get_timestamp(), self.name, self.host, self.port)
+		print ("[%s] Connecting to [%s] %s:%d..." % \
+			(get_timestamp(), self.name, self.host, self.port))
 		self.connect ((self.host, self.port))
 		ClientsByName[self.name] = self
 
@@ -116,17 +119,21 @@
 		self.write ("NICK %s" % self.mynick)
 
 	def handle_connect (self):
-		print "[%s] Connected to [%s] %s:%d" % (get_timestamp(), self.name, self.host, self.port)
+		print ("[%s] Connected to [%s] %s:%d" % (get_timestamp(), self.name, self.host, self.port))
 		self.register_to_irc()
 
 	def write (self, utfdata):
 		try:
-			self.send_buffer.append ("%s" % utfdata.decode("utf-8","ignore").encode("ascii","ignore"))
+			if sys.version_info < (3, 0):
+				self.send_buffer.append (utfdata.decode("utf-8","ignore").encode("ascii","ignore"))
+			else:
+				self.send_buffer.append (utfdata)
 		except UnicodeEncodeError:
 			pass
 
 	def handle_close (self):
-		print "[%s] Connection to [%s] %s:%d terminated." % (get_timestamp(), self.name, self.host, self.port)
+		print ("[%s] Connection to [%s] %s:%d terminated." %
+			(get_timestamp(), self.name, self.host, self.port))
 		self.close()
 
 	def handle_write (self):
@@ -141,24 +148,31 @@
 	def send_all_now (self):
 		for line in self.send_buffer:
 			if self.verbose:
-				print "[%s] [%s] <- %s" % (get_timestamp(), self.name, line)
-			self.send ("%s\n" % line)
+				print ("[%s] [%s] <- %s" % (get_timestamp(), self.name, line))
+
+			if sys.version_info >= (3, 0):
+				self.send (bytes (line + '\n', 'UTF-8'))
+			else:
+				self.send (line + '\n')
 			time.sleep (0.25)
 		self.send_buffer = []
 
 	def handle_read (self):
 		lines = self.recv (4096).splitlines()
-		for utfline in lines:
+		for line in lines:
 			try:
-				line = utfline.decode("utf-8","ignore").encode("ascii","ignore")
+				line = line.decode ('utf-8', 'ignore')
+
+				if sys.version_info < (3, 0):
+					line = line.encode ('ascii', 'ignore')
 			except UnicodeDecodeError:
 				continue
 
 			if self.verbose:
-				print "[%s] [%s] -> %s" % (get_timestamp(), self.name, line)
+				print ("[%s] [%s] -> %s" % (get_timestamp(), self.name, line))
 
-			if line.startswith ("PING :"):
-				self.write ("PONG :%s" % line[6:])
+			if line[:len('PING :')] == ('PING :'):
+				self.write ("PONG :%s" % line[len('PING :'):])
 				self.send_all_now() # pings must be responded to immediately!
 			else:
 				words = line.split(" ")
@@ -172,7 +186,9 @@
 							self.write ('MODE %s %s' % (self.mynick, self.cfg.get_value ('umode', '')))
 
 						for channel in self.channels:
-							self.write ("JOIN %s %s" % (channel.get_value ('name'), channel.get_value ('password', default='')))
+							self.write ("JOIN %s %s" %
+								(channel.get_value ('name'),
+								channel.get_value ('password', default='')))
 					elif words[1] == "PRIVMSG":
 						self.handle_privmsg (line)
 					elif words[1] == 'QUIT':
@@ -271,7 +287,7 @@
 			return
 
 	def handle_error(self):
-		raise RestartError (traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))
+		raise RestartError (traceback.format_exception (*sys.exc_info()))
 
 	def restart(self):
 		raise RestartError('')
--- a/mod_admin.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/mod_admin.py	Tue Aug 04 22:39:22 2015 +0300
@@ -1,3 +1,4 @@
+from __future__ import print_function
 from modulecore import command_error
 import sys
 
--- a/mod_bridge.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/mod_bridge.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,6 +26,7 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 import re
 from configfile import Config
 import irc as Irc
--- a/mod_bt.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/mod_bt.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,7 +26,8 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
-import bt as Bt
+from __future__ import print_function
+import bt
 
 ModuleData = {
 	'commands':
@@ -62,16 +63,16 @@
 }
 
 def cmd_ticket (bot, args, replyto, **rest):
-	Bt.get_ticket_data (bot, replyto, args['ticket'], True)
+	bt.get_ticket_data (bot, replyto, args['ticket'], True)
 
 def cmd_testannounce (bot, args, **rest):
-	Bt.announce_new_issue (bot, Bt.get_issue (args['ticket']))
+	bt.announce_new_issue (bot, bt.get_issue (args['ticket']))
 
 def cmd_checkbt (bot, **rest):
-	Bt.poll()
+	bt.poll()
 
 def cmd_btsoap (bot, args, reply, **rest):
-	result = Bt.custom_query (args['func'], args['args'].split (' ') if 'args' in args else [])
+	result = bt.custom_query (args['func'], args['args'].split (' ') if 'args' in args else [])
 
 	for line in result.splitlines():
 		reply (line)
\ No newline at end of file
--- a/mod_config.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/mod_config.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,6 +26,7 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 from modulecore import command_error
 from configfile import Config
 
--- a/mod_hg.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/mod_hg.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,10 +26,10 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 from datetime import datetime
 import hgpoll as HgPoll
 import re
-import bt as Bt
 import subprocess
 from configfile import Config
 from modulecore import command_error
--- a/mod_idgames.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/mod_idgames.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,10 +26,11 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 from modulecore import command_error
 import urllib
-import urllib2
 import json
+import utility
 
 ModuleData = {
 	'commands':
@@ -48,7 +49,7 @@
 def cmd_idgames (bot, args, reply, **rest):
 	try:
 		url = g_idgamesSearchURL % urllib.quote (args['wad'])
-		response = urllib2.urlopen (url).read()
+		response = utility.read_url (url)
 		data = json.loads (response)
 
 		if 'content' in data and 'file' in data['content']:
--- a/mod_util.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/mod_util.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,8 +26,8 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 import math
-import urllib2
 import json
 import subprocess
 import re
@@ -147,7 +147,7 @@
 def cmd_ud (bot, args, reply, **rest):
 	try:
 		url = 'http://api.urbandictionary.com/v0/define?term=%s' % (args['term'].replace (' ', '%20'))
-		response = urllib2.urlopen (url).read()
+		response = utility.read_url (url)
 		data = json.loads (response)
 		
 		if not 'list' in data \
@@ -198,7 +198,7 @@
 
 def cmd_calcfunctions (bot, reply, **rest):
 	reply ('Available functions for .calc: %s' % \
-		', '.join (sorted ([name for name, data in calc.Functions.iteritems()])))
+		', '.join (sorted ([name for name, data in calc.Functions.items()])))
 
 def cmd_calc (bot, reply, args, **rest):
 	reply (calc.Calculator().calc (args['expression']))
--- a/modulecore.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/modulecore.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,6 +26,7 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 import os
 import re
 import time
@@ -82,7 +83,7 @@
 				cmd['argnames'].append (argname)
 
 		if 'hooks' in module.ModuleData:
-			for key, hooks in module.ModuleData['hooks'].iteritems():
+			for key, hooks in module.ModuleData['hooks'].items():
 				for hook in hooks:
 					if key not in Hooks:
 						Hooks[key] = []
@@ -90,9 +91,9 @@
 					Hooks[key].append ({'name': hook, 'func': getattr (module, hook)})
 					numHooks += 1
 
-		print "Loaded module %s" % fn
+		print ('''Loaded module %s''' % fn)
 
-	print ('Loaded %d commands and %d hooks in %d modules' %
+	print ('''Loaded %d commands and %d hooks in %d modules''' %
 		(len (Commands), numHooks, len (Modules)))
 
 #
@@ -184,7 +185,7 @@
 	try:
 		func = getattr (commandObject['module'], 'cmd_' + cmdname)
 	except AttributeError:
-		command_error ('command "%s" is not defined!' % cmdname)
+		command_error ('''command '%s' does not have a definition''' % cmdname)
 
 	try:
 		func (**commandObject)
@@ -211,16 +212,17 @@
 	try:
 		cmd = Commands[cmdname]
 	except KeyError:
+		print ('''No such command %s''' % cmdname)
 		return
 
-	if not is_available (cmd, kvargs['ident'], kvargs['host']):
-		command_error ("you may not use %s" % cmdname)
+	if not is_available (cmd=cmd, ident=kvargs['ident'], host=kvargs['host']):
+		command_error ('''you may not use %s''' % cmdname)
 
 	match = re.compile (cmd['regex']).match (message)
 
 	if match == None:
 		# didn't match
-		command_error ('invalid arguments\nusage: %s %s' % (cmd['name'], cmd['args']))
+		command_error ('''usage: %s %s''' % (cmd['name'], cmd['args']))
 
 	# .more is special as it is an interface to the page system.
 	# Anything else resets it.
@@ -235,7 +237,7 @@
 		args[argname] = match.group (i)
 		i += 1
 
-	print "ModuleCore: %s called by %s" % (cmdname, kvargs['sender'])
+	print ('''%s was called by %s''' % (cmdname, kvargs['sender']))
 	commandObject = kvargs
 	commandObject['bot'] = bot
 	commandObject['cmdname'] = cmdname
@@ -295,7 +297,7 @@
 def get_available_commands (ident, host):
 	result=[]
 
-	for cmdname,cmd in Commands.iteritems():
+	for cmdname,cmd in Commands.items():
 		if not is_available (cmd, ident, host):
 			continue
 
@@ -329,11 +331,11 @@
 
 	for arg in arglist.split (' '):
 		if gotvariadic:
-			raise CommandError ('variadic argument is not last')
+			raise CommandError ('''variadic argument is not last''')
 
 		if arg == '':
 			continue
-		
+
 		gotliteral = False
 
 		if arg[0] == '[' and arg[-1] == ']':
@@ -341,7 +343,7 @@
 			gotoptional = True
 		elif arg[0] == '<' and arg[-1] == '>':
 			if gotoptional:
-				raise CommandError ('mandatory argument after optional one')
+				raise CommandError ('''mandatory argument after optional one''')
 
 			arg = arg[1:-1]
 		else:
@@ -349,7 +351,7 @@
 
 		if arg[-3:] == '...':
 			gotvariadic = True
-			arg = arg[0:-3]
+			arg = arg[:-3]
 
 		if gotoptional == False:
 			regex += r'\s+'
@@ -368,11 +370,8 @@
 				regex += r'(.+)'
 			else:
 				regex += r'([^ ]+)'
-		#fi
-	#done
 
 	if not gotvariadic:
 		regex += r'\s*'
 
 	return '^[^ ]+%s$' % regex
-#enddef
--- a/rest.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/rest.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,6 +26,7 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
+from __future__ import print_function
 import ssl
 import socket
 import errno
@@ -44,15 +45,17 @@
 g_portnumber = None
 g_throttle = []
 
+# TODO: get rid of this array
 valid_repos = ['torr_samaho/zandronum', 'torr_samaho/zandronum-stable',
-	'crimsondusk/zandronum-sandbox', 'crimsondusk/zandronum-sandbox-stable', 'crimsondusk/zfc9000', 'blzut3/doomseeker']
+	'crimsondusk/zandronum-sandbox', 'crimsondusk/zandronum-sandbox-stable',
+	'crimsondusk/zfc9000', 'blzut3/doomseeker']
 
 def is_throttled (address):
 	i = 0
 
 	while i < len (g_throttle):
 		if g_throttle[i][1] <= datetime.utcnow():
-			print 'Throttle of %s expired' % g_throttle[i][0]
+			print ('Throttle of %s expired' % g_throttle[i][0])
 			item = g_throttle.pop (i) # expired
 
 			if item[0] == address:
@@ -154,7 +157,7 @@
 			try:
 				self.socket.do_handshake()
 				break
-			except ssl.SSLError, err:
+			except ssl.SSLError as err:
 				if err.args[0] == ssl.SSL_ERROR_WANT_READ:
 					select.select([self.socket], [], [])
 				elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
@@ -211,7 +214,7 @@
 		self.setsockopt (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 		self.bind (('', g_portnumber))
 		self.listen (5)
-		print 'REST server initialized'
+		print ('REST server initialized')
 
 	def handle_accept (self):
 		sock, address = self.accept()
--- a/utility.py	Mon Aug 03 19:45:57 2015 +0300
+++ b/utility.py	Tue Aug 04 22:39:22 2015 +0300
@@ -26,18 +26,43 @@
 	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 '''
 
-import bitly_api
+from __future__ import print_function
+import sys
 import irc as Irc
 from configfile import Config
 
+try:
+	import bitly_api
+except ImportError as e:
+	print ('Unable to import bitly_api: %s' % e)
+
+try:
+	import urllib.request
+except ImportError:
+	import urllib2
+
 def shorten_link (link):
-	bitly_token = Config.get_node ('bitly').get_value ('access_token', '')
+	if 'bitly_api' in sys.modules:
+		bitly_token = Config.get_node ('bitly').get_value ('access_token', '')
 
-	if bitly_token:
-		c = bitly_api.Connection (access_token = bitly_token)
-		try:
-			return c.shorten (link)['url']
-		except Exception as e:
-			Irc.broadcast ('Error while shortening link "%s": %s' % (link, e))
+		if bitly_token:
+			c = bitly_api.Connection (access_token = bitly_token)
+			try:
+				return c.shorten (link)['url']
+			except Exception as e:
+				Irc.broadcast ('Error while shortening link "%s": %s' % (link, e))
+
+	return link
 
-	return link
\ No newline at end of file
+def read_url (url):
+	if 'urllib.request' in sys.modules:
+		return urllib.request.urlopen (urllib.request.Request (url)).read()
+	else:
+		return urllib2.urlopen (url).read()
+
+def make_progress_bar (p, barLength, colored=True):
+	BoldChar, ColorChar = (Irc.BoldChar, Irc.ColorChar)
+	return BoldChar + '[' + BoldChar \
+	     + ColorChar + '2,2' + ('=' * int (round (p * barLength))) \
+	     + ColorChar + '1,1' + ('-' * int (barLength - round (p * barLength))) \
+	     + ColorChar + BoldChar + ']' + BoldChar
\ No newline at end of file

mercurial