- added commits.db and mercurial support restructure

Sun, 19 Apr 2015 19:45:42 +0300

author
Teemu Piippo <crimsondusk64@gmail.com>
date
Sun, 19 Apr 2015 19:45:42 +0300
changeset 123
aeb0d0788869
parent 122
f899af683bbe
child 124
7b2cd8b1ba86

- added commits.db and mercurial support restructure
- added new, from-scratch calculator for .calc

.hgignore file | annotate | diff | comparison | revisions
bt.py file | annotate | diff | comparison | revisions
calc.py file | annotate | diff | comparison | revisions
commitsdb.py file | annotate | diff | comparison | revisions
hgpoll.py file | annotate | diff | comparison | revisions
irc.py file | annotate | diff | comparison | revisions
mod_hg.py file | annotate | diff | comparison | revisions
mod_util.py file | annotate | diff | comparison | revisions
rest.py file | annotate | diff | comparison | revisions
utility.py file | annotate | diff | comparison | revisions
--- a/.hgignore	Sat Apr 11 21:02:54 2015 +0300
+++ b/.hgignore	Sun Apr 19 19:45:42 2015 +0300
@@ -1,6 +1,7 @@
 syntax:glob
 cobalt*.json*
 untracked
-commits.txt
+commits.db
 *.pyc
 zandronum*
+cobalt.cfg
--- a/bt.py	Sat Apr 11 21:02:54 2015 +0300
+++ b/bt.py	Sun Apr 19 19:45:42 2015 +0300
@@ -100,6 +100,9 @@
 	global btannounce_timeout
 	global btannounce_id
 
+	if not suds_active:
+		return
+
 	if time.time() >= btannounce_timeout:
 		update_checktimeout()
 		newid = btannounce_id
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calc.py	Sun Apr 19 19:45:42 2015 +0300
@@ -0,0 +1,474 @@
+import re
+import cmath
+import math
+import random
+import time
+import operator
+from copy import deepcopy
+
+epsilon = 1e-10
+
+# http://stackoverflow.com/a/2182437
+class Enum (set):
+	def __init__ (self, *args):
+		super (Enum, self).__init__ (args)
+
+	def __getattr__ (self, name):
+		if name in self:
+			return name
+		raise AttributeError
+
+	def __setattr__ (self, name, value):
+		raise AttributeError
+
+class Operator (object):
+	def __init__ (self, name, symbol, operands, priority, function):
+		self.name = name
+		self.symbol = symbol
+		self.operands = operands
+		self.priority = priority
+		self.function = function
+
+	def __str__ (self):
+		return '''<operator %s>''' % self.name
+
+	def __repr__ (self):
+		return self.__str__()
+
+class FunctionCall (object):
+	def __init__ (self, funcname, args):
+		assert (type(args) is list)
+		self.funcname = funcname
+		self.args = args
+
+	def __str__ (self):
+		return '''<function %s (%s)>''' % (self.funcname, self.args)
+
+	def __repr__ (self):
+		return self.__str__()
+
+def do_realf (func, *args):
+	for x in args:
+		if x.imag:
+			raise ValueError ('%s called with a complex number' % func.__name__)
+
+	return func (*[x.real for x in args])
+
+def do_intf (func, *args):
+	for x in args:
+		if x.imag:
+			raise ValueError ('%s called with a complex number' % func.__name__)
+
+		if x.real - math.floor (x.real):
+			raise ValueError ('%s called with a floating point number' % func.__name__)
+
+	return func (*[int (x.real) for x in args])
+
+def realf (func):
+	return lambda *args: do_realf (func, *args)
+
+def intf (func):
+	return lambda *args: do_intf (func, *args)
+
+Operators = []
+OperatorData = {
+	'lneg':		{ 'symbol': '!',  'operands': 1, 'priority': 5, 'function': lambda x: not x },
+	'compl':	{ 'symbol': '~',  'operands': 1, 'priority': 5, 'function': intf (operator.inv) },
+	'neg':		{ 'symbol': '-',  'operands': 1, 'priority': 5, 'function': lambda x: -x },
+	'pow':		{ 'symbol': '**', 'operands': 2, 'priority': 10, 'function': lambda x, y: x ** y },
+	'mul':		{ 'symbol': '*',  'operands': 2, 'priority': 50, 'function': lambda x, y: x * y },
+	'div':		{ 'symbol': '/',  'operands': 2, 'priority': 50, 'function': lambda x, y: x / y },
+	'mod':		{ 'symbol': '%',  'operands': 2, 'priority': 50, 'function': lambda x, y: math.fmod (x, y) },
+	'add':		{ 'symbol': '+',  'operands': 2, 'priority': 100, 'function': lambda x, y: x + y },
+	'sub':		{ 'symbol': '-',  'operands': 2, 'priority': 100, 'function': lambda x, y: x - y },
+	'eq':		{ 'symbol': '==', 'operands': 2, 'priority': 500, 'function': lambda x, y: x == y },
+	'neq':		{ 'symbol': '!=', 'operands': 2, 'priority': 500, 'function': lambda x, y: x != y },
+	'lt':		{ 'symbol': '<',  'operands': 2, 'priority': 500, 'function': lambda x, y: x < y },
+	'lteq':		{ 'symbol': '<=', 'operands': 2, 'priority': 500, 'function': lambda x, y: x <= y },
+	'gt':		{ 'symbol': '>',  'operands': 2, 'priority': 500, 'function': lambda x, y: x > y },
+	'gteq':		{ 'symbol': '>=', 'operands': 2, 'priority': 500, 'function': lambda x, y: x >= y },
+	'btand':	{ 'symbol': '&',  'operands': 2, 'priority': 600, 'function': intf (operator.and_) },
+	'btxor':	{ 'symbol': '^',  'operands': 2, 'priority': 601, 'function': intf (operator.xor) },
+	'btor':		{ 'symbol': '|',  'operands': 2, 'priority': 602, 'function': intf (operator.or_) },
+	'and':		{ 'symbol': '&&', 'operands': 2, 'priority': 603, 'function': lambda x, y: x and y },
+	'or':		{ 'symbol': '||', 'operands': 2, 'priority': 604, 'function': lambda x, y: x or y },
+}
+
+for name, data in OperatorData.iteritems():
+	Operators.append (Operator (name=name, symbol=data['symbol'], operands=data['operands'],
+		priority=data['priority'], function=data['function']))
+
+OperatorSymbols={}
+for op in Operators:
+	if op.symbol in OperatorSymbols:
+		OperatorSymbols[op.symbol].append (op)
+	else:
+		OperatorSymbols[op.symbol] = [op]
+
+Constants = {
+	'pi': cmath.pi,
+	'e': cmath.e,
+	'phi': 1.6180339887498948482,
+	'epsilon': epsilon,
+}
+
+random.seed (time.time())
+
+Functions = {
+	'round':	{ 'function': realf (round) },
+	'floor':	{ 'function': realf (math.floor) },
+	'ceil':		{ 'function': realf (math.ceil) },
+	'fact':		{ 'function': intf (math.factorial) },
+	'abs':		{ 'function': realf (math.fabs) },
+	'degrees':	{ 'function': realf (math.degrees) },
+	'radians':	{ 'function': realf (math.radians) },
+	'erf':		{ 'function': realf (math.erf) },
+	'erfc':		{ 'function': realf (math.erfc) },
+	'gamma':	{ 'function': realf (math.gamma) },
+	'lgamma':	{ 'function': realf (math.lgamma) },
+	'sqrt':		{ 'function': cmath.sqrt },
+	'ln':		{ 'function': cmath.log },
+	'log':		{ 'function': cmath.log10 },
+	'sin':		{ 'function': cmath.sin },
+	'sinh':		{ 'function': cmath.sinh },
+	'asin':		{ 'function': cmath.asin },
+	'asinh':	{ 'function': cmath.asinh },
+	'cos':		{ 'function': cmath.cos },
+	'cosh':		{ 'function': cmath.cosh },
+	'acos':		{ 'function': cmath.acos },
+	'acosh':	{ 'function': cmath.acosh },
+	'tan':		{ 'function': cmath.tan },
+	'tanh':		{ 'function': cmath.tanh },
+	'atan':		{ 'function': cmath.atan },
+	'atanh':	{ 'function': cmath.atanh },
+	'exp':		{ 'function': cmath.exp },
+	'phase':	{ 'function': cmath.phase },
+	'lg':		{ 'function': lambda x: cmath.log (x, 2) },
+	're':		{ 'function': lambda x: x.real },
+	'im':		{ 'function': lambda x: x.imag },
+	'conjugate':{ 'function': lambda x: x.conjugate() },
+	'rand':		{ 'function': random.random },
+}
+
+Tokens = ['(', ')']
+
+# Symbol table
+SymbolType = Enum ('CONSTANT', 'FUNCTION', 'OPERATOR', 'TOKEN')
+SymbolTypes = {}
+Symbols = []
+
+for name, value in Constants.iteritems():
+	Symbols.append (name)
+	SymbolTypes[name] = SymbolType.CONSTANT
+
+for name, data in Functions.iteritems():
+	Symbols.append (name)
+	SymbolTypes[name] = SymbolType.FUNCTION
+
+for op in Operators:
+	if op.symbol not in Symbols:
+		Symbols.append (op.symbol)
+	SymbolTypes[op.symbol] = SymbolType.OPERATOR
+
+for name in Tokens:
+	SymbolTypes[name] = SymbolType.TOKEN
+
+Symbols += Tokens
+Symbols = sorted (Symbols, key=lambda x: len (x), reverse=True)
+
+def parse_number (expr):
+	"""Tries to parse a number from the expression. Returns (value, length) on success."""
+	i = 0
+	value = None
+
+	if expr[0] == 'i':
+		# Special case, we just have 'i' -- need special handling here because otherwise this would
+		# call float('') in the complex number routine, which throws an error.
+		# TODO: maybe 'i' can be a symbol instead?
+		return (1j, 1)
+
+	# Try parse increasingly long substrings until we are unable to create a float or complex number
+	# from it.
+	try:
+		while i < len (expr):
+			if expr[i] == 'i':
+				value = complex (0.0, float (expr[:i]))
+			else:
+				value = complex (float (expr [:i + 1]), 0.0)
+			i += 1
+
+		return (value, i)
+	except ValueError:
+		if i != 0:
+			# Got a number (the error happened when we tried to parse past the number)
+			return (value, i)
+		else:
+			# The error happened on the first character. So this is not a number.
+			return None
+
+def parse_symbol (expr):
+	for sym in Symbols:
+		if expr[:len (sym)] == sym:
+			return sym
+
+	return None
+
+def tokenize (expr):
+	expr = re.sub ('\s+', '', expr.strip())
+	i=0
+	tokens = []
+
+	while i < len(expr):
+		sym = parse_symbol (expr[i:])
+
+		if sym:
+			symtype = SymbolTypes[sym]
+			if symtype == SymbolType.CONSTANT:
+				tokens.append (Constants[sym])
+			else:
+				tokens.append (sym)
+
+			i += len(sym)
+			continue
+
+		result = parse_number (expr[i:])
+		if result:
+			num, length = result
+			tokens.append (num)
+			i += length
+			continue
+
+		raise ValueError ("""bad expression, couldn't parse: %s""" % expr[i:])
+
+	return tokens
+
+def rindex (li, value):
+	return (len(li) - 1) - li[::-1].index(value)
+
+def process_parens (expr):
+	"""Processes parentheses of expr into sublists in-place.
+	[1.0, '*', '(', 3.5, '+', 1j, ')']
+	-> [1.0, '*', [3.5, '+', 1j]]"""
+	if '(' not in expr and ')' not in expr:
+		return
+
+	try:
+		parenStart = rindex (expr, '(')
+		parenEnd = expr.index (')', parenStart)
+	except ValueError:
+		raise ValueError ("""mismatched parentheses in expression: %s""" % expr)
+
+	subexpr = expr[parenStart + 1:parenEnd]
+	del expr[parenStart + 1:parenEnd + 1]
+	expr[parenStart] = subexpr
+	process_parens (expr)
+
+def process_functions (expr):
+	"""Processes functions in-place"""
+	i = 0
+	while i < len (expr):
+		if type (expr[i]) is list:
+			process_functions (expr[i])
+
+		if (type(expr[i]) is not str) or (expr[i] not in Functions):
+			i += 1
+			continue
+
+		# Ensure that arguments follow
+		if (i + 1 >= len (expr)) or (type (expr[i + 1]) is not list):
+			raise ValueError ("""function %s used without arguments""" % expr[i])
+
+		args = expr[i + 1]
+		del expr[i + 1]
+		expr[i] = FunctionCall (expr[i], args)
+		i += 1
+
+def is_operand (x):
+	# Operands can be either lists (which also mean parens, thus can be single-expressions)
+	# or complex numbers
+	return type(x) in [complex, list]
+
+def find_fitting_operator (sym, numOperands):
+	# Pass 1: exact numOperands match
+	for op in Operators:
+		if op.symbol != sym:
+			continue
+
+		if op.operands == numOperands:
+			# print '%s, %d -> %s (1st pass)' % (sym, numOperands, op)
+			return op
+
+	# Pass 2: by symbol
+	for op in Operators:
+		if op.symbol == sym:
+			# print '%s, %d -> %s (2nd pass)' % (sym, numOperands, op)
+			return op
+
+	raise ValueError ('''unknown operator %s!''' % sym)
+
+def process_operators (expr):
+	"""Processes operators"""
+	i = 0
+
+	# Find all operators in this expression
+	while i < len (expr):
+		if type (expr[i]) is list:
+			process_functions (expr[i])
+			process_operators (expr[i])
+
+		if type (expr[i]) is FunctionCall:
+			process_functions (expr[i].args)
+			process_operators (expr[i].args)
+
+		if (type(expr[i]) is not str) or (expr[i] not in OperatorSymbols):
+			i += 1
+			continue
+
+		args = []
+		argIndices = []
+		if i > 0 and is_operand (expr[i - 1]):
+			args.append (expr[i - 1])
+			argIndices.append (i - 1)
+
+		if i - 1 < len(expr) and is_operand (expr[i + 1]):
+			args.append (expr[i + 1])
+			argIndices.append (i + 1)
+
+		# Resolve operators with the same symbol based on operand count
+		numOperands = 0
+		for arg in args:
+			if is_operand (arg):
+				numOperands += 1
+
+		expr[i] = find_fitting_operator (expr[i], numOperands)
+		i += 1
+
+def find_priority_operator (expr):
+	"""Finds the operator with most priority in the expression"""
+	bestOp = None
+	bestOpIndex = -1
+
+	for i in range (0, len (expr)):
+		sym = expr[i]
+
+		if type (sym) is not Operator:
+			continue
+
+		if not bestOp or sym.priority < bestOp.priority:
+			bestOp = sym
+			bestOpIndex = i
+
+	return (bestOp, bestOpIndex)
+
+def realPrint (x):
+	print x
+
+tabs=''
+def evaluate (expr, verbose=False):
+	global tabs
+	printFunc = realPrint if verbose else lambda x:None
+	printFunc (tabs + 'Preprocess: %s' % expr)
+
+	# If there are sub-expressions in here, those need to be evaluated first
+	i = 0
+	while i < len (expr):
+		sym = expr[i]
+
+		if type (sym) is list and sym:
+			printFunc (tabs + 'Evaluating sub-expression: %s' % (sym))
+			tabs += '\t'
+			expr[i] = evaluate (list (sym), verbose)
+			tabs = tabs[:-1]
+			printFunc (tabs + '-> %s' % expr[i])
+
+		# If there are function calls, evaluate those
+		if type (sym) is FunctionCall:
+			tabs += '\t'
+			if sym.args:
+				sym.args = [evaluate (sym.args, verbose)]
+			tabs = tabs[:-1]
+
+			printFunc (tabs + 'Evaluating function call: %s' % (sym))
+			expr[i] = Functions[sym.funcname]['function'] (*sym.args)
+			printFunc (tabs + '-> %s' % expr[i])
+			
+		i += 1
+
+	printFunc (tabs + 'Evaluate: %s' % expr)
+	runaway = 0
+	while True:
+		runaway += 1
+		if runaway > 1000:
+			raise ValueError ('infinite loop detected')
+
+		op, i = find_priority_operator (expr)
+		if not op:
+			break
+
+		if op.operands == 2:
+			argIndices = [i - 1, i + 1]
+		else:
+			argIndices = [i + 1]
+
+		args = [expr[x] for x in argIndices]
+		argIndices = sorted (argIndices, reverse=True)
+		printFunc (tabs + 'Processing: (%s, %d) with args %s' % (op, i, args))
+		expr[i] = op.function (*args)
+		printFunc (tabs + '-> %s' % expr[i])
+
+		for i2 in argIndices:
+			del expr[i2]
+
+	printFunc (tabs + 'Result: %s' % expr[0])
+
+	if len(expr) != 1:
+		printFunc (tabs + 'Bad expression detected, tokens: %s' % expr)
+		raise ValueError ('malformed expression')
+
+	return expr[0]
+
+def repr_number (x):
+	"""Returns a string representation for a real number"""
+	rep = '%.10f' % x
+
+	if '.' in rep:
+		while rep[-1] == '0':
+			rep = rep[:-1]
+
+		if rep[-1] == '.':
+			rep = rep[:-1]
+
+	return rep
+
+def repr_imaginary (x):
+	rep = repr_number (x)
+
+	if rep == '1':
+		return 'i'
+
+	if rep == '-1':
+		return '-i'
+
+	return rep + 'i'
+
+def represent (x):
+	"""Returns a string representation of a float or complex number"""
+	if math.fabs (x.imag) > epsilon:
+		if math.fabs (x.real) > epsilon:
+			# Complex number
+			return '%s %s %s' % (repr_number (x.real),
+				'+' if x.imag >= 0 else '-',
+				repr_imaginary (math.fabs (x.imag)))
+		else:
+			# Pure imaginary number
+			return repr_imaginary (x.imag)
+	else:
+		# Real number
+		return repr_number (x.real)
+
+def calc (expr, verbose=False):
+	expr = tokenize (expr)
+	process_parens (expr)
+	process_functions (expr)
+	process_operators (expr)
+	return represent (evaluate (expr, verbose))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/commitsdb.py	Sun Apr 19 19:45:42 2015 +0300
@@ -0,0 +1,84 @@
+class CommitsDb (object):
+	def __init__(self):
+		needNew = not os.path.isfile ('commits.db')
+		self.db = sqlite3.connect ('commits.db')
+
+		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()
+
+			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()
\ No newline at end of file
--- a/hgpoll.py	Sat Apr 11 21:02:54 2015 +0300
+++ b/hgpoll.py	Sun Apr 19 19:45:42 2015 +0300
@@ -7,7 +7,10 @@
 from datetime import datetime
 from configfile import Config
 import utility
-g_needCommitsTxtRebuild = True
+g_CommitsDb = None
+
+def all_repo_names():
+	return Config.get_node ('hg').get_value ('repos', {}).keys()
 
 class CommitsDb (object):
 	def __init__(self):
@@ -94,8 +97,6 @@
 	def commit(self):
 		self.db.commit()
 
-g_CommitsDb = CommitsDb()
-
 def color_for_repo (repo_name):
 	repo_name = repo_name.lower()
 	repoconfig = Config.get_node ('hg').get_node ('repos')
@@ -106,7 +107,10 @@
 	return 0
 
 def prettify_bookmarks (bookmarks):
-	return "\0036 [\002%s\002]" % bookmarks
+	if bookmarks:
+		return "\0036 [\002%s\002]" % bookmarks
+	else:
+		return ''
 
 def get_repo_info (reponame):
 	reponame = reponame.lower()
@@ -125,14 +129,20 @@
 	if not os.path.exists (repo_name):
 		os.makedirs (repo_name)
 
-	try:
-		zanrepo.hg_command ('id', '.')
-	except hgapi.hgapi.HgException:
+	if not os.path.isfile (os.path.join (repo_name, '.hg', 'hgrc')):
 		# If the repo does not exist, clone it.
 		repo_url = get_repo_info (repo_name).get_value ('url')
 		try:
 			print ('Cloning %s...' % repo_name)
 			zanrepo.hg_clone (repo_url, repo_name)
+
+			# We need to un-alias a few things, they can 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:
+					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 ('Cloning done.')
 		except Exception as e:
 			print ('Unable to clone %s from %s: %s' % (repo_name, repo_url, e))
@@ -250,14 +260,15 @@
 
 	Bt.post_note (ticket_id, message)
 
-def all_repo_names():
-	return Config.get_node ('hg').get_value ('repos', {}).keys()
+def init():
+	global repocheck_timeout
+	global g_CommitsDb
 
-def init():
 	for repo in all_repo_names():
 		check_repo_exists (repo)
-	global repocheck_timeout
+
 	repocheck_timeout = time.time() + 15
+	g_CommitsDb = CommitsDb()
 
 def get_commit_data (repo, rev, template):
 	return repo.hg_command ('log', '-l', '1', '-r', rev, '--template', template)
@@ -309,17 +320,18 @@
 
 	repo = hgapi.Repo (repo_name)
 	commit_data = []
-	delimeter = '@@@@@@@@@@'
+	delimeter = '^^^^^^^^^^'
+	delimeter2 = '@@@@@@@@@@'
 	print 'Checking %s for updates' % repo_name
 
 	try:
 		data = repo.hg_command ('incoming', '--quiet', '--template',
-			'{node|short} {desc}' + delimeter).split (delimeter)
+			delimeter.join (['{node|short}', '{desc}']) + delimeter2).split (delimeter2)
 	except hgapi.hgapi.HgException as e:
 		deciphered = decipher_hgapi_error (e)
 
 		if deciphered[0] and len(deciphered[1]) > 0:
-			Irc.broadcast ("error while using hg import on %s: %s" % (repo_name, deciphered[1]))
+			Irc.broadcast ("error while using hg incoming on %s: %s" % (repo_name, deciphered[1]))
 
 		return
 	except Exception as e:
@@ -330,20 +342,8 @@
 		print ('No updates to %s' % repo_name)
 
 	for line in data:
-		if line == '':
-			continue
-
-		rex = re.compile (r'([^ ]+) (.+)')
-		match = rex.match (line)
-		failed = False
-
-		if not match:
-			Irc.broadcast ('malformed hg data: %s' % line)
-			continue
-
-		commit_node = match.group (1)
-		commit_message = match.group (2)
-		commit_data.append ([commit_node, commit_message])
+		if line:
+			commit_data.append (line.split (delimeter))
 
 	process_new_commits (repo_name, commit_data)
 
@@ -357,17 +357,7 @@
 	num_commits = 0
 	zanrepo = hgapi.Repo (repo_name)
 	print '%d new commits on %s' % (len (commit_data), repo_name)
-
-	# Drop commits that we already have
-	i = 0
-	while i < len (commit_data):
-		try:
-			zanrepo.hg_command ('log', '-r', commit_data[i][0])
-			del commit_data[i]
-		except:
-			i += 1
-
-	pull_args = [];
+	pull_args = []
 
 	for commit in commit_data:
 		pull_args.append ('-r');
@@ -386,12 +376,12 @@
 		print 'Processing new commit %s...' % commit_node
 
 		try:
-			alreadyAdded = len (g_CommitsDb.get_commit_repos (commit_node)) > 0
+			existingrepos = g_CommitsDb.get_commit_repos (commit_node)
+			alreadyAdded = len (existingrepos) > 0
 
 			delim = '@@@@@@@@@@'
 			data = get_commit_data (zanrepo, commit_node, delim.join (['{author}', '{bookmarks}', \
 				'{date|hgdate}'])).split (delim)
-			print 'data is: %s' % data
 			commit_author = data[0]
 			commit_bookmarks = prettify_bookmarks (data[1])
 			commit_time = int (data[2].split (' ')[0])
@@ -402,6 +392,8 @@
 			# 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_node, timestamp=commit_time)
 			if alreadyAdded:
+				print ('''I already know of %s - they're in %s - not announcing.''' %
+					(commit_node, existingrepos))
 				continue
 
 			# Remove the email address from the author if possible
@@ -425,8 +417,8 @@
 							% (color_for_repo (repo_name), repo_name, commit_node, commit_bookmarks,
 							committer, utility.shorten_link (commit_url)))
 
-						for line in commit_message.split ('\n'):
-							irc_client.privmsg (channel.get_value ('name'), line)
+						for line in commit_message.splitlines():
+							irc_client.privmsg (channel.get_value ('name'), '    ' + line)
 
 			if ispublishing:
 				rex = re.compile (r'^.*(fixes|resolves|addresses|should fix) ([0-9]+).*$')
--- a/irc.py	Sat Apr 11 21:02:54 2015 +0300
+++ b/irc.py	Sun Apr 19 19:45:42 2015 +0300
@@ -122,6 +122,7 @@
 
 			if line.startswith ("PING :"):
 				self.write ("PONG :%s" % line[6:])
+				self.send_all_now() # pings must be responded to immediately!
 			else:
 				words = line.split(" ")
 				if len(words) >= 2:
--- a/mod_hg.py	Sat Apr 11 21:02:54 2015 +0300
+++ b/mod_hg.py	Sun Apr 19 19:45:42 2015 +0300
@@ -32,10 +32,17 @@
 
 		{
 			'name': 'resolves',
-			'description': 'Manually cause a ticket to be resolved by a changeset',
+			'description': '''Manually cause a ticket to be resolved by a changeset''',
 			'args': '<ticket> <changeset>',
 			'level': 'admin', # TODO
 		},
+
+		{
+			'name': 'rebuildcommitsdb',
+			'description': '''Rebuilds commits.db''',
+			'args': None,
+			'level': 'admin',
+		},
 	]
 }
 
@@ -140,7 +147,6 @@
 		except IOError:
 			pass
 
-
 		repo.hg_command ('revert', '--all')
 
 		# Remove the email address from the author if possible
@@ -211,7 +217,7 @@
 			command_error (`e`)
 
 def cmd_resolves (bot, args, **rest):
-	try:
-		HgPoll.announce_ticket_resolved (args['ticket'], args['changeset'])
-	except Exception as e:
-		command_error (str (e))
+	HgPoll.announce_ticket_resolved (args['ticket'], args['changeset'])
+
+def cmd_rebuildcommitsdb (bot, args, **rest):
+	HgPoll.g_CommitsDb.create_new()
\ No newline at end of file
--- a/mod_util.py	Sat Apr 11 21:02:54 2015 +0300
+++ b/mod_util.py	Sun Apr 19 19:45:42 2015 +0300
@@ -6,6 +6,7 @@
 from modulecore import command_error
 import modulecore as ModuleCore
 import utility
+import calc
 
 ModuleData = {
 	'commands':
@@ -46,6 +47,13 @@
 		},
 
 		{
+			'name': 'calcfunctions',
+			'description': 'Lists the functions supported by .calc',
+			'args': None,
+			'level': 'normal',
+		},
+
+		{
 			'name': 'more',
 			'description': 'Prints subsequent command result pages',
 			'args': None,
@@ -160,30 +168,34 @@
 
 	return expr
 
-def cmd_calc (bot, reply, args, **rest):
-	expr = args['expression']
+def cmd_calcfunctions (bot, reply, **rest):
+	reply ('Available functions for .calc: %s' % \
+		', '.join (sorted ([name for name, data in calc.Functions.iteritems()])))
 
-	try:
+def cmd_calc (bot, reply, args, **rest):
+	reply (calc.calc (args['expression']))
+	# expr = args['expression']
+	# try:
 		# Substitute some mathematical constants
-		expr = mathsubstitute (expr, 'pi' , 3.14159265358979323846264338327950288419716939937510)
-		expr = mathsubstitute (expr, 'e'  , 2.71828182845904523536028747135266249775724709369995)
-		expr = mathsubstitute (expr, 'phi', 1.6180339887498948482) # golden ratio
+		# expr = mathsubstitute (expr, 'pi' , 3.14159265358979323846264338327950288419716939937510)
+		# expr = mathsubstitute (expr, 'e'  , 2.71828182845904523536028747135266249775724709369995)
+		# expr = mathsubstitute (expr, 'phi', 1.6180339887498948482) # golden ratio
 
-		result = subprocess.check_output (['calc', '--', expr], stderr=subprocess.STDOUT) \
-			.replace ('\t', '') \
-			.replace ('\n', '')
+		# result = subprocess.check_output (['calc', '--', expr], stderr=subprocess.STDOUT) \
+			# .replace ('\t', '') \
+			# .replace ('\n', '')
 
-		errmatch = re.compile (r'^.*\bError\b.*$').match (result)
+		# errmatch = re.compile (r'^.*\bError\b.*$').match (result)
 
-		if errmatch:
-			command_error ('math error')
-			return
+		# if errmatch:
+			# command_error ('math error')
+			# return
 
-		reply ('Result: %s' % result)
-	except subprocess.CalledProcessError as e:
-		command_error (e.output.split('\n')[0])
-	except OSError as e:
-		command_error ('failed to execute calc: ' + e.strerror)
+		# reply ('Result: %s' % result)
+	# except subprocess.CalledProcessError as e:
+		# command_error (e.output.split('\n')[0])
+	# except OSError as e:
+		# command_error ('failed to execute calc: ' + e.strerror)
 
 def cmd_more (commandObject, **rest):
 	ModuleCore.print_responses (commandObject)
--- a/rest.py	Sat Apr 11 21:02:54 2015 +0300
+++ b/rest.py	Sun Apr 19 19:45:42 2015 +0300
@@ -100,6 +100,7 @@
 
 		try:
 			with open ('rejected_json.txt', 'w') as fp:
+				fp.write (str (e))
 				fp.write (jsonstring)
 			Irc.broadcast ('bad json written to rejected_json.txt')
 		except Exception as e:
--- a/utility.py	Sat Apr 11 21:02:54 2015 +0300
+++ b/utility.py	Sun Apr 19 19:45:42 2015 +0300
@@ -12,4 +12,4 @@
 		except Exception as e:
 			Irc.broadcast ('Error while shortening link "%s": %s' % (link, e))
 
-	return link
+	return link
\ No newline at end of file

mercurial