- commit work done on splitting bt/hg

Sun, 09 Nov 2014 19:59:10 +0200

author
Teemu Piippo <crimsondusk64@gmail.com>
date
Sun, 09 Nov 2014 19:59:10 +0200
changeset 72
2266d6d73de3
parent 67
f8cc57c608e2
child 73
d67cc4fbc3f1

- commit work done on splitting bt/hg

bt.py file | annotate | diff | comparison | revisions
cobalt.py file | annotate | diff | comparison | revisions
hgpoll.py file | annotate | diff | comparison | revisions
mod_bt.py file | annotate | diff | comparison | revisions
mod_hgpoll.py file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bt.py	Sun Nov 09 19:59:10 2014 +0200
@@ -0,0 +1,91 @@
+import suds
+import cobalt
+
+suds_active = False
+btannounce_active = False
+btannounce_timeout = 0
+
+def is_active():
+	return suds_active
+
+def init():
+	try:
+		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
+
+	if suds_active:
+		sys.stdout.write ('Retrieving latest tracker ticket... ')
+		user, password = bt_credentials()
+		btannounce_id = suds_client.service.mc_issue_get_biggest_id (user, password, 0)
+		btannounce_active = True
+		bt_updatechecktimeout()
+		print btannounce_id
+
+def update_checktimeout():
+	global btannounce_timeout
+	btannounce_timeout = time.time() + (Config.get_node ('bt').get_value ('checkinterval', default=5) * 60)
+
+def credentials():
+	bt = Config.get_node ('bt')
+	user = bt.get_value ('username', '')
+	password = bt.get_value ('password', '')
+	return [user, password]
+
+def get_issue(ticket):
+	global suds_client
+	user, password = bt_credentials()
+	return suds_client.service.mc_issue_get (user, password, ticket)
+
+def poll():
+	global btannounce_timeout
+	global btannounce_id
+
+	if time.time() >= btannounce_timeout:
+		bt_updatechecktimeout()
+		newid = btannounce_id
+		try:
+			user, password = bt_credentials()
+			newid = suds_client.service.mc_issue_get_biggest_id (user, password, 0)
+		except Exception as e:
+			pass
+
+		while newid > btannounce_id:
+			try:
+				btannounce_id += 1
+				data = bt_getissue (btannounce_id)
+
+				for client in cobalt.all_clients:
+					announce_new_ticket (client, data)
+			except Exception as e:
+				pass
+
+def get_ticket_url (ticket):
+	url = Config.get_node ('bt').get_value ('url')
+	return 'https://%s/view.php?id=%s' % (url, ticket)
+
+
+#
+# Print a ticket announce to appropriate channels
+#
+def announce_new_issue (bot, data):
+	idstring = "%d" % data.id
+	while len(idstring) < 7:
+		idstring = "0" + idstring
+
+	isprivate = data['view_state']['name'] == 'private'
+	reporter = data['reporter']['name'] if hasattr (data['reporter'], 'name') else '<nobody>'
+
+	for channel in self.channels:
+		if channel.get_value ('btannounce', False):
+			if not isprivate or (channel.get_value ('btprivate', False)):
+				self.write ("PRIVMSG %s :[%s] New issue %s, reported by %s: %s: %s" % \
+					(channel['name'], data['project']['name'], idstring, reporter,
+					data['summary'], self.get_ticket_url (idstring)))
+			#fi
+		#fi
+	#done
--- a/cobalt.py	Sun Nov 09 19:18:58 2014 +0200
+++ b/cobalt.py	Sun Nov 09 19:59:10 2014 +0200
@@ -37,92 +37,53 @@
 import urllib2
 import hgapi
 import os
-import suds
 import math
 import json
 from datetime import datetime
 import modulecore as ModuleCore
 import configfile
 from configfile import Config
-
-ModuleCore.init_data()
-
-try:
-	uid = os.geteuid()
-except:
-	uid = -1
+import hgpoll as HgPoll
+import bt as Bt
 
-if uid == 0 and raw_input ('Do you seriously want to run cobalt as root? [y/N] ') != 'y':
-	quit()
+g_BotActive = False
 
-configfile.init()
-g_admins = Config.get_value ('admins', default=[])
-g_mynick = Config.get_value ('nickname', default='cobalt')
-g_BotActive = False
-g_needCommitsTxtRebuild = True
+def main():
+	ModuleCore.init_data()
 
-#
-# SOAP stuff
-#
-suds_active = False
+	try:
+		uid = os.geteuid()
+	except:
+		uid = -1
 
-try:
-	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
-
-btannounce_active = False
-btannounce_timeout = 0
-
-def bt_updatechecktimeout():
-	global btannounce_timeout
-	btannounce_timeout = time.time() + (Config.get_node ('bt').get_value ('checkinterval', default=5) * 60)
+	if uid == 0 and raw_input ('Do you seriously want to run cobalt as root? [y/N] ') != 'y':
+		quit()
 
-def bt_credentials():
-	bt = Config.get_node ('bt')
-	user = bt.get_value ('username', '')
-	password = bt.get_value ('password', '')
-	return [user, password]
-
-if suds_active:
-	sys.stdout.write ('Retrieving latest tracker ticket... ')
-	user, password = bt_credentials()
-	btannounce_id = suds_client.service.mc_issue_get_biggest_id (user, password, 0)
-	btannounce_active = True
-	bt_updatechecktimeout()
-	print btannounce_id
-
-def bt_getissue(ticket):
-	global suds_client
-	user, password = bt_credentials()
-	return suds_client.service.mc_issue_get (user, password, ticket)
-
-def bt_checklatest():
-	global btannounce_timeout
-	global btannounce_id
-
-	if time.time() >= btannounce_timeout:
-		bt_updatechecktimeout()
-		newid = btannounce_id
-		try:
-			user, password = bt_credentials()
-			newid = suds_client.service.mc_issue_get_biggest_id (user, password, 0)
-		except Exception as e:
-			pass
-
-		while newid > btannounce_id:
-			try:
-				btannounce_id += 1
-				data = bt_getissue (btannounce_id)
-
-				for client in g_clients:
-					client.announce_ticket (data)
-			except Exception as e:
-				pass
+	configfile.init()
+	g_admins = Config.get_value ('admins', default=[])
+	g_mynick = Config.get_value ('nickname', default='cobalt')
+	
+	try:
+		autoconnects = Config.get_value ('autoconnect', [])
+		
+		if len (autoconnects) == 0:
+			print "Nowhere to connect."
+			quit()
+		
+		for aconn in autoconnects:
+			for conndata in Config.get_nodelist ('connections'):
+				if conndata.get_value ('name') == aconn:
+					irc_client (conndata, 0)
+					break
+			else:
+				raise ValueError ("unknown autoconnect entry %s" % (aconn))
+		
+		g_BotActive = True
+		asyncore.loop()
+	except KeyboardInterrupt:
+		for client in all_clients:
+			client.keyboardinterrupt()
+		quit()
 
 #
 # irc_client flags
@@ -132,7 +93,7 @@
 #
 # List of all clients
 #
-g_clients = []
+all_clients = []
 
 class channel (object):
 	name = ""
@@ -145,7 +106,7 @@
 # Prints a line to log channel(s)
 #
 def chanlog (line):
-	for client in g_clients:
+	for client in all_clients:
 		if not client.flags & CLIF_CONNECTED:
 			continue
 
@@ -165,7 +126,7 @@
 			print line
 			chanlog (line)
 
-	for client in g_clients:
+	for client in all_clients:
 		if len(data) > 0:
 			client.exceptdie()
 		else:
@@ -193,388 +154,6 @@
 	python = sys.executable
 	os.execl (python, python, * sys.argv)
 
-def make_commits_txt():
-	global g_needCommitsTxtRebuild
-
-	if g_needCommitsTxtRebuild == False:
-		return
-
-	print 'Building commits.txt...'
-	# Update zandronum-everything
-	repo = hgapi.Repo ('zandronum-everything')
-	repo.hg_command ('pull', '../zandronum-sandbox')
-	repo.hg_command ('pull', '../zandronum-sandbox-stable')
-
-	data = repo.hg_command ('log', '--template', '{node} {date|hgdate}\n')
-
-	f = open ('commits.txt', 'w')
-
-	for line in data.split ('\n'):
-		if line == '':
-			continue
-
-		words = line.split (' ')
-		timestamp = int (words[1])
-		f.write ('%s %s\n' % (words[0], datetime.utcfromtimestamp (timestamp).strftime ('%y%m%d-%H%M')))
-	f.close()
-	g_needCommitsTxtRebuild = False
-#enddef
-
-' Check if a repository exists '
-def check_repo_exists (repo_name, repo_owner):
-	print 'Checking that %s exists...' % repo_name
-	repo_url = 'https://bitbucket.org/%s/%s' % (repo_owner, repo_name)
-	zanrepo = hgapi.Repo (repo_name)
-
-	try:
-		zanrepo.hg_command ('id', '.')
-	except hgapi.hgapi.HgException:
-		# If the repo does not exist, clone it. zandronum-everything can be spawned off other repos though
-		if repo_name == 'zandronum-everything':
-			if not os.path.exists (repo_name):
-				os.makedirs (repo_name)
-
-			global g_needCommitsTxtRebuild
-			g_needCommitsTxtRebuild = True
-			print 'Init %s' % repo_name
-			zanrepo.hg_command ('init')
-			print 'Cloning zandronum-sandbox into %s' % repo_name
-			zanrepo.hg_command ('pull', '../zandronum-sandbox')
-			print 'Cloning zandronum-sandbox-stable into %s' % repo_name
-			zanrepo.hg_command ('pull', '../zandronum-sandbox-stable')
-			print 'Done'
-			make_commits_txt()
-			return
-		#fi
-
-		try:
-			print 'Cloning %s...' % repo_name
-			zanrepo.hg_clone (repo_url, repo_name)
-			print 'Cloning done.'
-		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', 'Torr_Samaho')
-check_repo_exists ('zandronum-stable', 'Torr_Samaho')
-check_repo_exists ('zandronum-sandbox', 'crimsondusk')
-check_repo_exists ('zandronum-sandbox-stable', 'crimsondusk')
-check_repo_exists ('zandronum-everything', '')
-
-repocheck_timeout = (time.time()) + 15
-
-def get_commit_data (zanrepo, rev, template):
-	return zanrepo.hg_command ('log', '-l', '1', '-r', rev, '--template', template)
-#enddef
-
-def decipher_hgapi_error (e):
-	# Blah, hgapi, why must your error messages be so mangled?
-	try:
-		rawmsg = e.message.replace('\n', '').replace('" +','').replace('\t','')
-		errmsg = re.compile (r'.*: tErr: (.*)Out:.*').match (rawmsg).group (1)
-		return [True, errmsg]
-	except:
-		return [False, '']
-	#endtry
-#enddef
-
-def bbcodify(commit_diffstat):
-	result=''
-	for line in commit_diffstat.split('\n'):
-		# Add green color-tags for the ++++++++++ stream
-		rex = re.compile (r'^(.*)\|(.*) (\+*)(-*)(.*)$')
-		match = rex.match (line)
-		if match:
-			line = '%s|%s [color=#5F7]%s[/color][color=#F53]%s[/color]%s\n' \
-				% (match.group (1), match.group (2), match.group (3), match.group (4), match.group (5))
-
-			# Tracker doesn't seem to like empty color tags
-			line = line.replace ('[color=#5F7][/color]', '').replace ('[color=#F53][/color]', '')
-		#else:
-			#rex = re.compile (r'^(.*) ([0-9]+) insertions\(\+\), ([0-9]+) deletions\(\-\)$')
-			#match = rex.match (line)
-			#if match:
-				#line = '%s [b][color=green]%s[/color][/b] insertions, [b][color=red]%s[/color][/b] deletions\n' \
-					#% (match.group (1), match.group (2), match.group (3))
-
-		result += line
-	#done
-
-	return result
-#enddef
-
-def find_developer_by_email (commit_email):
-	for developer, emails in Config.get_value ('developer_emails', default={}).iteritems():
-		for email in emails:
-			if commit_email == email:
-				return developer
-			#fi
-		#done
-	#done
-
-	return ''
-#enddef
-
-' Retrieves and processes commits for zandronum repositories '
-' Ensure both repositories are OK before using this! '
-def process_zan_repo_updates():
-	for n in ['zandronum-stable', 'zandronum', 'zandronum-sandbox', 'zandronum-sandbox-stable']:
-		process_one_repo (n)
-
-def process_one_repo (repo_name):
-	global repocheck_timeout
-	global suds_client
-	global g_clients
-	
-	hgns = Config.get_node ('hg')
-	
-	if not hgns.get_value ('track', default=True):
-		return
-
-	usestable = repo_name == 'zandronum-stable'
-	usesandbox = repo_name == 'zandronum-sandbox' or repo_name == 'zandronum-sandbox-stable'
-	repo_owner = 'Torr_Samaho' if not usesandbox else 'crimsondusk'
-	repo_url = 'https://bitbucket.org/%s/%s' % (repo_owner, repo_name)
-	num_commits = 0
-	btuser, btpassword = bt_credentials()
-
-	if time.time() < repocheck_timeout:
-		return
-
-	repocheck_timeout = time.time() + hgns.get_value ('checkinterval', default=15) * 60
-	zanrepo = hgapi.Repo (repo_name)
-	commit_data = []
-	delimeter = '@@@@@@@@@@'
-
-	try:
-		data = zanrepo.hg_command ('incoming', '--quiet', '--template',
-			'{node|short} {desc}' + delimeter)
-	except hgapi.hgapi.HgException as e:
-		deciphered = decipher_hgapi_error (e)
-
-		if deciphered[0] and len(deciphered[1]) > 0:
-			chanlog ("error while using hg import on %s: %s" % (repo_name, deciphered[1]))
-		#fi
-
-		return
-	except Exception as e:
-		chanlog ("%s" % `e`)
-		return
-	#tried
-
-	for line in data.split (delimeter):
-		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
-
-		commit_node = match.group (1)
-		commit_message = match.group (2)
-		commit_data.append ([commit_node, commit_message])
-	#done
-
-	if len (commit_data) > 0:
-		pull_args = [];
-
-		for commit in commit_data:
-			pull_args.append ('-r');
-			pull_args.append (commit[0]);
-		#done
-
-		try:
-			zanrepo.hg_command ('pull', *pull_args)
-
-			# Also pull these commits to the zandronum main repository
-			if usestable:
-				devrepo = hgapi.Repo ('zandronum')
-				devrepo.hg_command ('pull', '../zandronum-stable', *pull_args)
-			#fi
-
-			# Pull everything into sandboxes too
-			if not usesandbox:
-				devrepo = hgapi.Repo ('zandronum-sandbox')
-				devrepo.hg_command ('pull', '../%s' % repo_name, *pull_args)
-
-				devrepo = hgapi.Repo ('zandronum-sandbox-stable')
-				devrepo.hg_command ('pull', '../%s' % repo_name, *pull_args)
-			#fi
-
-			devrepo = hgapi.Repo ('zandronum-everything')
-			devrepo.hg_command ('pull', '../%s' % repo_name, *pull_args)
-
-			global g_needCommitsTxtRebuild
-			g_needCommitsTxtRebuild = True
-		except Exception as e:
-			chanlog ('Warning: unable to pull: %s' % `e`)
-			return
-		#tried
-	#fi
-
-	for commit in commit_data:
-		commit_node = commit[0]
-		commit_message = commit[1]
-
-		try:
-			if usesandbox:
-				commit_author = get_commit_data (zanrepo, commit_node, '{author}')
-				commit_url = '%s/commits/%s' % (repo_url, commit_node)
-				commit_email = ''
-
-				# 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
-
-				commit_trackeruser = find_developer_by_email (commit_email)
-				committer = commit_trackeruser if commit_trackeruser != '' else commit_author
-
-				for irc_client in g_clients:
-					for channel in irc_client.cfg['channels']:
-						if 'btprivate' in channel and channel['btprivate'] == True:
-							irc_client.privmsg (channel['name'],
-								"%s: new commit %s by %s: %s"
-								% (repo_name, commit_node, committer, commit_url))
-
-							for line in commit_message.split ('\n'):
-								irc_client.privmsg (channel['name'], line)
-						#fi
-					#done
-				#done
-
-				num_commits += 1
-				continue
-			#fi
-
-			rex = re.compile (r'^.*(fixes|resolves|addresses|should fix) ([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_commit_data (zanrepo, commit_node,
-				'{author|nonempty}\n{date(date, \'%A %d %B %Y %T\')}').split('\n')
-
-			if len (moredata) != 2:
-				chanlog ('error while processing %s: malformed hg data' % commit_node)
-				continue
-			#fi
-
-			commit_author = moredata[0]
-			commit_date = moredata[1]
-			commit_email = ""
-
-			try:
-				ticket_data = suds_client.service.mc_issue_get (btuser, btpassword, ticket_id)
-			except Exception as e:
-				chanlog ('error while processing %s: %s' % (commit_node, `e`))
-				continue
-			#tried
-
-			# 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
-
-			commit_diffstat = zanrepo.hg_command ('diff', '--change', commit_node, '--stat')
-
-			if len(commit_diffstat) > 0:
-				# commit_diffstat = 'Changes in files:\n[code]\n' + commit_diffstat + '\n[/code]'
-				commit_diffstat = 'Changes in files:\n' + bbcodify(commit_diffstat)
-			else:
-				commit_diffstat = 'No changes in files.'
-
-			# Compare the email addresses against known developer usernames
-			commit_trackeruser = find_developer_by_email (commit_email)
-
-			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)
-
-			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
-
-			# Set target version if not set
-			if not 'target_version' in ticket_data:
-				ticket_data['target_version'] = '1.4' if repo_name == 'zandronum-stable' else '2.0'
-				need_update = True
-			elif (ticket_data['target_version'] == '2.0' or ticket_data['target_version'] == '2.0-beta') \
-			and repo_name == 'zandronum-stable':
-				# Target version was 2.0 but this was just committed to zandronum-stable, adjust
-				ticket_data['target_version'] = '1.4'
-				need_update = True
-			elif ticket_data['target_version'] == '2.0-beta':
-				# Fix target version from 2.0-beta to 2.0
-				ticket_data['target_version'] = '2.0'
-				need_update = True
-			#fi
-
-			# Announce on IRC
-			for irc_client in g_clients:
-				for channel in irc_client.channels:
-					if channel.get_value ('btannounce', default=True):
-						irc_client.privmsg (channel.get_value ('name'),
-							"%s: commit %s fixes issue %d: %s"
-							% (repo_name, commit_node, ticket_id, commit_message))
-						irc_client.privmsg (channel.get_value ('name'),
-							"Read all about it here: " + irc_client.get_ticket_url (ticket_id))
-					#fi
-				#done
-			#done
-
-			if need_update:
-				# We need to remove the note data, otherwise the ticket notes
-				# will get unnecessary updates. WTF, MantisBT?
-				ticket_data.notes = []
-				suds_client.service.mc_issue_update (btuser, btpassword, ticket_id, ticket_data)
-			#fi
-
-			suds_client.service.mc_issue_note_add (btuser, btpassword, ticket_id, { 'text': message })
-			num_commits += 1
-		except Exception as e:
-			chanlog ('Error while processing %s: %s' % (commit_node, `e`))
-			continue
-		#tried
-	#done
-#enddef
-
 def plural (a):
 	return '' if a == 1 else 's'
 
@@ -597,7 +176,7 @@
 		self.verbose = Config.get_value ('verbose', default=False)
 		self.commandprefix = Config.get_value ('commandprefix', default='.')
 
-		g_clients.append (self)
+		all_clients.append (self)
 		asyncore.dispatcher.__init__ (self)
 		self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
 		self.connect ((self.host, self.port))
@@ -684,7 +263,7 @@
 			bt_checklatest()
 
 			# Check for new commits in the repositories
-			process_zan_repo_updates()
+			HgPoll.poll()
 
 	def channel_by_name (self, name):
 		for channel in self.channels:
@@ -729,59 +308,6 @@
 		else:
 			chanlog ("Recieved bad PRIVMSG: %s" % line)
 
-	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-	#
-	#	Get the URL for a specified ticket
-	#
-	def get_ticket_url (self, ticket):
-		url = Config.get_node ('bt').get_value ('url')
-		return 'https://%s/view.php?id=%s' % (url, ticket)
-
-	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-	#
-	#	Retrieve a ticket from mantisbt
-	#
-	def get_ticket_data (self, replyto, ticket, withlink):
-		if suds_active == False:
-			return
-
-		data = {}
-		try:
-			data = bt_getissue (ticket)
-		except Exception, e:
-			self.privmsg (replyto, "Failed to get info for issue %s: %s" % (ticket, `e`))
-
-		if data:
-			if data['view_state']['name'] == 'private':
-				allowprivate = False
-
-				for channel in self.channels:
-					if channel.get_value ('name') == replyto and channel.get_value ('btprivate', False):
-						allowprivate = True
-						break
-					#fi
-				#done
-
-				if not allowprivate:
-					self.privmsg (replyto, 'Error: ticket %s is private' % ticket)
-					return
-				#fi
-			#fi
-
-			self.privmsg (replyto, "Issue %s: %s: Reporter: %s, assigned to: %s, status: %s (%s)" % \
-				(ticket, \
-				data.summary, \
-				data.reporter.name if hasattr (data.reporter, 'name') else "<unknown>", \
-				data.handler.name if hasattr (data, 'handler') else "nobody", \
-				data.status.name, \
-				data.resolution.name))
-
-			if withlink:
-				self.privmsg (replyto, "Read all about it here: " + self.get_ticket_url (ticket))
-			#fi
-		#fi
-	#enddef
-
 	def is_admin (self, ident, host):
 		return ("%s@%s" % (ident, host)) in g_admins
 
@@ -806,21 +332,7 @@
 			return
 		#tried
 
-		if command == 'ticket':
-			if len(args) != 1:
-				raise logical_exception ("usage: .%s <ticket>" % command)
-			self.get_ticket_data (replyto, args[0], True)
-		elif command == 'testannounce':
-			check_admin (sender, ident, host, command)
-			if len(args) != 1:
-				raise logical_exception ("usage: .%s <ticket>" % command)
-			self.announce_ticket (bt_getissue (args[0]))
-		elif command == 'checkhg':
-			check_admin (sender, ident, host, command)
-			global repocheck_timeout
-			repocheck_timeout = 0
-			process_zan_repo_updates()
-		elif command == 'die':
+		if command == 'die':
 			check_admin (sender, ident, host, command)
 			quit()
 		elif command == 'convert':
@@ -872,138 +384,9 @@
 				raise e
 			except Exception as e:
 				raise logical_exception ('Urban dictionary lookup failed: %s' % `e`)
-		elif command == 'hg':
-			check_admin (sender, ident, host, command)
-
-			if len(args) < 2:
-				raise logical_exception ('usage: %s <repo> <command...>' % command)
-
-			try:
-				repo = hgapi.Repo (args[0])
-				result = repo.hg_command (*args[1:])
-				self.privmsg (replyto, result)
-			except hgapi.hgapi.HgException as e:
-				result = decipher_hgapi_error (e)
-
-				if result[0]:
-					self.privmsg (replyto, 'error: %s' % result[1])
-				else:
-					self.privmsg (replyto, 'error: %s' % `e`)
-				#fi
-			#tried
-		elif command == 'changeset' or command == 'cset' or command == 'rev':
-			if len(args) != 1:
-				raise logical_exception ('usage: %s <changeset>' % command)
-
-			repo = hgapi.Repo ('zandronum-everything')
-			data = ""
-			node = args[0]
-
-			# Possibly we're passed a date version instead. Try find the node for this.
-			try:
-				datetime.strptime (args[0], '%y%m%d-%H%M')
-				make_commits_txt()
-				commits_txt = open ('commits.txt', 'r')
-
-				for line in commits_txt:
-					data = line.replace ('\n', '').split (' ')
-					if data[1] == args[0]:
-						node = data[0]
-						break
-				else:
-					self.privmsg (replyto, 'couldn\'t find changset for date %s' % args[0])
-					return
-				#done
-			except ValueError:
-				pass
-			#tried
-
-			# The sandboxes contain all revisions in zandronum and zandronum-stable.
-			# Thus we only need to try find the revision in the sandbox repos.
-			try:
-				repo.hg_command ("log", "-r", node, "--template", " ")
-			except hgapi.hgapi.HgException:
-				self.privmsg (replyto, 'couldn\'t find changeset %s' % (node))
-				return
-			#tried
-
-			try:
-				data = repo.hg_command ("log", "-r", node, "--template",
-					"{node|short}@@@@@@@{desc}@@@@@@@{author}@@@@@@@{diffstat}@@@@@@@{date|hgdate}")
-				data = data.split ('@@@@@@@')
-
-				node = data[0]
-				message = data[1]
-				author = data[2]
-				diffstat = data[3]
-				date = datetime.utcfromtimestamp (int (data[4].split (' ')[0]))
-				delta = datetime.now() - date
-				datestring = ''
-
-				# Remove the email address from the author if possible
-				match = re.compile (r'^(.+) <([^>]+)>$.*').match (author)
-				if match:
-					author = match.group (1)
-					email = match.group (2)
-
-				username = find_developer_by_email (email)
-
-				if username != '':
-					author = username
-
-				if delta.days < 4:
-					if delta.days == 0:
-						if delta.seconds < 60:
-							datestring = 'just now'
-						elif delta.seconds < 3600:
-							minutes = delta.seconds / 60
-							datestring = '%d minute%s ago' % (minutes, plural (minutes))
-						else:
-							hours = delta.seconds / 3600
-							datestring = '%d hour%s ago' % (hours, plural (hours))
-					else:
-						datestring = '%d day%s ago' % (delta.days, plural (delta.days))
-				else:
-					datestring = 'on %s' % (str (date))
-				#fi
-
-				self.privmsg (replyto, 'changeset %s (%s): committed by %s %s (%s)' % \
-					(node, date.strftime ('%y%m%d-%H%M'), author, datestring, diffstat))
-
-				for line in message.split ('\n'):
-					self.privmsg (replyto, '    ' + line)
-			except hgapi.hgapi.HgException as e:
-				result = decipher_hgapi_error (e)
-
-				if result[0]:
-					self.privmsg (replyto, 'error: %s' % result[1])
-				else:
-					self.privmsg (replyto, 'error: %s' % `e`)
-			#tried
 #		else:
 #			raise logical_exception ("unknown command `.%s`" % command)
 
-	#
-	# Print a ticket announce to appropriate channels
-	#
-	def announce_ticket (self, data):
-		idstring = "%d" % data.id
-		while len(idstring) < 7:
-			idstring = "0" + idstring
-
-		isprivate = data['view_state']['name'] == 'private'
-		reporter = data['reporter']['name'] if hasattr (data['reporter'], 'name') else '<nobody>'
-
-		for channel in self.channels:
-			if channel.get_value ('btannounce', False):
-				if not isprivate or (channel.get_value ('btprivate', False)):
-					self.write ("PRIVMSG %s :[%s] New issue %s, reported by %s: %s: %s" % \
-						(channel['name'], data['project']['name'], idstring, reporter,
-						data['summary'], self.get_ticket_url (idstring)))
-				#fi
-			#fi
-		#done
-
 	def handle_error(self):
 		excepterm (traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))
 
@@ -1028,27 +411,5 @@
 	def keyboardinterrupt (self):
 		self.close_connection ('KeyboardInterrupt')
 
-#
-# Main procedure:
-#
-try:
-	autoconnects = Config.get_value ('autoconnect', [])
-	
-	if len (autoconnects) == 0:
-		print "Nowhere to connect."
-		quit()
-	
-	for aconn in autoconnects:
-		for conndata in Config.get_nodelist ('connections'):
-			if conndata.get_value ('name') == aconn:
-				irc_client (conndata, 0)
-				break
-		else:
-			raise ValueError ("unknown autoconnect entry %s" % (aconn))
-	
-	g_BotActive = True
-	asyncore.loop()
-except KeyboardInterrupt:
-	for client in g_clients:
-		client.keyboardinterrupt()
-	quit()
+if __name__ == '__main__':
+	main()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgpoll.py	Sun Nov 09 19:59:10 2014 +0200
@@ -0,0 +1,388 @@
+impot hgapi
+from configfile import Config
+g_needCommitsTxtRebuild = True
+
+def make_commits_txt():
+	global g_needCommitsTxtRebuild
+
+	if g_needCommitsTxtRebuild == False:
+		return
+
+	print 'Building commits.txt...'
+	# Update zandronum-everything
+	repo = hgapi.Repo ('zandronum-everything')
+	repo.hg_command ('pull', '../zandronum-sandbox')
+	repo.hg_command ('pull', '../zandronum-sandbox-stable')
+
+	data = repo.hg_command ('log', '--template', '{node} {date|hgdate}\n')
+
+	f = open ('commits.txt', 'w')
+
+	for line in data.split ('\n'):
+		if line == '':
+			continue
+
+		words = line.split (' ')
+		timestamp = int (words[1])
+		f.write ('%s %s\n' % (words[0], datetime.utcfromtimestamp (timestamp).strftime ('%y%m%d-%H%M')))
+	f.close()
+	g_needCommitsTxtRebuild = False
+#enddef
+
+' Check if a repository exists '
+def check_repo_exists (repo_name, repo_owner):
+	print 'Checking that %s exists...' % repo_name
+	repo_url = 'https://bitbucket.org/%s/%s' % (repo_owner, repo_name)
+	zanrepo = hgapi.Repo (repo_name)
+
+	try:
+		zanrepo.hg_command ('id', '.')
+	except hgapi.hgapi.HgException:
+		# If the repo does not exist, clone it. zandronum-everything can be spawned off other repos though
+		if repo_name == 'zandronum-everything':
+			if not os.path.exists (repo_name):
+				os.makedirs (repo_name)
+
+			global g_needCommitsTxtRebuild
+			g_needCommitsTxtRebuild = True
+			print 'Init %s' % repo_name
+			zanrepo.hg_command ('init')
+			print 'Cloning zandronum-sandbox into %s' % repo_name
+			zanrepo.hg_command ('pull', '../zandronum-sandbox')
+			print 'Cloning zandronum-sandbox-stable into %s' % repo_name
+			zanrepo.hg_command ('pull', '../zandronum-sandbox-stable')
+			print 'Done'
+			make_commits_txt()
+			return
+		#fi
+
+		try:
+			print 'Cloning %s...' % repo_name
+			zanrepo.hg_clone (repo_url, repo_name)
+			print 'Cloning done.'
+		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', 'Torr_Samaho')
+check_repo_exists ('zandronum-stable', 'Torr_Samaho')
+check_repo_exists ('zandronum-sandbox', 'crimsondusk')
+check_repo_exists ('zandronum-sandbox-stable', 'crimsondusk')
+check_repo_exists ('zandronum-everything', '')
+
+repocheck_timeout = (time.time()) + 15
+
+def get_commit_data (zanrepo, rev, template):
+	return zanrepo.hg_command ('log', '-l', '1', '-r', rev, '--template', template)
+#enddef
+
+def decipher_hgapi_error (e):
+	# Blah, hgapi, why must your error messages be so mangled?
+	try:
+		rawmsg = e.message.replace('\n', '').replace('" +','').replace('\t','')
+		errmsg = re.compile (r'.*: tErr: (.*)Out:.*').match (rawmsg).group (1)
+		return [True, errmsg]
+	except:
+		return [False, '']
+	#endtry
+#enddef
+
+def bbcodify(commit_diffstat):
+	result=''
+	for line in commit_diffstat.split('\n'):
+		# Add green color-tags for the ++++++++++ stream
+		rex = re.compile (r'^(.*)\|(.*) (\+*)(-*)(.*)$')
+		match = rex.match (line)
+		if match:
+			line = '%s|%s [color=#5F7]%s[/color][color=#F53]%s[/color]%s\n' \
+				% (match.group (1), match.group (2), match.group (3), match.group (4), match.group (5))
+
+			# Tracker doesn't seem to like empty color tags
+			line = line.replace ('[color=#5F7][/color]', '').replace ('[color=#F53][/color]', '')
+		#else:
+			#rex = re.compile (r'^(.*) ([0-9]+) insertions\(\+\), ([0-9]+) deletions\(\-\)$')
+			#match = rex.match (line)
+			#if match:
+				#line = '%s [b][color=green]%s[/color][/b] insertions, [b][color=red]%s[/color][/b] deletions\n' \
+					#% (match.group (1), match.group (2), match.group (3))
+
+		result += line
+	#done
+
+	return result
+#enddef
+
+def find_developer_by_email (commit_email):
+	for developer, emails in Config.get_value ('developer_emails', default={}).iteritems():
+		for email in emails:
+			if commit_email == email:
+				return developer
+			#fi
+		#done
+	#done
+
+	return ''
+#enddef
+
+' Retrieves and processes commits for zandronum repositories '
+' Ensure both repositories are OK before using this! '
+def poll():
+	for n in ['zandronum-stable', 'zandronum', 'zandronum-sandbox', 'zandronum-sandbox-stable']:
+		process_one_repo (n)
+
+def process_one_repo (repo_name):
+	global repocheck_timeout
+	global g_clients
+	
+	hgns = Config.get_node ('hg')
+	
+	if not hgns.get_value ('track', default=True):
+		return
+
+	usestable = repo_name == 'zandronum-stable'
+	usesandbox = repo_name == 'zandronum-sandbox' or repo_name == 'zandronum-sandbox-stable'
+	repo_owner = 'Torr_Samaho' if not usesandbox else 'crimsondusk'
+	repo_url = 'https://bitbucket.org/%s/%s' % (repo_owner, repo_name)
+	num_commits = 0
+	btuser, btpassword = bt_credentials()
+
+	if time.time() < repocheck_timeout:
+		return
+
+	repocheck_timeout = time.time() + hgns.get_value ('checkinterval', default=15) * 60
+	zanrepo = hgapi.Repo (repo_name)
+	commit_data = []
+	delimeter = '@@@@@@@@@@'
+
+	try:
+		data = zanrepo.hg_command ('incoming', '--quiet', '--template',
+			'{node|short} {desc}' + delimeter)
+	except hgapi.hgapi.HgException as e:
+		deciphered = decipher_hgapi_error (e)
+
+		if deciphered[0] and len(deciphered[1]) > 0:
+			chanlog ("error while using hg import on %s: %s" % (repo_name, deciphered[1]))
+		#fi
+
+		return
+	except Exception as e:
+		chanlog ("%s" % `e`)
+		return
+	#tried
+
+	for line in data.split (delimeter):
+		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
+
+		commit_node = match.group (1)
+		commit_message = match.group (2)
+		commit_data.append ([commit_node, commit_message])
+	#done
+
+	if len (commit_data) > 0:
+		pull_args = [];
+
+		for commit in commit_data:
+			pull_args.append ('-r');
+			pull_args.append (commit[0]);
+		#done
+
+		try:
+			zanrepo.hg_command ('pull', *pull_args)
+
+			# Also pull these commits to the zandronum main repository
+			if usestable:
+				devrepo = hgapi.Repo ('zandronum')
+				devrepo.hg_command ('pull', '../zandronum-stable', *pull_args)
+			#fi
+
+			# Pull everything into sandboxes too
+			if not usesandbox:
+				devrepo = hgapi.Repo ('zandronum-sandbox')
+				devrepo.hg_command ('pull', '../%s' % repo_name, *pull_args)
+
+				devrepo = hgapi.Repo ('zandronum-sandbox-stable')
+				devrepo.hg_command ('pull', '../%s' % repo_name, *pull_args)
+			#fi
+
+			devrepo = hgapi.Repo ('zandronum-everything')
+			devrepo.hg_command ('pull', '../%s' % repo_name, *pull_args)
+
+			global g_needCommitsTxtRebuild
+			g_needCommitsTxtRebuild = True
+		except Exception as e:
+			chanlog ('Warning: unable to pull: %s' % `e`)
+			return
+		#tried
+	#fi
+
+	for commit in commit_data:
+		commit_node = commit[0]
+		commit_message = commit[1]
+
+		try:
+			if usesandbox:
+				commit_author = get_commit_data (zanrepo, commit_node, '{author}')
+				commit_url = '%s/commits/%s' % (repo_url, commit_node)
+				commit_email = ''
+
+				# 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
+
+				commit_trackeruser = find_developer_by_email (commit_email)
+				committer = commit_trackeruser if commit_trackeruser != '' else commit_author
+
+				for irc_client in g_clients:
+					for channel in irc_client.cfg['channels']:
+						if 'btprivate' in channel and channel['btprivate'] == True:
+							irc_client.privmsg (channel['name'],
+								"%s: new commit %s by %s: %s"
+								% (repo_name, commit_node, committer, commit_url))
+
+							for line in commit_message.split ('\n'):
+								irc_client.privmsg (channel['name'], line)
+						#fi
+					#done
+				#done
+
+				num_commits += 1
+				continue
+			#fi
+
+			rex = re.compile (r'^.*(fixes|resolves|addresses|should fix) ([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_commit_data (zanrepo, commit_node,
+				'{author|nonempty}\n{date(date, \'%A %d %B %Y %T\')}').split('\n')
+
+			if len (moredata) != 2:
+				chanlog ('error while processing %s: malformed hg data' % commit_node)
+				continue
+			#fi
+
+			commit_author = moredata[0]
+			commit_date = moredata[1]
+			commit_email = ""
+
+			try:
+				ticket_data = suds_client.service.mc_issue_get (btuser, btpassword, ticket_id)
+			except Exception as e:
+				chanlog ('error while processing %s: %s' % (commit_node, `e`))
+				continue
+			#tried
+
+			# 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
+
+			commit_diffstat = zanrepo.hg_command ('diff', '--change', commit_node, '--stat')
+
+			if len(commit_diffstat) > 0:
+				# commit_diffstat = 'Changes in files:\n[code]\n' + commit_diffstat + '\n[/code]'
+				commit_diffstat = 'Changes in files:\n' + bbcodify(commit_diffstat)
+			else:
+				commit_diffstat = 'No changes in files.'
+
+			# Compare the email addresses against known developer usernames
+			commit_trackeruser = find_developer_by_email (commit_email)
+
+			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)
+
+			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
+
+			# Set target version if not set
+			if not 'target_version' in ticket_data:
+				ticket_data['target_version'] = '1.4' if repo_name == 'zandronum-stable' else '2.0'
+				need_update = True
+			elif (ticket_data['target_version'] == '2.0' or ticket_data['target_version'] == '2.0-beta') \
+			and repo_name == 'zandronum-stable':
+				# Target version was 2.0 but this was just committed to zandronum-stable, adjust
+				ticket_data['target_version'] = '1.4'
+				need_update = True
+			elif ticket_data['target_version'] == '2.0-beta':
+				# Fix target version from 2.0-beta to 2.0
+				ticket_data['target_version'] = '2.0'
+				need_update = True
+			#fi
+
+			# Announce on IRC
+			for irc_client in g_clients:
+				for channel in irc_client.channels:
+					if channel.get_value ('btannounce', default=True):
+						irc_client.privmsg (channel.get_value ('name'),
+							"%s: commit %s fixes issue %d: %s"
+							% (repo_name, commit_node, ticket_id, commit_message))
+						irc_client.privmsg (channel.get_value ('name'),
+							"Read all about it here: " + irc_client.get_ticket_url (ticket_id))
+					#fi
+				#done
+			#done
+
+			if need_update:
+				# We need to remove the note data, otherwise the ticket notes
+				# will get unnecessary updates. WTF, MantisBT?
+				ticket_data.notes = []
+				suds_client.service.mc_issue_update (btuser, btpassword, ticket_id, ticket_data)
+			#fi
+
+			suds_client.service.mc_issue_note_add (btuser, btpassword, ticket_id, { 'text': message })
+			num_commits += 1
+		except Exception as e:
+			chanlog ('Error while processing %s: %s' % (commit_node, `e`))
+			continue
+		#tried
+	#done
+#enddef
+
+def force_poll():
+	repocheck_timeout = 0
+	poll()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_bt.py	Sun Nov 09 19:59:10 2014 +0200
@@ -0,0 +1,71 @@
+import bt as Bt
+
+ModuleData = {
+	'commands':
+	[
+		{
+			'name': 'ticket',
+			'description': 'Gets ticket info',
+			'args': '<ticket>',
+			'level': 'normal',
+		},
+
+		{
+			'name': 'testannounce',
+			'description': 'Tests the ticket announcer',
+			'args': '<recipient> <message...>',
+			'level': 'admin',
+		},
+	]
+}
+
+def get_ticket_data (bot, replyto, ticket, withlink):
+	if suds_active == False:
+		return
+
+	data = {}
+	try:
+		data = bt_getissue (ticket)
+	except Exception, e:
+		bot.privmsg (replyto, "Failed to get info for issue %s: %s" % (ticket, `e`))
+
+	if data:
+		if data['view_state']['name'] == 'private':
+			allowprivate = False
+
+			for channel in bot.channels:
+				if channel.get_value ('name') == replyto and channel.get_value ('btprivate', False):
+					allowprivate = True
+					break
+				#fi
+			#done
+
+			if not allowprivate:
+				bot.privmsg (replyto, 'Error: ticket %s is private' % ticket)
+				return
+			#fi
+		#fi
+
+		bot.privmsg (replyto, "Issue %s: %s: Reporter: %s, assigned to: %s, status: %s (%s)" % \
+			(ticket, \
+			data.summary, \
+			data.reporter.name if hasattr (data.reporter, 'name') else "<unknown>", \
+			data.handler.name if hasattr (data, 'handler') else "nobody", \
+			data.status.name, \
+			data.resolution.name))
+
+		if withlink:
+			bot.privmsg (replyto, "Read all about it here: " + bot.get_ticket_url (ticket))
+		#fi
+	#fi
+#enddef
+
+def cmd_ticket (bot, args, **rest):
+	Bt.get_ticket_data (bot, replyto, args['ticket'], True)
+
+def cmd_testannounce (bot, args, **rest);
+elif command == 'testannounce':
+	check_admin (sender, ident, host, command)
+	if len(args) != 1:
+		raise logical_exception ("usage: .%s <ticket>" % command)
+	self.announce_ticket (bt_getissue (args[0]))
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_hgpoll.py	Sun Nov 09 19:59:10 2014 +0200
@@ -0,0 +1,131 @@
+from hgapi import hgapi, Repo
+import hgpoll as HgPoll
+
+ModuleData = {
+	'commands':
+	[
+		{
+			'name': 'checkhg',
+			'description': 'Polls the zandronum repositories for updates',
+			'args': None,
+			'level': 'admin',
+		},
+		{
+			'name': 'cset',
+			'description': 'Yields changeset information (use a hash or date as key)',
+			'args': '<key>',
+			'level': 'normal',
+		},
+		{
+			'name': 'hg',
+			'description': 'Executes a hg command',
+			'args': '<repo> <command...>',
+			'level': 'admin',
+		}
+	]
+}
+
+def cmd_checkhg (bot, **rest):
+	HgPoll.force_poll()
+
+def cmd_cset (bot, args, **rest)
+	repo = Repo ('zandronum-everything')
+	data = ""
+	node = args['key']
+
+	# Possibly we're passed a date version instead. Try find the node for this.
+	try:
+		datetime.strptime (args['key'], '%y%m%d-%H%M')
+		make_commits_txt()
+		commits_txt = open ('commits.txt', 'r')
+
+		for line in commits_txt:
+			data = line.replace ('\n', '').split (' ')
+			if data[1] == args['key']:
+				node = data[0]
+				break
+		else:
+			bot.privmsg (replyto, 'couldn\'t find changset for date %s' % args['key'])
+			return
+		#done
+	except ValueError:
+		pass
+	#tried
+
+	# The sandboxes contain all revisions in zandronum and zandronum-stable.
+	# Thus we only need to try find the revision in the sandbox repos.
+	try:
+		repo.hg_command ("log", "-r", node, "--template", " ")
+	except hgapi.HgException:
+		bot.privmsg (replyto, 'couldn\'t find changeset %s' % (node))
+		return
+	#tried
+
+	try:
+		data = repo.hg_command ("log", "-r", node, "--template",
+			"{node|short}@@@@@@@{desc}@@@@@@@{author}@@@@@@@{diffstat}@@@@@@@{date|hgdate}")
+		data = data.split ('@@@@@@@')
+
+		node = data[0]
+		message = data[1]
+		author = data[2]
+		diffstat = data[3]
+		date = datetime.utcfromtimestamp (int (data[4].split (' ')[0]))
+		delta = datetime.now() - date
+		datestring = ''
+
+		# Remove the email address from the author if possible
+		match = re.compile (r'^(.+) <([^>]+)>$.*').match (author)
+		if match:
+			author = match.group (1)
+			email = match.group (2)
+
+		username = find_developer_by_email (email)
+
+		if username != '':
+			author = username
+
+		if delta.days < 4:
+			if delta.days == 0:
+				if delta.seconds < 60:
+					datestring = 'just now'
+				elif delta.seconds < 3600:
+					minutes = delta.seconds / 60
+					datestring = '%d minute%s ago' % (minutes, plural (minutes))
+				else:
+					hours = delta.seconds / 3600
+					datestring = '%d hour%s ago' % (hours, plural (hours))
+			else:
+				datestring = '%d day%s ago' % (delta.days, plural (delta.days))
+		else:
+			datestring = 'on %s' % (str (date))
+		#fi
+
+		bot.privmsg (replyto, 'changeset %s (%s): committed by %s %s (%s)' % \
+			(node, date.strftime ('%y%m%d-%H%M'), author, datestring, diffstat))
+
+		for line in message.split ('\n'):
+			bot.privmsg (replyto, '    ' + line)
+	except hgapi.HgException as e:
+		result = HgPoll.decipher_hgapi_error (e)
+
+		if result[0]:
+			bot.privmsg (replyto, 'error: %s' % result[1])
+		else:
+			bot.privmsg (replyto, 'error: %s' % `e`)
+	#tried
+
+def cmd_hg (bot, args, **rest):
+	try:
+		repo = hgapi.Repo (args['repo'])
+		result = repo.hg_command (*args['command'])
+		self.privmsg (replyto, result)
+	except hgapi.hgapi.HgException as e:
+		result = HgPoll.decipher_hgapi_error (e)
+
+		if result[0]:
+			self.privmsg (replyto, 'error: %s' % result[1])
+		else:
+			self.privmsg (replyto, 'error: %s' % `e`)
+		#fi
+	#tried
\ No newline at end of file

mercurial