patch bfc8c5f7c020e7fcfcb4f2f7b76473da576f3576
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Fri Nov 20 18:13:48 GMT 2020
  tagged 1.2
patch 7615eff358a9430b4a7529079dd8c6dbfe7523cf
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Fri Nov 20 13:22:25 GMT 2020
  * Version 1.2
patch bbcff73a80378b5925ad2d67f67ede7b5ca48202
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Fri Nov 20 13:20:41 GMT 2020
  tagged 1.2-rc2
patch 8df24868d93b5b0a7381a50162113bb99a4cf4b4
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Fri Nov 20 11:35:49 GMT 2020
  * Fix possibly infinite loop on filehistory error
  
  The last patch introduced a shameful bug: if darcs returns an error during a
  file history lookup, "<changelog>" won't appear but darcsweb will be stuck
  looking for it.
  
  This patch fixes the problem by returning early if EOF is reached.
patch 3d7da42fe33eb61c0604d9e804b65b0bc25fb625
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Fri Nov 20 09:42:29 GMT 2020
  * Fix filehistory by working around non-XML output
  
  "darcs changes --xml-output FILE" can emit non-XML output at first, before
  changing to XML later on.
  
  This (reasonably) confuses the XML parser, causing filehistory to break.
  
  This patch implements a workaround to this, which is to read and discard the
  output until "<changelog>" appears, which is the top-level element in the XML.
patch 71504726e8195bcbe812dba5882c8cfd078f8b87
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Fri Nov 20 01:00:58 GMT 2020
  * Ignore "explicit_dependencies" in tag patches
  
  In the XML output, tag patches now have an "explicit_dependencies" section
  which includes patches inside.
  
  Since the parser is non-hierarchical, this confuses it and those patches are
  currently being counted twice.
  
  This patch fixes the issue by ignoring the "explicit_dependencies" section.
patch 09f93bac0a514ce34aebcaabf77867c089a5da05
Author: ernst@creagraphy.nl
Date:   Fri Nov 20 00:21:19 GMT 2020
  * Work around a Darcs bug, especially version 2.12.4.
  Darcs annotate sometimes generates a patch number of 0000000000000000000000000000000000000000
  Skip such an invalid patch number, using the previous one instead.
  
patch 98e48e16c207ced7735b402585607e41d1fb4a44
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Thu Nov 19 17:47:10 GMT 2020
  * Update annotate to the new machine-readable format
  
  Darcs no longer supports annotate --xml-output, it has been replaced by a
  custom machine-readable format.
  
  This patch updates the annotate handler to parse this new format.
  
  Thanks to Ernst de Moor (ernst@creagraphy.nl) for reporting this and providing
  an alternative patch!
patch 04d31398ebef99b968bbee90b2d01b84faab4e2f
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Thu Sep  6 00:02:29 IST 2012
  * Tell pygments to output in utf8
  
  pygment, by default, sends unicode strings in the output, which we don't
  support. Instead, we need to tell it to output in utf8-encoded strings.
  
  Thanks to both Øistein E. Andersen and Alexei Matveev for the bug reports and
  patches.
  
patch 074b2fa3a79549917bae15cb7dfbe835afab267a
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Wed Feb 15 22:17:34 GMT 2012
  * Fix hashlib import
patch 8979fbb8bc87354024df06861711d328e33e2cdb
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Wed Feb 15 00:16:38 GMT 2012
  * Use subprocess instead of popen4()
  
  This patch makes darcsweb use subprocess.Popen() for running darcs, instead of
  os.popen4() which has been deprecated.
  
  The bulk of the change is actually a modification of run_darcs()' API, because
  it now takes the parameters as a list of strings, instead of just a big string
  as it was before, which is cleaner and safer.
  
patch a5bd7f22416daa18c3cef17ff4442038435f737e
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Tue Feb 14 02:05:03 GMT 2012
  * Set $PATH if it's not in the environment
  Recent versions of darcs require the $PATH environment variable to be set, in
  order to find "diff". Some http servers don't set it, and darcs will fail to
  diff.
  
  This patch attempts to set a basic path variable hoping that it will work for
  most cases; it is really up to the server to set it if diff is not in one of
  these places.
  
patch 90e034038cd50f3a647486deb19a414b52665951
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Tue Feb 14 01:54:06 GMT 2012
  * Use the new hashlib if available
  
  We should use hashlib when available (Python >= 2.5); we continue to fall back
  to the old sha library if it's not.
  
  Thanks to Karl O. Pinc for the bug report.
patch 7214bf99f9cc9b3ea05fa6d74d47d563cd288bdb
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Tue Feb 14 01:43:03 GMT 2012
  * Ignore comments in the author file
  
  This patch makes darcsweb ignore comments in the author file.
  
patch f29ed7e843e909162f4cb89c72e28ea9ba20a0c4
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Wed Nov 24 15:52:33 GMT 2010
  tagged 1.2-rc1
patch 8a239f73201275869131831568f2068e2e872b20
Author: Dave Love <fx@gnu.org>
Date:   Fri Oct 15 13:55:46 IST 2010
  * Use diff --quiet.
  Prevents `Copying pristine' messages in output with darcs pre-2.5.
patch 1a421ea50019e1f5b5af8216aad61b9330af5bce
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Fri Jun 25 01:32:55 IST 2010
  * Do not use string exceptions
  String exceptions are deprecated and will soon no longer be supported at all.
  
  Most of the instances of string exceptions in our code were not meant to be
  catched, but just to get meaningful backtraces.
  
  This patch replaces them with appropriate, usually generic, exceptions that
  serve the same purpose.
  
patch f978b7b0fda048b477f3fc3a65f3a45bb0c0cc9f
Author: Simon Michael <simon@joyful.com>
Date:   Sun Mar 28 19:21:44 IST 2010
  * log view: display the patch name just once, tighten up whitespace
patch d9bc3d38b832ae7ec4513066414f6ee55df5cf66
Author: Simon Michael <simon@joyful.com>
Date:   Sun Mar 28 17:40:18 IST 2010
  * allow custom "last" value in url, eg to view all patches on one page
patch 8527402c0d1a811144f758b20615ab516797b4b7
Author: Simon Michael <simon@joyful.com>
Date:   Sun Mar 28 17:37:55 IST 2010
  * hide darcs' Ignore-this: metadata in log view
patch 0811a066257b5fffa5716757e44be87bd5bbb1a7
Author: Simon Michael <simon@joyful.com>
Date:   Sun Mar 28 19:57:50 IST 2010
  * more robust timestamp parsing
  With this, darcsweb can list the full darcs repo log.
patch 9c08efbd7c856aae8d8e190e2f5ca8c122711c39
Author: gaetan.lehmann@jouy.inra.fr
Date:   Thu Mar 26 19:58:42 GMT 2009
  * add the year in the date field if the patch wasn't made in the current year
patch da571dba4978ab2a1087540acd59d35400815572
Author: Alberto Bertogli <albertito@blitiri.com.ar>
Date:   Tue Oct 28 15:44:36 GMT 2008
  tagged 1.1
diff -rN -u old-darcsweb/darcsweb.cgi new-darcsweb/darcsweb.cgi
--- old-darcsweb/darcsweb.cgi	2020-11-20 22:02:00.257209913 +0000
+++ new-darcsweb/darcsweb.cgi	2020-11-20 22:02:00.257209913 +0000
@@ -23,6 +23,8 @@
 
 iso_datetime = '%Y-%m-%dT%H:%M:%SZ'
 
+PATCHES_PER_PAGE = 50
+
 # In order to be able to store the config file in /etc/darcsweb, it has to be
 # added to sys.path. It's mainly used by distributions, which place the
 # default configuration there. Add it second place, so it goes after '.' but
@@ -77,7 +79,7 @@
 
 def filter_file(s):
 	if '..' in s or '"' in s:
-		raise 'FilterFile FAILED'
+		raise Exception, 'FilterFile FAILED'
 	if s == '/':
 		return s
 
@@ -114,7 +116,7 @@
 			return s.decode(e).encode('utf8', 'replace')
 		except UnicodeDecodeError:
 			pass
-	raise 'DecodingError', config.repoencoding
+	raise UnicodeDecodeError, config.repoencoding
 
 
 def escape(s):
@@ -128,7 +130,11 @@
 		# the cached entries will have old data; so in this case just
 		# return a nice string
 		t = time.localtime(epoch)
-		s = time.strftime("%d %b %H:%M", t)
+		currentYear = time.localtime()[0]
+		if t[0] == currentYear:
+			s = time.strftime("%d %b %H:%M", t)
+		else:
+			s = time.strftime("%d %b %Y %H:%M", t)
 		return s
 	age = int(time.time()) - int(epoch)
 	if age > 60*60*24*365*2:
@@ -186,6 +192,10 @@
 
 	return s
 
+def strip_ignore_this(s):
+	"""Strip out darcs' Ignore-this: metadata if present."""
+	import re
+	return re.sub(r'^Ignore-this:[^\n]*\n?','',s)
 
 def highlight(s, l):
 	"Highlights appearences of s in l"
@@ -284,7 +294,7 @@
 	if darcs_runs:
 		s += "\truns:\n"
 		for params in darcs_runs:
-			s += '\t\t%s\n' % params
+			s += '\t\t%s\n' % ' '.join(params)
 	s += '\n'
 
 	lf = open(config.logtimes, 'a')
@@ -298,11 +308,12 @@
 		return time.strptime(s, "%Y%m%d%H%M%S")
 	except ValueError:
 		# very old darcs commits use a different format, for example:
-		# "Wed May 21 19:39:10 CEST 2003"
-		# we can't parse the "CEST" part reliably, so we leave it out
+		# "Wed May 21 19:39:10 CEST 2003" or even:
+		# "Sun Sep 21 07:23:57 Pacific Daylight Time 2003"
+		# we can't parse the time zone part reliably, so we ignore it
 		fmt = "%a %b %d %H:%M:%S %Y"
 		parts = s.split()
-		ns = ' '.join(parts[0:4]) + ' ' + parts[5]
+		ns = ' '.join(parts[0:4]) + ' ' + parts[-1]
 		return time.strptime(ns, fmt)
 
 
@@ -318,7 +329,7 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 		"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
-<!-- darcsweb 1.1
+<!-- darcsweb 1.2
      Alberto Bertogli (albertito@blitiri.com.ar).
 
      Based on gitweb, which is written by Kay Sievers <kay.sievers@vrfy.org>
@@ -533,10 +544,15 @@
 
 class Cache:
 	def __init__(self, basedir, url):
-		import sha
+		try:
+			import hashlib
+			sha = hashlib.sha1
+		except ImportError:
+			from sha import sha
+
 		self.basedir = basedir
 		self.url = url
-		self.fname = sha.sha(repr(url)).hexdigest()
+		self.fname = sha(repr(url)).hexdigest()
 		self.file = None
 		self.mode = None
 		self.real_stdout = sys.stdout
@@ -620,12 +636,33 @@
 def repo_get_owner():
 	try:
 		fd = open(config.repodir + '/_darcs/prefs/author')
-		author = fd.readlines()[0].strip()
-	except:
-		author = None
+	except IOError:
+		return None
+
+	author = None
+	for line in fd:
+		line = line.strip()
+		if not line.startswith("#"):
+			author = line
+			break
 	return author
 
-def run_darcs(params):
+def run_subprocess(params):
+	"""Wrapper to subprocess.Popen(), which we use to provide backwards
+	compatibility when it's not available. Returns stdout."""
+	try:
+		import subprocess
+		p = subprocess.Popen(params,
+				stdin = None,
+				stdout = subprocess.PIPE,
+				stderr = subprocess.STDOUT)
+		return p.stdout
+	except ImportError:
+		cmd = ' '.join(params)
+		inf, outf = os.popen4(cmd, 't')
+		return outf
+
+def run_darcs(*params):
 	"""Runs darcs on the repodir with the given params, return a file
 	object with its output."""
 	os.chdir(config.repodir)
@@ -634,8 +671,17 @@
 	except KeyError:
 		original_8bit_setting = None
 	os.environ['DARCS_DONT_ESCAPE_8BIT'] = '1'
-	cmd = config.darcspath + "darcs " + params
-	inf, outf = os.popen4(cmd, 't')
+
+	# Recent darcs versions require the PATH environment variable to be
+	# present in order to find "diff"; if it's not set we give it a
+	# reasonably basic one which would work in most cases, but ideally it
+	# should be properly set by the web server.
+	if 'PATH' not in os.environ:
+		os.environ['PATH'] = ':'.join([config.darcspath, '/bin',
+						'/usr/bin'])
+
+	outf = run_subprocess([config.darcspath + "darcs"] + list(params))
+
 	darcs_runs.append(params)
 	if original_8bit_setting == None:
 		del(os.environ['DARCS_DONT_ESCAPE_8BIT'])
@@ -671,8 +717,8 @@
 	def getdiff(self):
 		"""Returns a list of lines from the diff -u corresponding with
 		the patch."""
-		params = 'diff -u --match "hash %s"' % self.hash
-		f = run_darcs(params)
+		f = run_darcs('diff', '--quiet', '-u',
+				'--match', 'hash %s' % self.hash)
 		return f.readlines()
 
 	def matches(self, s):
@@ -699,13 +745,27 @@
 class XmlInputWrapper:
 	def __init__(self, fd):
 		self.fd = fd
-		self.times = 0
+		self._header_done = False
+		self._changelog_done = False
 		self._read = self.read
 
 	def read(self, *args, **kwargs):
-		self.times += 1
-		if self.times == 1:
+		if not self._header_done:
+			self._header_done = True
 			return '<?xml version="1.0" encoding="utf-8"?>\n'
+
+		if not self._changelog_done:
+			# Read lines until we see "<changelog>". Sometimes the output has
+			# human-readable lines before the XML begins, this is our way to
+			# work around that.
+			self._changelog_done = True
+			while True:
+				l = self.fd.readline()
+				if not l:
+					return l
+				if "<changelog>" in l:
+					return l
+
 		s = self.fd.read(*args, **kwargs)
 		if not s:
 			return s
@@ -725,17 +785,24 @@
 		self.cur_val = ''
 		self.cur_file = ''
 
+		# Ignore the following elements (and sub-elements):
+		# - created_as: When you ask for changes to a given file, the xml
+		#   output begins with the patch that creates it is enclosed in a
+		#   "created_as" tag; then, later, it gets shown again in its usual
+		#   place. Ignore it to avoid double-counting.
+		# - explicit_dependencies: they appear within tag patches as a way of
+		#   indicating dependencies, but we don't really use them, and
+		#   counting them in could be confusing.
+		self.ignored_elems = ['created_as', 'explicit_dependencies']
+
 	def startElement(self, name, attrs):
-		# When you ask for changes to a given file, the xml output
-		# begins with the patch that creates it is enclosed in a
-		# "created_as" tag; then, later, it gets shown again in its
-		# usual place. The following two "if"s take care of ignoring
-		# everything inside the "created_as" tag, since we don't care.
-		if name == 'created_as':
-			self.cur_elem = 'created_as'
-			return
-		if self.cur_elem == 'created_as':
-			return
+		# Skip ignored elements and sub-elements.
+		for ignore_elem in self.ignored_elems:
+			if name == ignore_elem:
+				self.cur_elem = ignore_elem
+				return
+			if self.cur_elem == ignore_elem:
+				return
 
 		# now parse the tags normally
 		if name == 'patch':
@@ -820,13 +887,15 @@
 		self.cur_val += s
 
 	def endElement(self, name):
-		# See the comment in startElement()
-		if name == 'created_as':
-			self.cur_elem = None
-			self.cur_val = ''
-			return
-		if self.cur_elem == 'created_as':
-			return
+		# Skip ignored elements and sub-elements.
+		for ignore_elem in self.ignored_elems:
+			if name == ignore_elem:
+				self.cur_elem = None
+				self.cur_val = ''
+				return
+			if self.cur_elem == ignore_elem:
+				return
+
 		if name == 'replaced_tokens':
 			return
 
@@ -836,7 +905,7 @@
 			if p.inverted:
 				p.name = 'UNDO: ' + p.name
 		elif name == 'comment':
-			self.db[self.current].comment = fixu8(self.cur_val)
+			self.db[self.current].comment = fixu8(strip_ignore_this(self.cur_val))
 		elif name == 'add_file':
 			scv = fixu8(self.cur_val.strip())
 			self.db[self.current].adds.append(scv)
@@ -874,14 +943,14 @@
 	def get_list_db(self):
 		return (self.list, self.db)
 
-def get_changes_handler(params):
+def get_changes_handler(*params):
 	"Returns a handler for the changes output, run with the given params"
 	parser = xml.sax.make_parser()
 	handler = BuildPatchList()
 	parser.setContentHandler(handler)
 
 	# get the xml output and parse it
-	xmlf = run_darcs("changes --xml-output " + params)
+	xmlf = run_darcs("changes", "--xml-output", *params)
 	parser.parse(XmlInputWrapper(xmlf))
 	xmlf.close()
 
@@ -902,31 +971,35 @@
 
 	if fname:
 		if fname[0] == '/': fname = fname[1:]
-		s = '-s "%s"' % fname
+		params = ('-s', fname)
 	else:
-		s = "-s --last=%d" % toget
+		params = ('-s', '--last=%d' % toget)
 
-	handler = get_changes_handler(s)
+	handler = get_changes_handler(*params)
 
 	# return the list of all the patch objects
 	return handler.get_list()[topi:toget]
 
 def get_patch(hash):
-	handler = get_changes_handler('-s --match "hash %s"' % hash)
+	handler = get_changes_handler('-s', '--match', 'hash %s' % hash)
 	patch = handler.db[handler.list[0]]
 	return patch
 
 def get_diff(hash):
-	return run_darcs('diff -u --match "hash %s"' % hash)
+	return run_darcs('diff', '--quiet', '-u',
+			'--match', 'hash %s' % hash)
 
 def get_file_diff(hash, fname):
-	return run_darcs('diff -u --match "hash %s" "%s"' % (hash, fname))
+	return run_darcs('diff', '--quiet', '-u',
+			'--match', 'hash %s' % hash, fname)
 
 def get_file_headdiff(hash, fname):
-	return run_darcs('diff -u --from-match "hash %s" "%s"' % (hash, fname))
+	return run_darcs('diff', '--quiet', '-u',
+			'--from-match', 'hash %s' % hash, fname)
 
 def get_patch_headdiff(hash):
-	return run_darcs('diff -u --from-match "hash %s"' % hash)
+	return run_darcs('diff', '--quiet', '-u',
+			'--from-match', 'hash %s' % hash)
 
 def get_raw_diff(hash):
 	import gzip
@@ -944,30 +1017,28 @@
 	return dsrc
 
 def get_darcs_diff(hash, fname = None):
-	cmd = 'changes -v --matches "hash %s"' % hash
+	params = ['changes', '-v', '--matches', 'hash %s' % hash]
 	if fname:
-		cmd += ' "%s"' % fname
-	return run_darcs(cmd)
+		params.append(fname)
+	return run_darcs(*params)
 
 def get_darcs_headdiff(hash, fname = None):
-	cmd = 'changes -v --from-match "hash %s"' % hash
+	params = ['changes', '-v', '--from-match', 'hash %s' % hash]
 	if fname:
-		cmd += ' "%s"' % fname
-	return run_darcs(cmd)
+		params.append(fname)
+	return run_darcs(*params)
 
 
 class Annotate:
 	def __init__(self):
 		self.fname = ""
-		self.creator_hash = ""
-		self.created_as = ""
 		self.lastchange_hash = ""
 		self.lastchange_author = ""
 		self.lastchange_name = ""
 		self.lastchange_date = None
-		self.firstdate = None
-		self.lastdate = None
 		self.lines = []
+
+		# hash -> date
 		self.patches = {}
 
 	class Line:
@@ -977,88 +1048,45 @@
 			self.pauthor = None
 			self.pdate = None
 
-def parse_annotate(src):
-	import xml.dom.minidom
-
+def parse_annotate(fname, src):
 	annotate = Annotate()
+	annotate.fname = fname
 
-	# FIXME: convert the source to UTF8; it _has_ to be a way to let
-	# minidom know the source encoding
-	s = ""
-	for i in src:
-		s += fixu8(i)
-
-	dom = xml.dom.minidom.parseString(s)
-
-	file = dom.getElementsByTagName("file")[0]
-	annotate.fname = fixu8(file.getAttribute("name"))
-
-	createinfo = dom.getElementsByTagName("created_as")[0]
-	annotate.created_as = fixu8(createinfo.getAttribute("original_name"))
-
-	creator = createinfo.getElementsByTagName("patch")[0]
-	annotate.creator_hash = fixu8(creator.getAttribute("hash"))
-
-	mod = dom.getElementsByTagName("modified")[0]
-	lastpatch = mod.getElementsByTagName("patch")[0]
-	annotate.lastchange_hash = fixu8(lastpatch.getAttribute("hash"))
-	annotate.lastchange_author = fixu8(lastpatch.getAttribute("author"))
-
-	lastname = lastpatch.getElementsByTagName("name")[0]
-	lastname = lastname.childNodes[0].wholeText
-	annotate.lastchange_name = fixu8(lastname)
+	# Hash -> Patch.
+	patches = {}
 
-	lastdate = parse_darcs_time(lastpatch.getAttribute("date"))
-	annotate.lastchange_date = lastdate
-
-	annotate.patches[annotate.lastchange_hash] = annotate.lastchange_date
-
-	# these will be overriden by the real dates later
-	annotate.firstdate = lastdate
-	annotate.lastdate = 0
-
-	file = dom.getElementsByTagName("file")[0]
-
-	for l in file.childNodes:
-		# we're only intrested in normal and added lines
-		if l.nodeName not in ["normal_line", "added_line"]:
+	for l in src:
+		if "|" not in l:
 			continue
+
 		line = Annotate.Line()
+		line.phash, line.text = l.split('|', 1)
 
-		if l.nodeName == "normal_line":
-			patch = l.getElementsByTagName("patch")[0]
-			phash = patch.getAttribute("hash")
-			pauthor = patch.getAttribute("author")
-			pdate = patch.getAttribute("date")
-			pdate = parse_darcs_time(pdate)
-		else:
-			# added lines inherit the creation from the annotate
-			# patch
-			phash = annotate.lastchange_hash
-			pauthor = annotate.lastchange_author
-			pdate = annotate.lastchange_date
-
-		text = ""
-		for node in l.childNodes:
-			if node.nodeType == node.TEXT_NODE:
-				text += node.wholeText
-
-		# strip all "\n"s at the beginning; because the way darcs
-		# formats the xml output it makes the DOM parser to add "\n"s
-		# in front of it
-		text = text.lstrip("\n")
-
-		line.text = fixu8(text)
-		line.phash = fixu8(phash)
-		line.pauthor = fixu8(pauthor)
-		line.pdate = pdate
+		# Darcs sometimes generates 00..00 hashes in the middle of the output
+		# due to a bug. Skip these and use the previous hash instead.
+		if int(line.phash, 16) == 0:
+			line.phash = prevhash
+		else:
+			prevhash = line.phash
+
+		if line.phash not in patches:
+			patch = get_patch(line.phash)
+			patches[line.phash] = patch
+
+		line.pauthor = patches[line.phash].author
+		line.pdate = time.localtime(patches[line.phash].date)
+		annotate.patches[line.phash] = patch.date
 		annotate.lines.append(line)
-		annotate.patches[line.phash] = line.pdate
 
-		if pdate > annotate.lastdate:
-			annotate.lastdate = pdate
-		if pdate < annotate.firstdate:
-			annotate.firstdate = pdate
+	if not annotate.lines:
+		return
+
+	# Find the last patch, and update annotate information accordingly.
+	lastp = sorted(patches.values(), cmp=lambda a, b: cmp(a.date, b.date))[-1]
+	annotate.lastchange_hash = lastp.hash
+	annotate.lastchange_author = lastp.author
+	annotate.lastchange_name = lastp.name
+	annotate.lastchange_date = time.localtime(lastp.date)
 
 	return annotate
 
@@ -1066,17 +1094,17 @@
 	if config.disable_annotate:
 		return None
 
-	cmd = 'annotate --xml-output'
+	params = ['annotate', '--machine-readable']
 	if hash:
-		cmd += ' --match="hash %s"' % hash
+		params += ['--match', 'hash %s' % hash]
 
 	if fname.startswith('/'):
 		# darcs 2 doesn't like files starting with /, and darcs 1
 		# doesn't really care
 		fname = fname[1:]
-	cmd += ' "%s"' % fname
+	params.append(fname)
 
-	return parse_annotate(run_darcs(cmd))
+	return parse_annotate(fname, run_darcs(*params))
 
 
 
@@ -1135,7 +1163,7 @@
 		print '<div %s>' % cl + escape(l) + '</div>'
 
 
-def print_shortlog(last = 50, topi = 0, fname = None):
+def print_shortlog(last = PATCHES_PER_PAGE, topi = 0, fname = None):
 	ps = get_last_patches(last, topi, fname)
 
 	if fname:
@@ -1209,7 +1237,7 @@
 	print "</table>"
 
 
-def print_log(last = 50, topi = 0):
+def print_log(last = PATCHES_PER_PAGE, topi = 0):
 	ps = get_last_patches(last, topi)
 
 	if topi != 0:
@@ -1239,8 +1267,6 @@
   <i>%(author)s [%(date)s]</i><br/>
 </div>
 <div class="log_body">
-  %(desc)s<br/>
-  <br/>
   %(comment)s
 </div>
 
@@ -1331,7 +1357,7 @@
 	else:
 		linenos_method = True
 	formatter = pygments.formatters.HtmlFormatter(linenos=linenos_method,
-				cssclass='page_body')
+				cssclass='page_body', encoding='utf-8')
 
 	print pygments.highlight(code, lexer, formatter)
 
@@ -1974,22 +2000,22 @@
 		sys.stdout.write(l.text)
 
 
-def do_shortlog(topi):
+def do_shortlog(topi, last=PATCHES_PER_PAGE):
 	print_header()
 	print_navbar()
-	print_shortlog(topi = topi)
+	print_shortlog(topi = topi, last = last)
 	print_footer()
 
-def do_filehistory(topi, f):
+def do_filehistory(topi, f, last=PATCHES_PER_PAGE):
 	print_header()
 	print_navbar(f = fname)
-	print_shortlog(topi = topi, fname = fname)
+	print_shortlog(topi = topi, fname = fname, last = last)
 	print_footer()
 
-def do_log(topi):
+def do_log(topi, last=PATCHES_PER_PAGE):
 	print_header()
 	print_navbar()
-	print_log(topi = topi)
+	print_log(topi = topi, last = last)
 	print_footer()
 
 def do_atom():
@@ -2395,7 +2421,7 @@
 				break
 		else:
 			# not found
-			raise "RepoNotFound", name
+			raise Exception, "Repo not found: " + repr(name)
 
 	# fill the configuration
 	base = all_configs.base
@@ -2523,7 +2549,7 @@
 	url_request = os.environ['QUERY_STRING']
 	# create a string representation of the request, ignoring all the
 	# unused parameters to avoid DoS
-	params = ['r', 'a', 'f', 'h', 'topi']
+	params = ['r', 'a', 'f', 'h', 'topi', 'last']
 	params = [ x for x in form.keys() if x in params ]
 	url_request = [ (x, form[x].value) for x in params ]
 	url_request.sort()
@@ -2628,7 +2654,11 @@
 		topi = int(filter_num(form["topi"].value))
 	else:
 		topi = 0
-	do_shortlog(topi)
+	if form.has_key("last"):
+		last = int(filter_num(form["last"].value))
+	else:
+		last = PATCHES_PER_PAGE
+	do_shortlog(topi=topi,last=last)
 
 elif action == "filehistory":
 	if form.has_key("topi"):
@@ -2636,14 +2666,22 @@
 	else:
 		topi = 0
 	fname = filter_file(form["f"].value)
-	do_filehistory(topi, fname)
+	if form.has_key("last"):
+		last = int(filter_num(form["last"].value))
+	else:
+		last = PATCHES_PER_PAGE
+	do_filehistory(topi, fname, last=last)
 
 elif action == "log":
 	if form.has_key("topi"):
 		topi = int(filter_num(form["topi"].value))
 	else:
 		topi = 0
-	do_log(topi)
+	if form.has_key("last"):
+		last = int(filter_num(form["last"].value))
+	else:
+		last = PATCHES_PER_PAGE
+	do_log(topi, last=last)
 
 elif action == 'headblob':
 	fname = filter_file(form["f"].value)
