/
darcsweb.cgi
   1 #!/usr/bin/env python
   2 
   3 """
   4 darcsweb - A web interface for darcs
   5 Alberto Bertogli (albertito@blitiri.com.ar)
   6 
   7 Inspired on gitweb (as of 28/Jun/2005), which is written by Kay Sievers
   8 <kay.sievers@vrfy.org> and Christian Gierke <ch@gierke.de>
   9 """
  10 
  11 import time
  12 time_begin = time.time()
  13 import sys
  14 import os
  15 import string
  16 import stat
  17 import cgi
  18 import cgitb; cgitb.enable()
  19 import urllib
  20 import xml.sax
  21 from xml.sax.saxutils import escape as xml_escape
  22 time_imports = time.time() - time_begin
  23 
  24 iso_datetime = '%Y-%m-%dT%H:%M:%SZ'
  25 
  26 PATCHES_PER_PAGE = 50
  27 
  28 # In order to be able to store the config file in /etc/darcsweb, it has to be
  29 # added to sys.path. It's mainly used by distributions, which place the
  30 # default configuration there. Add it second place, so it goes after '.' but
  31 # before the normal path. This allows per-directory config files (desirable
  32 # for multiple darcsweb installations on the same machin), and avoids name
  33 # clashing if there's a config.py in the standard path.
  34 sys.path.insert(1, '/etc/darcsweb')
  35 
  36 # Similarly, when hosting multiple darcsweb instrances on the same
  37 # server, you can just 'SetEnv DARCSWEB_CONFPATH' in the httpd config,
  38 # and this will have a bigger priority than the system-wide
  39 # configuration file.
  40 if 'DARCSWEB_CONFPATH' in os.environ:
  41 	sys.path.insert(1, os.environ['DARCSWEB_CONFPATH'])
  42 
  43 # empty configuration class, we will fill it in later depending on the repo
  44 class config:
  45 	pass
  46 
  47 # list of run_darcs() invocations, for performance measures
  48 darcs_runs = []
  49 
  50 # exception handling
  51 def exc_handle(t, v, tb):
  52 	try:
  53 		cache.cancel()
  54 	except:
  55 		pass
  56 	cgitb.handler((t, v, tb))
  57 sys.excepthook = exc_handle
  58 
  59 #
  60 # utility functions
  61 #
  62 
  63 def filter_num(s):
  64 	l = [c for c in s if c in string.digits]
  65 	return ''.join(l)
  66 
  67 
  68 allowed_in_action = string.ascii_letters + string.digits + '_'
  69 def filter_act(s):
  70 	l = [c for c in s if c in allowed_in_action]
  71 	return ''.join(l)
  72 
  73 
  74 allowed_in_hash = string.ascii_letters + string.digits + '-.'
  75 def filter_hash(s):
  76 	l = [c for c in s if c in allowed_in_hash]
  77 	return ''.join(l)
  78 
  79 
  80 def filter_file(s):
  81 	if '..' in s or '"' in s:
  82 		raise Exception, 'FilterFile FAILED'
  83 	if s == '/':
  84 		return s
  85 
  86 	# remove extra "/"s
  87 	r = s[0]
  88 	last = s[0]
  89 	for c in s[1:]:
  90 		if c == last and c == '/':
  91 			continue
  92 		r += c
  93 		last = c
  94 	return r
  95 
  96 
  97 def printd(*params):
  98 	print ' '.join(params), '<br/>'
  99 
 100 
 101 # I _hate_ this.
 102 def fixu8(s):
 103 	"""Calls _fixu8(), which does the real work, line by line. Otherwise
 104 	we choose the wrong encoding for big buffers and end up messing
 105 	output."""
 106 	n = []
 107 	for i in s.split('\n'):
 108 		n.append(_fixu8(i))
 109 	return '\n'.join(n)
 110 
 111 def _fixu8(s):
 112 	if type(s) == unicode:
 113 		return s.encode('utf8', 'replace')
 114 	for e in config.repoencoding:
 115 		try:
 116 			return s.decode(e).encode('utf8', 'replace')
 117 		except UnicodeDecodeError:
 118 			pass
 119 	raise UnicodeDecodeError, config.repoencoding
 120 
 121 
 122 def escape(s):
 123 	s = xml_escape(s)
 124 	s = s.replace('"', '&quot;')
 125 	return s
 126 
 127 def how_old(epoch):
 128 	if config.cachedir:
 129 		# when we have a cache, the how_old() becomes a problem since
 130 		# the cached entries will have old data; so in this case just
 131 		# return a nice string
 132 		t = time.localtime(epoch)
 133 		currentYear = time.localtime()[0]
 134 		if t[0] == currentYear:
 135 			s = time.strftime("%d %b %H:%M", t)
 136 		else:
 137 			s = time.strftime("%d %b %Y %H:%M", t)
 138 		return s
 139 	age = int(time.time()) - int(epoch)
 140 	if age > 60*60*24*365*2:
 141 		s = str(age/60/60/24/365)
 142 		s += " years ago"
 143 	elif age > 60*60*24*(365/12)*2:
 144 		s = str(age/60/60/24/(365/12))
 145 		s += " months ago"
 146 	elif age > 60*60*24*7*2:
 147 		s = str(age/60/60/24/7)
 148 		s += " weeks ago"
 149 	elif age > 60*60*24*2:
 150 		s = str(age/60/60/24)
 151 		s += " days ago"
 152 	elif age > 60*60*2:
 153 		s = str(age/60/60)
 154 		s += " hours ago"
 155 	elif age > 60*2:
 156 		s = str(age/60)
 157 		s += " minutes ago"
 158 	elif age > 2:
 159 		s = str(age)
 160 		s += " seconds ago"
 161 	else:
 162 		s = "right now"
 163 	return s
 164 
 165 def shorten_str(s, max = 60):
 166 	if len(s) > max:
 167 		s = s[:max - 4] + ' ...'
 168 	return s
 169 
 170 def replace_tabs(s):
 171 	pos = s.find("\t")
 172 	while pos != -1:
 173 		count = 8 - (pos % 8)
 174 		if count:
 175 			spaces = ' ' * count
 176 			s = s.replace('\t', spaces, 1)
 177 		pos = s.find("\t")
 178 	return s
 179 
 180 def replace_links(s):
 181 	"""Replace user defined strings with links, as specified in the
 182 	configuration file."""
 183 	import re
 184 
 185 	vardict = {
 186 		"myreponame": config.myreponame,
 187 		"reponame": config.reponame,
 188 	}
 189 
 190 	for link_pat, link_dst in config.url_links:
 191 		s = re.sub(link_pat, link_dst % vardict, s)
 192 
 193 	return s
 194 
 195 def strip_ignore_this(s):
 196 	"""Strip out darcs' Ignore-this: metadata if present."""
 197 	import re
 198 	return re.sub(r'^Ignore-this:[^\n]*\n?','',s)
 199 
 200 def highlight(s, l):
 201 	"Highlights appearences of s in l"
 202 	import re
 203 	# build the regexp by leaving "(s)", replacing '(' and ') first
 204 	s = s.replace('\\', '\\\\')
 205 	s = s.replace('(', '\\(')
 206 	s = s.replace(')', '\\)')
 207 	s = '(' + escape(s) + ')'
 208 	try:
 209 		pat = re.compile(s, re.I)
 210 		repl = '<span style="color:#e00000">\\1</span>'
 211 		l = re.sub(pat, repl, l)
 212 	except:
 213 		pass
 214 	return l
 215 
 216 def fperms(fname):
 217 	m = os.stat(fname)[stat.ST_MODE]
 218 	m = m & 0777
 219 	s = []
 220 	if os.path.isdir(fname): s.append('d')
 221 	else: s.append('-')
 222 
 223 	if m & 0400: s.append('r')
 224 	else: s.append('-')
 225 	if m & 0200: s.append('w')
 226 	else: s.append('-')
 227 	if m & 0100: s.append('x')
 228 	else: s.append('-')
 229 
 230 	if m & 0040: s.append('r')
 231 	else: s.append('-')
 232 	if m & 0020: s.append('w')
 233 	else: s.append('-')
 234 	if m & 0010: s.append('x')
 235 	else: s.append('-')
 236 
 237 	if m & 0004: s.append('r')
 238 	else: s.append('-')
 239 	if m & 0002: s.append('w')
 240 	else: s.append('-')
 241 	if m & 0001: s.append('x')
 242 	else: s.append('-')
 243 
 244 	return ''.join(s)
 245 
 246 def fsize(fname):
 247 	s = os.stat(fname)[stat.ST_SIZE]
 248 	if s < 1024:
 249 		return "%s" % s
 250 	elif s < 1048576:
 251 		return "%sK" % (s / 1024)
 252 	elif s < 1073741824:
 253 		return "%sM" % (s / 1048576)
 254 
 255 def isbinary(fname):
 256 	import re
 257 	bins = open(config.repodir + '/_darcs/prefs/binaries').readlines()
 258 	bins = [b[:-1] for b in bins if b and b[0] != '#']
 259 	for b in bins:
 260 		if re.compile(b).search(fname):
 261 			return 1
 262 	return 0
 263 
 264 def realpath(fname):
 265 	realf = filter_file(config.repodir + '/_darcs/pristine/' + fname)
 266 	if os.path.exists(realf):
 267 		return realf
 268 	realf = filter_file(config.repodir + '/_darcs/current/' + fname)
 269 	if os.path.exists(realf):
 270 		return realf
 271 	realf = filter_file(config.repodir + '/' + fname)
 272 	return realf
 273 
 274 def log_times(cache_hit, repo = None, event = None):
 275 	if not config.logtimes:
 276 		return
 277 
 278 	time_total = time.time() - time_begin
 279 	processing = time_total - time_imports
 280 	if not event:
 281 		event = action
 282 	if cache_hit:
 283 		event = event + " (hit)"
 284 	s = '%s\n' % event
 285 
 286 	if repo:
 287 		s += '\trepo: %s\n' % repo
 288 
 289 	s += """\
 290 	total: %.3f
 291 	processing: %.3f
 292 	imports: %.3f\n""" % (time_total, processing, time_imports)
 293 
 294 	if darcs_runs:
 295 		s += "\truns:\n"
 296 		for params in darcs_runs:
 297 			s += '\t\t%s\n' % params
 298 	s += '\n'
 299 
 300 	lf = open(config.logtimes, 'a')
 301 	lf.write(s)
 302 	lf.close()
 303 
 304 
 305 def parse_darcs_time(s):
 306 	"Try to convert a darcs' time string into a Python time tuple."
 307 	try:
 308 		return time.strptime(s, "%Y%m%d%H%M%S")
 309 	except ValueError:
 310 		# very old darcs commits use a different format, for example:
 311 		# "Wed May 21 19:39:10 CEST 2003" or even:
 312 		# "Sun Sep 21 07:23:57 Pacific Daylight Time 2003"
 313 		# we can't parse the time zone part reliably, so we ignore it
 314 		fmt = "%a %b %d %H:%M:%S %Y"
 315 		parts = s.split()
 316 		ns = ' '.join(parts[0:4]) + ' ' + parts[-1]
 317 		return time.strptime(ns, fmt)
 318 
 319 
 320 
 321 #
 322 # generic html functions
 323 #
 324 
 325 def print_header():
 326 	print "Content-type: text/html; charset=utf-8"
 327 	print """
 328 <?xml version="1.0" encoding="utf-8"?>
 329 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 330 		"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 331 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
 332 <!-- darcsweb 1.1
 333      Alberto Bertogli (albertito@blitiri.com.ar).
 334 
 335      Based on gitweb, which is written by Kay Sievers <kay.sievers@vrfy.org>
 336      and Christian Gierke <ch@gierke.de>
 337 -->
 338 <head>
 339 <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
 340 <meta name="robots" content="index, nofollow"/>
 341 <title>darcs - %(reponame)s</title>
 342 <link rel="stylesheet" type="text/css" href="%(css)s"/>
 343 <link rel="alternate" title="%(reponame)s" href="%(url)s;a=rss"
 344 		type="application/rss+xml"/>
 345 <link rel="alternate" title="%(reponame)s" href="%(url)s;a=atom"
 346 		type='application/atom+xml'/>
 347 <link rel="shortcut icon" href="%(fav)s"/>
 348 <link rel="icon" href="%(fav)s"/>
 349 </head>
 350 
 351 <body>
 352 <div class="page_header">
 353   <div class="search_box">
 354     <form action="%(myname)s" method="get"><div>
 355       <input type="hidden" name="r" value="%(reponame)s"/>
 356       <input type="hidden" name="a" value="search"/>
 357       <input type="text" name="s" size="20" class="search_text"/>
 358       <input type="submit" value="search" class="search_button"/>
 359       <a href="http://darcs.net" title="darcs">
 360         <img src="%(logo)s" alt="darcs logo" class="logo"/>
 361       </a>
 362     </div></form>
 363   </div>
 364   <a href="%(myname)s">repos</a> /
 365   <a href="%(myreponame)s;a=summary">%(reponame)s</a> /
 366   %(action)s
 367 </div>
 368 	""" % {
 369 		'reponame': config.reponame,
 370 		'css': config.cssfile,
 371 		'url': config.myurl + '/' + config.myreponame,
 372 		'fav': config.darcsfav,
 373 		'logo': config.darcslogo,
 374 		'myname': config.myname,
 375 		'myreponame': config.myreponame,
 376 		'action': action
 377 	}
 378 
 379 
 380 def print_footer(put_rss = 1):
 381 	print """
 382 <div class="page_footer">
 383 <div class="page_footer_text">%s</div>
 384 	""" % config.footer
 385 	if put_rss:
 386 		print '<a class="rss_logo" href="%s;a=rss">RSS</a>' % \
 387 				(config.myurl + '/' + config.myreponame)
 388 	print "</div>\n</body>\n</html>"
 389 
 390 
 391 def print_navbar(h = "", f = ""):
 392 	print """
 393 <div class="page_nav">
 394 <a href="%(myreponame)s;a=summary">summary</a>
 395 | <a href="%(myreponame)s;a=shortlog">shortlog</a>
 396 | <a href="%(myreponame)s;a=log">log</a>
 397 | <a href="%(myreponame)s;a=tree">tree</a>
 398 	""" % { "myreponame": config.myreponame }
 399 
 400 	if h:
 401 		print """
 402 | <a href="%(myreponame)s;a=commit;h=%(hash)s">commit</a>
 403 | <a href="%(myreponame)s;a=commitdiff;h=%(hash)s">commitdiff</a>
 404 | <a href="%(myreponame)s;a=headdiff;h=%(hash)s">headdiff</a>
 405 		""" % { "myreponame": config.myreponame, 'hash': h }
 406 
 407 	realf = realpath(f)
 408 	f = urllib.quote(f)
 409 
 410 	if f and h:
 411 		print """
 412 | <a href="%(myreponame)s;a=annotate_shade;f=%(fname)s;h=%(hash)s">annotate</a>
 413 		""" % {
 414 			'myreponame': config.myreponame,
 415 			'hash': h,
 416 			'fname': f
 417 		}
 418 	elif f:
 419 		print """
 420 | <a href="%(myreponame)s;a=annotate_shade;f=%(fname)s">annotate</a>
 421 		""" % { "myreponame": config.myreponame, 'fname': f }
 422 
 423 	if f and os.path.isfile(realf):
 424 		print """
 425 | <a href="%(myreponame)s;a=headblob;f=%(fname)s">headblob</a>
 426 		""" % { "myreponame": config.myreponame, 'fname': f }
 427 
 428 	if f and os.path.isdir(realf):
 429 		print """
 430 | <a href="%(myreponame)s;a=tree;f=%(fname)s">headtree</a>
 431 		"""  % { "myreponame": config.myreponame, 'fname': f }
 432 
 433 	if h and f and (os.path.isfile(realf) or os.path.isdir(realf)):
 434 		print """
 435 | <a href="%(myreponame)s;a=headfilediff;h=%(hash)s;f=%(fname)s">headfilediff</a>
 436 		""" % { "myreponame": config.myreponame, 'hash': h, 'fname': f }
 437 
 438 	if f:
 439 		print """
 440 | <a class="link" href="%(myreponame)s;a=filehistory;f=%(fname)s">filehistory</a>
 441 		""" % { "myreponame": config.myreponame, 'fname': f }
 442 
 443 	print "<br/>"
 444 
 445 	efaction = action
 446 	if '_' in action:
 447 		# action is composed as "format_action", like
 448 		# "darcs_commitdiff"; so we get the "effective action" to
 449 		# decide if we need to present the "alternative formats" menu
 450 		pos = action.find('_')
 451 		fmt = action[:pos]
 452 		efaction = action[pos + 1:]
 453 	if efaction in ("commit", "commitdiff", "filediff", "headdiff",
 454 			"headfilediff"):
 455 
 456 		# in order to show the small bar in the commit page too, we
 457 		# accept it here and change efaction to commitdiff, because
 458 		# that's what we're really intrested in
 459 		if efaction == "commit":
 460 			efaction = "commitdiff"
 461 
 462 		params = 'h=%s;' % h
 463 		if f:
 464 			params += 'f=%s;' % f
 465 
 466 		# normal (unified)
 467 		print """
 468 <a class="link" href="%(myreponame)s;a=%(act)s;%(params)s">unified</a>
 469 		""" % { "myreponame": config.myreponame, "act": efaction,
 470 			"params": params }
 471 
 472 		# plain
 473 		print """
 474 | <a class="link" href="%(myreponame)s;a=plain_%(act)s;%(params)s">plain</a>
 475 		""" % { "myreponame": config.myreponame, "act": efaction,
 476 			"params": params }
 477 
 478 		# darcs, htmlized
 479 		print """
 480 | <a class="link" href="%(myreponame)s;a=darcs_%(act)s;%(params)s">darcs</a>
 481 		""" % { "myreponame": config.myreponame, "act": efaction,
 482 			"params": params }
 483 
 484 		# darcs, raw, if available; and only for commitdiff
 485 		realf = filter_file(config.repodir + '/_darcs/patches/' + h)
 486 		if efaction == "commitdiff" and os.path.isfile(realf):
 487 			print """
 488 | <a class="link" href="%(myreponame)s;a=raw_%(act)s;%(params)s">raw</a>
 489 			""" % { "myreponame": config.myreponame,
 490 				"act": efaction, "params": params }
 491 
 492 	elif f and action == "headblob":
 493 		# show the only alternative format: plain
 494 		print """
 495 <a class="link" href="%(myreponame)s;a=plainblob;f=%(fname)s">plain</a>
 496 		""" % { "myreponame": config.myreponame, "fname": f }
 497 
 498 	elif f and h and action.startswith("annotate"):
 499 		# same for annotate
 500 		print """
 501 <a href="%(myreponame)s;a=annotate_normal;f=%(fname)s;h=%(hash)s">normal</a>
 502 | <a href="%(myreponame)s;a=annotate_plain;f=%(fname)s;h=%(hash)s">plain</a>
 503 | <a href="%(myreponame)s;a=annotate_shade;f=%(fname)s;h=%(hash)s">shade</a>
 504 | <a href="%(myreponame)s;a=annotate_zebra;f=%(fname)s;h=%(hash)s">zebra</a>
 505 		""" % {
 506 			"myreponame": config.myreponame,
 507 			"fname": f,
 508 			"hash": h
 509 		}
 510 
 511 	print '<br/>'
 512 	print '</div>'
 513 
 514 def print_plain_header():
 515 	print "Content-type: text/plain; charset=utf-8\n"
 516 
 517 def print_binary_header(fname = None):
 518 	import mimetypes
 519 	if fname :
 520 		(mime, enc) = mimetypes.guess_type(fname)
 521 	else :
 522 		mime = None
 523 	if mime :
 524 		print "Content-type: %s" % mime
 525 	else :
 526 		print "Content-type: application/octet-stream"
 527 	if fname:
 528 		print "Content-Disposition:attachment;filename=%s" % fname
 529 	print
 530 
 531 def gen_authorlink(author, shortauthor=None):
 532 	if not config.author_links:
 533 		if shortauthor:
 534 			return shortauthor
 535 		else:
 536 			return author
 537 	if not shortauthor:
 538 		shortauthor = author
 539 	return '<a href="' + config.author_links % { 'author': author } + '">%s</a>' % shortauthor
 540 
 541 #
 542 # basic caching
 543 #
 544 
 545 class Cache:
 546 	def __init__(self, basedir, url):
 547 		import sha
 548 		self.basedir = basedir
 549 		self.url = url
 550 		self.fname = sha.sha(repr(url)).hexdigest()
 551 		self.file = None
 552 		self.mode = None
 553 		self.real_stdout = sys.stdout
 554 
 555 	def open(self):
 556 		"Returns 1 on hit, 0 on miss"
 557 		fname = self.basedir + '/' + self.fname
 558 
 559 		if not os.access(fname, os.R_OK):
 560 			# the file doesn't exist, direct miss
 561 			pid = str(os.getpid())
 562 			fname = self.basedir + '/.' + self.fname + '-' + pid
 563 			self.file = open(fname, 'w')
 564 			self.mode = 'w'
 565 			os.chmod(fname, stat.S_IRUSR | stat.S_IWUSR)
 566 
 567 			# step over stdout so when "print" tries to write
 568 			# output, we get it first
 569 			sys.stdout = self
 570 			return 0
 571 
 572 		inv = config.repodir + '/_darcs/patches'
 573 		cache_lastmod = os.stat(fname).st_mtime
 574 		repo_lastmod = os.stat(inv).st_mtime
 575 		dw_lastmod = os.stat(sys.argv[0]).st_mtime
 576 
 577 		if repo_lastmod > cache_lastmod or dw_lastmod > cache_lastmod:
 578 			# the entry is too old, remove it and return a miss
 579 			os.unlink(fname)
 580 
 581 			pid = str(os.getpid())
 582 			fname = self.basedir + '/.' + self.fname + '-' + pid
 583 			self.file = open(fname, 'w')
 584 			self.mode = 'w'
 585 			sys.stdout = self
 586 			return 0
 587 
 588 		# the entry is still valid, hit!
 589 		self.file = open(fname, 'r')
 590 		self.mode = 'r'
 591 		return 1
 592 
 593 
 594 	def dump(self):
 595 		for l in self.file:
 596 			self.real_stdout.write(l)
 597 
 598 	def write(self, s):
 599 		# this gets called from print, because we replaced stdout with
 600 		# ourselves
 601 		self.file.write(s)
 602 		self.real_stdout.write(s)
 603 
 604 	def close(self):
 605 		if self.file:
 606 			self.file.close()
 607 		sys.stdout = self.real_stdout
 608 		if self.mode == 'w':
 609 			pid = str(os.getpid())
 610 			fname1 = self.basedir + '/.' + self.fname + '-' + pid
 611 			fname2 = self.basedir + '/' + self.fname
 612 			os.rename(fname1, fname2)
 613 			self.mode = 'c'
 614 
 615 	def cancel(self):
 616 		"Like close() but don't save the entry."
 617 		if self.file:
 618 			self.file.close()
 619 		sys.stdout = self.real_stdout
 620 		if self.mode == 'w':
 621 			pid = str(os.getpid())
 622 			fname = self.basedir + '/.' + self.fname + '-' + pid
 623 			os.unlink(fname)
 624 			self.mode = 'c'
 625 
 626 
 627 #
 628 # darcs repo manipulation
 629 #
 630 
 631 def repo_get_owner():
 632 	try:
 633 		fd = open(config.repodir + '/_darcs/prefs/author')
 634 		for line in fd:
 635 			line = line.strip()
 636 			if line != "" and line[0] != "#":
 637 				return line.strip()
 638 	except:
 639 		return None
 640 
 641 def run_darcs(params):
 642 	"""Runs darcs on the repodir with the given params, return a file
 643 	object with its output."""
 644 	os.chdir(config.repodir)
 645 	try:
 646 		original_8bit_setting = os.environ['DARCS_DONT_ESCAPE_8BIT']
 647 	except KeyError:
 648 		original_8bit_setting = None
 649 	os.environ['DARCS_DONT_ESCAPE_8BIT'] = '1'
 650 	cmd = config.darcspath + "darcs " + params
 651 	inf, outf = os.popen4(cmd, 't')
 652 	darcs_runs.append(params)
 653 	if original_8bit_setting == None:
 654 		del(os.environ['DARCS_DONT_ESCAPE_8BIT'])
 655 	else:
 656 		os.environ['DARCS_DONT_ESCAPE_8BIT'] = original_8bit_setting
 657 	return outf
 658 
 659 
 660 class Patch:
 661 	"Represents a single patch/record"
 662 	def __init__(self):
 663 		self.hash = ''
 664 		self.author = ''
 665 		self.shortauthor = ''
 666 		self.date = 0
 667 		self.local_date = 0
 668 		self.name = ''
 669 		self.comment = ''
 670 		self.inverted = False;
 671 		self.adds = []
 672 		self.removes = []
 673 		self.modifies = {}
 674 		self.diradds = []
 675 		self.dirremoves = []
 676 		self.replaces = {}
 677 		self.moves = {}
 678 
 679 	def tostr(self):
 680 		s = "%s\n\tAuthor: %s\n\tDate: %s\n\tHash: %s\n" % \
 681 			(self.name, self.author, self.date, self.hash)
 682 		return s
 683 
 684 	def getdiff(self):
 685 		"""Returns a list of lines from the diff -u corresponding with
 686 		the patch."""
 687 		params = 'diff --quiet -u --match "hash %s"' % self.hash
 688 		f = run_darcs(params)
 689 		return f.readlines()
 690 
 691 	def matches(self, s):
 692 		"Defines if the patch matches a given string"
 693 		if s.lower() in self.comment.lower():
 694 			return self.comment
 695 		elif s.lower() in self.name.lower():
 696 			return self.name
 697 		elif s.lower() in self.author.lower():
 698 			return self.author
 699 		elif s == self.hash:
 700 			return self.hash
 701 
 702 		s = s.lower()
 703 		for l in (self.adds, self.removes, self.modifies,
 704 				self.diradds, self.dirremoves,
 705 				self.replaces.keys(), self.moves.keys(),
 706 				self.moves.keys() ):
 707 			for i in l:
 708 				if s in i.lower():
 709 					return i
 710 		return ''
 711 
 712 class XmlInputWrapper:
 713 	def __init__(self, fd):
 714 		self.fd = fd
 715 		self.times = 0
 716 		self._read = self.read
 717 
 718 	def read(self, *args, **kwargs):
 719 		self.times += 1
 720 		if self.times == 1:
 721 			return '<?xml version="1.0" encoding="utf-8"?>\n'
 722 		s = self.fd.read(*args, **kwargs)
 723 		if not s:
 724 			return s
 725 		return fixu8(s)
 726 
 727 	def close(self, *args, **kwargs):
 728 		return self.fd.close(*args, **kwargs)
 729 
 730 
 731 # patch parsing, we get them through "darcs changes --xml-output"
 732 class BuildPatchList(xml.sax.handler.ContentHandler):
 733 	def __init__(self):
 734 		self.db = {}
 735 		self.list = []
 736 		self.cur_hash = ''
 737 		self.cur_elem = None
 738 		self.cur_val = ''
 739 		self.cur_file = ''
 740 
 741 	def startElement(self, name, attrs):
 742 		# When you ask for changes to a given file, the xml output
 743 		# begins with the patch that creates it is enclosed in a
 744 		# "created_as" tag; then, later, it gets shown again in its
 745 		# usual place. The following two "if"s take care of ignoring
 746 		# everything inside the "created_as" tag, since we don't care.
 747 		if name == 'created_as':
 748 			self.cur_elem = 'created_as'
 749 			return
 750 		if self.cur_elem == 'created_as':
 751 			return
 752 
 753 		# now parse the tags normally
 754 		if name == 'patch':
 755 			p = Patch()
 756 			p.hash = fixu8(attrs.get('hash'))
 757 
 758 			au = attrs.get('author', None)
 759 			p.author = fixu8(escape(au))
 760 			if au.find('<') != -1:
 761 				au = au[:au.find('<')].strip()
 762 			p.shortauthor = fixu8(escape(au))
 763 
 764 			td = parse_darcs_time(attrs.get('date', None))
 765 			p.date = time.mktime(td)
 766 			p.date_str = time.strftime("%a, %d %b %Y %H:%M:%S", td)
 767 
 768 			td = time.strptime(attrs.get('local_date', None),
 769 					"%a %b %d %H:%M:%S %Z %Y")
 770 			p.local_date = time.mktime(td)
 771 			p.local_date_str = \
 772 				time.strftime("%a, %d %b %Y %H:%M:%S", td)
 773 
 774 			inverted = attrs.get('inverted', None)
 775 			if inverted and inverted == 'True':
 776 				p.inverted = True
 777 
 778 			self.db[p.hash] = p
 779 			self.current = p.hash
 780 			self.list.append(p.hash)
 781 		elif name == 'name':
 782 			self.db[self.current].name = ''
 783 			self.cur_elem = 'name'
 784 		elif name == 'comment':
 785 			self.db[self.current].comment = ''
 786 			self.cur_elem = 'comment'
 787 		elif name == 'add_file':
 788 			self.cur_elem = 'add_file'
 789 		elif name == 'remove_file':
 790 			self.cur_elem = 'remove_file'
 791 		elif name == 'add_directory':
 792 			self.cur_elem = 'add_directory'
 793 		elif name == 'remove_directory':
 794 			self.cur_elem = 'remove_dir'
 795 		elif name == 'modify_file':
 796 			self.cur_elem = 'modify_file'
 797 		elif name == 'removed_lines':
 798 			if self.cur_val:
 799 				self.cur_file = fixu8(self.cur_val.strip())
 800 			cf = self.cur_file
 801 			p = self.db[self.current]
 802 			# the current value holds the file name at this point
 803 			if not p.modifies.has_key(cf):
 804 				p.modifies[cf] = { '+': 0, '-': 0 }
 805 			p.modifies[cf]['-'] = int(attrs.get('num', None))
 806 		elif name == 'added_lines':
 807 			if self.cur_val:
 808 				self.cur_file = fixu8(self.cur_val.strip())
 809 			cf = self.cur_file
 810 			p = self.db[self.current]
 811 			if not p.modifies.has_key(cf):
 812 				p.modifies[cf] = { '+': 0, '-': 0 }
 813 			p.modifies[cf]['+'] = int(attrs.get('num', None))
 814 		elif name == 'move':
 815 			src = fixu8(attrs.get('from', None))
 816 			dst = fixu8(attrs.get('to', None))
 817 			p = self.db[self.current]
 818 			p.moves[src] = dst
 819 		elif name == 'replaced_tokens':
 820 			if self.cur_val:
 821 				self.cur_file = fixu8(self.cur_val.strip())
 822 			cf = self.cur_file
 823 			p = self.db[self.current]
 824 			if not p.replaces.has_key(cf):
 825 				p.replaces[cf] = 0
 826 			p.replaces[cf] = int(attrs.get('num', None))
 827 		else:
 828 			self.cur_elem = None
 829 
 830 	def characters(self, s):
 831 		if not self.cur_elem:
 832 			return
 833 		self.cur_val += s
 834 
 835 	def endElement(self, name):
 836 		# See the comment in startElement()
 837 		if name == 'created_as':
 838 			self.cur_elem = None
 839 			self.cur_val = ''
 840 			return
 841 		if self.cur_elem == 'created_as':
 842 			return
 843 		if name == 'replaced_tokens':
 844 			return
 845 
 846 		if name == 'name':
 847 			p = self.db[self.current]
 848 			p.name = fixu8(self.cur_val)
 849 			if p.inverted:
 850 				p.name = 'UNDO: ' + p.name
 851 		elif name == 'comment':
 852 			self.db[self.current].comment = fixu8(strip_ignore_this(self.cur_val))
 853 		elif name == 'add_file':
 854 			scv = fixu8(self.cur_val.strip())
 855 			self.db[self.current].adds.append(scv)
 856 		elif name == 'remove_file':
 857 			scv = fixu8(self.cur_val.strip())
 858 			self.db[self.current].removes.append(scv)
 859 		elif name == 'add_directory':
 860 			scv = fixu8(self.cur_val.strip())
 861 			self.db[self.current].diradds.append(scv)
 862 		elif name == 'remove_directory':
 863 			scv = fixu8(self.cur_val.strip())
 864 			self.db[self.current].dirremoves.append(scv)
 865 
 866 		elif name == 'modify_file':
 867 			if not self.cur_file:
 868 				# binary modification appear without a line
 869 				# change summary, so we add it manually here
 870 				f = fixu8(self.cur_val.strip())
 871 				p = self.db[self.current]
 872 				p.modifies[f] = { '+': 0, '-': 0, 'b': 1 }
 873 			self.cur_file = ''
 874 
 875 		self.cur_elem = None
 876 		self.cur_val = ''
 877 
 878 	def get_list(self):
 879 		plist = []
 880 		for h in self.list:
 881 			plist.append(self.db[h])
 882 		return plist
 883 
 884 	def get_db(self):
 885 		return self.db
 886 
 887 	def get_list_db(self):
 888 		return (self.list, self.db)
 889 
 890 def get_changes_handler(params):
 891 	"Returns a handler for the changes output, run with the given params"
 892 	parser = xml.sax.make_parser()
 893 	handler = BuildPatchList()
 894 	parser.setContentHandler(handler)
 895 
 896 	# get the xml output and parse it
 897 	xmlf = run_darcs("changes --xml-output " + params)
 898 	parser.parse(XmlInputWrapper(xmlf))
 899 	xmlf.close()
 900 
 901 	return handler
 902 
 903 def get_last_patches(last = 15, topi = 0, fname = None):
 904 	"""Gets the last N patches from the repo, returns a patch list. If
 905 	"topi" is specified, then it will return the N patches that preceeded
 906 	the patch number topi in the list. It sounds messy but it's quite
 907 	simple. You can optionally pass a filename and only changes that
 908 	affect it will be returned. FIXME: there's probably a more efficient
 909 	way of doing this."""
 910 
 911 	# darcs calculate last first, and then filters the filename,
 912 	# so it's not so simple to combine them; that's why we do so much
 913 	# special casing here
 914 	toget = last + topi
 915 
 916 	if fname:
 917 		if fname[0] == '/': fname = fname[1:]
 918 		s = '-s "%s"' % fname
 919 	else:
 920 		s = "-s --last=%d" % toget
 921 
 922 	handler = get_changes_handler(s)
 923 
 924 	# return the list of all the patch objects
 925 	return handler.get_list()[topi:toget]
 926 
 927 def get_patch(hash):
 928 	handler = get_changes_handler('-s --match "hash %s"' % hash)
 929 	patch = handler.db[handler.list[0]]
 930 	return patch
 931 
 932 def get_diff(hash):
 933 	return run_darcs('diff --quiet -u --match "hash %s"' % hash)
 934 
 935 def get_file_diff(hash, fname):
 936 	return run_darcs('diff --quiet -u --match "hash %s" "%s"' % (hash, fname))
 937 
 938 def get_file_headdiff(hash, fname):
 939 	return run_darcs('diff --quiet -u --from-match "hash %s" "%s"' % (hash, fname))
 940 
 941 def get_patch_headdiff(hash):
 942 	return run_darcs('diff --quiet -u --from-match "hash %s"' % hash)
 943 
 944 def get_raw_diff(hash):
 945 	import gzip
 946 	realf = filter_file(config.repodir + '/_darcs/patches/' + hash)
 947 	if not os.path.isfile(realf):
 948 		return None
 949 	file = open(realf, 'rb')
 950 	if file.read(2) == '\x1f\x8b':
 951 		# file begins with gzip magic
 952 		file.close()
 953 		dsrc = gzip.open(realf)
 954 	else:
 955 		file.seek(0)
 956 		dsrc = file
 957 	return dsrc
 958 
 959 def get_darcs_diff(hash, fname = None):
 960 	cmd = 'changes -v --matches "hash %s"' % hash
 961 	if fname:
 962 		cmd += ' "%s"' % fname
 963 	return run_darcs(cmd)
 964 
 965 def get_darcs_headdiff(hash, fname = None):
 966 	cmd = 'changes -v --from-match "hash %s"' % hash
 967 	if fname:
 968 		cmd += ' "%s"' % fname
 969 	return run_darcs(cmd)
 970 
 971 
 972 class Annotate:
 973 	def __init__(self):
 974 		self.fname = ""
 975 		self.creator_hash = ""
 976 		self.created_as = ""
 977 		self.lastchange_hash = ""
 978 		self.lastchange_author = ""
 979 		self.lastchange_name = ""
 980 		self.lastchange_date = None
 981 		self.firstdate = None
 982 		self.lastdate = None
 983 		self.lines = []
 984 		self.patches = {}
 985 
 986 	class Line:
 987 		def __init__(self):
 988 			self.text = ""
 989 			self.phash = None
 990 			self.pauthor = None
 991 			self.pdate = None
 992 
 993 def parse_annotate(src):
 994 	import xml.dom.minidom
 995 
 996 	annotate = Annotate()
 997 
 998 	# FIXME: convert the source to UTF8; it _has_ to be a way to let
 999 	# minidom know the source encoding
1000 	s = ""
1001 	for i in src:
1002 		s += fixu8(i).replace('', '^L')
1003 
1004 	dom = xml.dom.minidom.parseString(s)
1005 
1006 	file = dom.getElementsByTagName("file")[0]
1007 	annotate.fname = fixu8(file.getAttribute("name"))
1008 
1009 	createinfo = dom.getElementsByTagName("created_as")[0]
1010 	annotate.created_as = fixu8(createinfo.getAttribute("original_name"))
1011 
1012 	creator = createinfo.getElementsByTagName("patch")[0]
1013 	annotate.creator_hash = fixu8(creator.getAttribute("hash"))
1014 
1015 	mod = dom.getElementsByTagName("modified")[0]
1016 	lastpatch = mod.getElementsByTagName("patch")[0]
1017 	annotate.lastchange_hash = fixu8(lastpatch.getAttribute("hash"))
1018 	annotate.lastchange_author = fixu8(lastpatch.getAttribute("author"))
1019 
1020 	lastname = lastpatch.getElementsByTagName("name")[0]
1021 	lastname = lastname.childNodes[0].wholeText
1022 	annotate.lastchange_name = fixu8(lastname)
1023 
1024 	lastdate = parse_darcs_time(lastpatch.getAttribute("date"))
1025 	annotate.lastchange_date = lastdate
1026 
1027 	annotate.patches[annotate.lastchange_hash] = annotate.lastchange_date
1028 
1029 	# these will be overriden by the real dates later
1030 	annotate.firstdate = lastdate
1031 	annotate.lastdate = 0
1032 
1033 	file = dom.getElementsByTagName("file")[0]
1034 
1035 	for l in file.childNodes:
1036 		# we're only intrested in normal and added lines
1037 		if l.nodeName not in ["normal_line", "added_line"]:
1038 			continue
1039 		line = Annotate.Line()
1040 
1041 		if l.nodeName == "normal_line":
1042 			patch = l.getElementsByTagName("patch")[0]
1043 			phash = patch.getAttribute("hash")
1044 			pauthor = patch.getAttribute("author")
1045 			pdate = patch.getAttribute("date")
1046 			pdate = parse_darcs_time(pdate)
1047 		else:
1048 			# added lines inherit the creation from the annotate
1049 			# patch
1050 			phash = annotate.lastchange_hash
1051 			pauthor = annotate.lastchange_author
1052 			pdate = annotate.lastchange_date
1053 
1054 		text = ""
1055 		for node in l.childNodes:
1056 			if node.nodeType == node.TEXT_NODE:
1057 				text += node.wholeText
1058 
1059 		# strip all "\n"s at the beginning; because the way darcs
1060 		# formats the xml output it makes the DOM parser to add "\n"s
1061 		# in front of it
1062 		text = text.lstrip("\n")
1063 
1064 		line.text = fixu8(text)
1065 		line.phash = fixu8(phash)
1066 		line.pauthor = fixu8(pauthor)
1067 		line.pdate = pdate
1068 		annotate.lines.append(line)
1069 		annotate.patches[line.phash] = line.pdate
1070 
1071 		if pdate > annotate.lastdate:
1072 			annotate.lastdate = pdate
1073 		if pdate < annotate.firstdate:
1074 			annotate.firstdate = pdate
1075 
1076 	return annotate
1077 
1078 def get_annotate(fname, hash = None):
1079 	if config.disable_annotate:
1080 		return None
1081 
1082 	cmd = 'annotate --xml-output'
1083 	if hash:
1084 		cmd += ' --match="hash %s"' % hash
1085 
1086 	if fname.startswith('/'):
1087 		# darcs 2 doesn't like files starting with /, and darcs 1
1088 		# doesn't really care
1089 		fname = fname[1:]
1090 	cmd += ' "%s"' % fname
1091 
1092 	return parse_annotate(run_darcs(cmd))
1093 
1094 def get_readme():
1095 	import glob
1096 	readmes = glob.glob("README*")
1097 	if len(readmes) == 0: return False, False
1098 	import re
1099 	for p in readmes:
1100 		file = os.path.basename(p)
1101 		if re.search('\.(md|markdown)$', p):
1102 			import codecs
1103 			import markdown
1104 			f = codecs.open(p, encoding=config.repoencoding[0])
1105 			str = f.read()
1106 			html = markdown.markdown(str, ['extra', 'codehilite(css_class=page_body)'])
1107 			return file, fixu8(html)
1108 		elif re.search('README$', p):
1109 			f = open(p)
1110 			str = f.read()
1111 			return file, '<pre>%s</pre>' % fixu8(escape(str))
1112 	# We can't handle this ourselves, try shelling out
1113 	if not config.readme_converter: return False, False
1114 	cmd = '%s "%s"' % (config.readme_converter, readmes[0])
1115 	inf, outf = os.popen2(cmd, 't')
1116 	return os.path.basename(readmes[0]), fixu8(outf.read())
1117 
1118 
1119 #
1120 # specific html functions
1121 #
1122 
1123 def print_diff(dsrc):
1124 	for l in dsrc:
1125 		l = fixu8(l)
1126 
1127 		# remove the trailing newline
1128 		if len(l) > 1:
1129 			l = l[:-1]
1130 
1131 		if l.startswith('diff'):
1132 			# file lines, they have their own class
1133 			print '<div class="diff_info">%s</div>' % escape(l)
1134 			continue
1135 
1136 		color = ""
1137 		if l[0] == '+':
1138 			color = 'style="color:#008800;"'
1139 		elif l[0] == '-':
1140 			color = 'style="color:#cc0000;"'
1141 		elif l[0] == '@':
1142 			color = 'style="color:#990099; '
1143 			color += 'border: solid #ffe0ff; '
1144 			color += 'border-width: 1px 0px 0px 0px; '
1145 			color += 'margin-top: 2px;"'
1146 		elif l.startswith('Files'):
1147 			# binary differences
1148 			color = 'style="color:#666;"'
1149 		print '<div class="pre" %s>' % color + escape(l) + '</div>'
1150 
1151 
1152 def print_darcs_diff(dsrc):
1153 	for l in dsrc:
1154 		l = fixu8(l)
1155 
1156 		if not l.startswith("    "):
1157 			# comments and normal stuff
1158 			print '<div class="pre">' + escape(l) + "</div>"
1159 			continue
1160 
1161 		l = l.strip()
1162 		if not l:
1163 			continue
1164 
1165 		if l[0] == '+':
1166 			cl = 'class="pre" style="color:#008800;"'
1167 		elif l[0] == '-':
1168 			cl = 'class="pre" style="color:#cc0000;"'
1169 		else:
1170 			cl = 'class="diff_info"'
1171 		print '<div %s>' % cl + escape(l) + '</div>'
1172 
1173 
1174 def print_shortlog(last = PATCHES_PER_PAGE, topi = 0, fname = None):
1175 	ps = get_last_patches(last, topi, fname)
1176 
1177 	if fname:
1178 		title = '<a class="title" href="%s;a=filehistory;f=%s">' % \
1179 				(config.myreponame, fname)
1180 		title += 'History for path %s' % escape(fname)
1181 		title += '</a>'
1182 	else:
1183 		title = '<a class="title" href="%s;a=shortlog">shortlog</a>' \
1184 				% config.myreponame
1185 
1186 	print '<div>%s</div>' % title
1187 	print '<table cellspacing="0">'
1188 
1189 	if topi != 0:
1190 		# put a link to the previous page
1191 		ntopi = topi - last
1192 		if ntopi < 0:
1193 			ntopi = 0
1194 		print '<tr><td>'
1195 		if fname:
1196 			print '<a href="%s;a=filehistory;topi=%d;f=%s">...</a>' \
1197 				% (config.myreponame, ntopi, fname)
1198 		else:
1199 			print '<a href="%s;a=shortlog;topi=%d">...</a>' \
1200 				% (config.myreponame, ntopi)
1201 		print '</td></tr>'
1202 
1203 	alt = True
1204 	for p in ps:
1205 		if p.name.startswith("TAG "):
1206 			print '<tr class="tag">'
1207 		elif alt:
1208 			print '<tr class="dark">'
1209 		else:
1210 			print '<tr class="light">'
1211 		alt = not alt
1212 
1213 		print """
1214   <td><i>%(age)s</i></td>
1215   <td>%(author)s</td>
1216   <td>
1217     <a class="list" title="%(fullname)s" href="%(myrname)s;a=commit;h=%(hash)s">
1218       <b>%(name)s</b>
1219     </a>
1220   </td>
1221   <td class="link">
1222     <a href="%(myrname)s;a=commit;h=%(hash)s">commit</a> |
1223     <a href="%(myrname)s;a=commitdiff;h=%(hash)s">commitdiff</a>
1224   </td>
1225 		""" % {
1226 			'age': how_old(p.local_date),
1227 			'author': gen_authorlink(p.author, shorten_str(p.shortauthor, 26)),
1228 			'myrname': config.myreponame,
1229 			'hash': p.hash,
1230 			'name': escape(shorten_str(p.name)),
1231 			'fullname': escape(p.name),
1232 		}
1233 		print "</tr>"
1234 
1235 	if len(ps) >= last:
1236 		# only show if we've not shown them all already
1237 		print '<tr><td>'
1238 		if fname:
1239 			print '<a href="%s;a=filehistory;topi=%d;f=%s">...</a>' \
1240 				% (config.myreponame, topi + last, fname)
1241 		else:
1242 			print '<a href="%s;a=shortlog;topi=%d">...</a>' \
1243 				% (config.myreponame, topi + last)
1244 		print '</td></tr>'
1245 	print "</table>"
1246 
1247 
1248 def print_readme():
1249 	head, body = get_readme()
1250 	if not head: return False
1251 	print '<div class="title">%s</div>' % head
1252 	print '<section>%s</section' % body
1253 
1254 
1255 def print_log(last = PATCHES_PER_PAGE, topi = 0):
1256 	ps = get_last_patches(last, topi)
1257 
1258 	if topi != 0:
1259 		# put a link to the previous page
1260 		ntopi = topi - last
1261 		if ntopi < 0:
1262 			ntopi = 0
1263 		print '<p/><a href="%s;a=log;topi=%d">&lt;- Prev</a><p/>' % \
1264 				(config.myreponame, ntopi)
1265 
1266 	for p in ps:
1267 		if p.comment:
1268 			comment = replace_links(escape(p.comment))
1269 			fmt_comment = comment.replace('\n', '<br/>') + '\n'
1270 			fmt_comment += '<br/><br/>'
1271 		else:
1272 			fmt_comment = ''
1273 		print """
1274 <div><a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1275     <span class="age">%(age)s</span>%(desc)s
1276 </a></div>
1277 <div class="title_text">
1278   <div class="log_link">
1279     <a href="%(myreponame)s;a=commit;h=%(hash)s">commit</a> |
1280     <a href="%(myreponame)s;a=commitdiff;h=%(hash)s">commitdiff</a><br/>
1281   </div>
1282   <i>%(author)s [%(date)s]</i><br/>
1283 </div>
1284 <div class="log_body">
1285   %(comment)s
1286 </div>
1287 
1288 		""" % {
1289 			'myreponame': config.myreponame,
1290 			'age': how_old(p.local_date),
1291 			'date': p.local_date_str,
1292 			'author': gen_authorlink(p.author, p.shortauthor),
1293 			'hash': p.hash,
1294 			'desc': escape(p.name),
1295 			'comment': fmt_comment
1296 		}
1297 
1298 	if len(ps) >= last:
1299 		# only show if we've not shown them all already
1300 		print '<p><a href="%s;a=log;topi=%d">Next -&gt;</a></p>' % \
1301 				(config.myreponame, topi + last)
1302 
1303 
1304 def print_blob(fname):
1305 	print '<div class="page_path"><b>%s</b></div>' % escape(fname)
1306 	if isbinary(fname):
1307 		print """
1308 <div class="page_body">
1309 <i>This is a binary file and its contents will not be displayed.</i>
1310 </div>
1311 		"""
1312 		return
1313 
1314 	try:
1315 		import pygments
1316 	except ImportError:
1317 		pygments = False
1318 
1319 	if not pygments:
1320 		print_blob_simple(fname)
1321 		return
1322 	else:
1323 		try:
1324 			print_blob_highlighted(fname)
1325 		except ValueError:
1326 			# pygments couldn't guess a lexer to highlight the code, try
1327 			# another method with sampling the file contents.
1328 			try:
1329 				print_blob_highlighted(fname, sample_code=True)
1330 			except ValueError:
1331 				# pygments really could not find any lexer for this file.
1332 				print_blob_simple(fname)
1333 
1334 def print_blob_simple(fname):
1335 	print '<div class="page_body">'
1336 
1337 	f = open(realpath(fname), 'r')
1338 	count = 1
1339 	for l in f:
1340 		l = fixu8(escape(l))
1341 		if l and l[-1] == '\n':
1342 			l = l[:-1]
1343 		l = replace_tabs(l)
1344 
1345 		print """\
1346 <div class="pre">\
1347 <a id="l%(c)d" href="#l%(c)d" class="linenr">%(c)4d</a> %(l)s\
1348 </div>\
1349 		""" % {
1350 			'c': count,
1351 			'l': l
1352 		}
1353 		count += 1
1354 	print '</div>'
1355 
1356 def print_blob_highlighted(fname, sample_code=False):
1357 	import pygments
1358 	import pygments.lexers
1359 	import pygments.formatters
1360 
1361 	code = open(realpath(fname), 'r').read()
1362 	if sample_code:
1363 		lexer = pygments.lexers.guess_lexer(code[:200],
1364 				encoding=config.repoencoding[0])
1365 	else:
1366 		lexer = pygments.lexers.guess_lexer_for_filename(fname, code[:200],
1367 				encoding=config.repoencoding[0])
1368 
1369 	pygments_version = map(int, pygments.__version__.split('.'))
1370 	if pygments_version >= [0, 7]:
1371 		linenos_method = 'inline'
1372 	else:
1373 		linenos_method = True
1374 	formatter = pygments.formatters.HtmlFormatter(linenos=linenos_method,
1375 				cssclass='page_body')
1376 
1377 	print pygments.highlight(code, lexer, formatter)
1378 	print """<script type='text/javascript'>
1379 (function () {
1380   if (!document.getElementsByClassName || !document.evaluate || !window.addEventListener)
1381     return;
1382 
1383   function fakeAnchors () {
1384     var pbody = document.getElementsByClassName("page_body")[0];
1385     var anchor = window.location.hash;
1386     if (!pbody || "" == anchor || "#" == anchor) return;
1387 
1388     /* Avoid xpath injection, because so far as I can tell there's no way to set
1389        xpath variables from JavaScript. */
1390     if (anchor.match(/['"]/)) return;
1391 
1392     anchor = anchor.substr(1);
1393     var parts = anchor.split(/ /);
1394     var xpq = '//span[string()="'+parts[0]+'"]';
1395     for (var i = 1; i < parts.length; i++) {
1396       xpq += '//following-sibling::span[1][string()="'+parts[i]+'"]';
1397     }
1398     var res = document.evaluate(xpq, pbody, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
1399     if (!res || !res.singleNodeValue) return;
1400 
1401     res.singleNodeValue.scrollIntoView(true);
1402   };
1403   window.addEventListener("DOMContentLoaded", fakeAnchors, false);
1404   window.addEventListener("hashchange", fakeAnchors, false);
1405 })();
1406 </script>"""
1407 
1408 def print_annotate(ann, style):
1409 	print '<div class="page_body">'
1410 	if isbinary(ann.fname):
1411 		print """
1412 <i>This is a binary file and its contents will not be displayed.</i>
1413 </div>
1414 		"""
1415 		return
1416 
1417 	if style == 'shade':
1418 		# here's the idea: we will assign to each patch a shade of
1419 		# color from its date (newer gets darker)
1420 		max = 0xff
1421 		min = max - 80
1422 
1423 		# to do that, we need to get a list of the patch hashes
1424 		# ordered by their dates
1425 		l = [ (date, hash) for (hash, date) in ann.patches.items() ]
1426 		l.sort()
1427 		l = [ hash for (date, hash) in l ]
1428 
1429 		# now we have to map each element to a number in the range
1430 		# min-max, with max being close to l[0] and min l[len(l) - 1]
1431 		lenn = max - min
1432 		lenl = len(l)
1433 		shadetable = {}
1434 		for i in range(0, lenl):
1435 			hash = l[i]
1436 			n = float(i * lenn) / lenl
1437 			n = max - int(round(n))
1438 			shadetable[hash] = n
1439 	elif style == "zebra":
1440 		lineclass = 'dark'
1441 
1442 	count = 1
1443 	prevhash = None
1444 	for l in ann.lines:
1445 		text = escape(l.text)
1446 		text = text.rstrip()
1447 		text = replace_tabs(text)
1448 		plongdate = time.strftime("%Y-%m-%d %H:%M:%S", l.pdate)
1449 		title = "%s by %s" % (plongdate, escape(l.pauthor) )
1450 
1451 		link = "%(myrname)s;a=commit;h=%(hash)s" % {
1452 			'myrname': config.myreponame,
1453 			'hash': l.phash
1454 		}
1455 
1456 		if style == "shade":
1457 			linestyle = 'style="background-color:#ffff%.2x"' % \
1458 					shadetable[l.phash]
1459 			lineclass = ''
1460 		elif style == "zebra":
1461 			linestyle = ''
1462 			if l.phash != prevhash:
1463 				if lineclass == 'dark':
1464 					lineclass = 'light'
1465 				else:
1466 					lineclass = 'dark'
1467 		else:
1468 			linestyle = ''
1469 			lineclass = ''
1470 
1471 		if l.phash != prevhash:
1472 			pdate = time.strftime("%Y-%m-%d", l.pdate)
1473 
1474 			left = l.pauthor.find('<')
1475 			right = l.pauthor.find('@')
1476 			if left != -1 and right != -1:
1477 				shortau = l.pauthor[left + 1:right]
1478 			elif l.pauthor.find(" ") != -1:
1479 				shortau = l.pauthor[:l.pauthor.find(" ")]
1480 			elif right != -1:
1481 				shortau = l.pauthor[:right]
1482 			else:
1483 				shortau = l.pauthor
1484 
1485 			desc = "%12.12s" % shortau
1486 			date = "%-10.10s" % pdate
1487 			prevhash = l.phash
1488 			line = 1
1489 		else:
1490 			if line == 1 and style in ["shade", "zebra"]:
1491 				t = "%s  " % time.strftime("%H:%M:%S", l.pdate)
1492 				desc = "%12.12s" % "'"
1493 				date = "%-10.10s" % t
1494 			else:
1495 				desc = "%12.12s" % "'"
1496 				date = "%-10.10s" % ""
1497 			line += 1
1498 
1499 		print """\
1500 <div class="pre %(class)s" %(style)s>\
1501 <a href="%(link)s" title="%(title)s" class="annotate_desc">%(date)s %(desc)s</a> \
1502 <a href="%(link)s" title="%(title)s" class="linenr">%(c)4d</a> \
1503 <a href="%(link)s" title="%(title)s" class="line">%(text)s</a>\
1504 </div>\
1505 		""" % {
1506 			'class': lineclass,
1507 			'style': linestyle,
1508 			'date': date,
1509 			'desc': escape(desc),
1510 			'c': count,
1511 			'text': text,
1512 			'title': title,
1513 			'link': link
1514 		}
1515 
1516 		count += 1
1517 
1518 	print '</div>'
1519 
1520 
1521 #
1522 # available actions
1523 #
1524 
1525 def do_summary():
1526 	print_header()
1527 	print_navbar()
1528 	owner = repo_get_owner()
1529 
1530 	# we should optimize this, it's a pity to go in such a mess for just
1531 	# one hash
1532 	ps = get_last_patches(1)
1533 
1534 	print '<div class="title">&nbsp;</div>'
1535 	print '<table cellspacing="0">'
1536 	print '  <tr><td>description</td><td>%s</td></tr>' % \
1537 			escape(config.repodesc)
1538 	if owner:
1539 		print '  <tr><td>owner</td><td>%s</td></tr>' % escape(owner)
1540 	if len(ps) > 0:
1541 		print '  <tr><td>last change</td><td>%s</td></tr>' % \
1542 			ps[0].local_date_str
1543 	print '  <tr><td>url</td><td><a href="%(url)s">%(url)s</a></td></tr>' %\
1544 			{ 'url': config.repourl }
1545 	if config.repoprojurl:
1546 		print '  <tr><td>project url</td>'
1547 		print '  <td><a href="%(url)s">%(url)s</a></td></tr>' % \
1548 			{ 'url': config.repoprojurl }
1549 	if config.repolisturl:
1550 		print '  <tr><td>mailing list url</td>'
1551 		print '  <td><a href="%(url)s">%(url)s</a></td></tr>' % \
1552 			{ 'url': config.repolisturl }
1553 	print '</table>'
1554 
1555 	print_shortlog(15)
1556 	print_readme()
1557 	print_footer()
1558 
1559 
1560 def do_commitdiff(phash):
1561 	print_header()
1562 	print_navbar(h = phash)
1563 	p = get_patch(phash)
1564 	print """
1565 <div>
1566   <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1567 </div>
1568 	""" % {
1569 		'myreponame': config.myreponame,
1570 		'hash': p.hash,
1571 		'name': escape(p.name),
1572 	}
1573 
1574 	dsrc = p.getdiff()
1575 	print_diff(dsrc)
1576 	print_footer()
1577 
1578 def do_plain_commitdiff(phash):
1579 	print_plain_header()
1580 	dsrc = get_diff(phash)
1581 	for l in dsrc:
1582 		sys.stdout.write(fixu8(l))
1583 
1584 def do_darcs_commitdiff(phash):
1585 	print_header()
1586 	print_navbar(h = phash)
1587 	p = get_patch(phash)
1588 	print """
1589 <div>
1590   <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1591 </div>
1592 	""" % {
1593 		'myreponame': config.myreponame,
1594 		'hash': p.hash,
1595 		'name': escape(p.name),
1596 	}
1597 
1598 	dsrc = get_darcs_diff(phash)
1599 	print_darcs_diff(dsrc)
1600 	print_footer()
1601 
1602 def do_raw_commitdiff(phash):
1603 	print_plain_header()
1604 	dsrc = get_raw_diff(phash)
1605 	if not dsrc:
1606 		print "Error opening file!"
1607 		return
1608 	for l in dsrc:
1609 		sys.stdout.write(l)
1610 
1611 
1612 def do_headdiff(phash):
1613 	print_header()
1614 	print_navbar(h = phash)
1615 	p = get_patch(phash)
1616 	print """
1617 <div>
1618   <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1619     %(name)s --&gt; to head</a>
1620 </div>
1621 	""" % {
1622 		'myreponame': config.myreponame,
1623 		'hash': p.hash,
1624 		'name': escape(p.name),
1625 	}
1626 
1627 	dsrc = get_patch_headdiff(phash)
1628 	print_diff(dsrc)
1629 	print_footer()
1630 
1631 def do_plain_headdiff(phash):
1632 	print_plain_header()
1633 	dsrc = get_patch_headdiff(phash)
1634 	for l in dsrc:
1635 		sys.stdout.write(fixu8(l))
1636 
1637 def do_darcs_headdiff(phash):
1638 	print_header()
1639 	print_navbar(h = phash)
1640 	p = get_patch(phash)
1641 	print """
1642 <div>
1643   <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1644     %(name)s --&gt; to head</a>
1645 </div>
1646 	""" % {
1647 		'myreponame': config.myreponame,
1648 		'hash': p.hash,
1649 		'name': escape(p.name),
1650 	}
1651 
1652 	dsrc = get_darcs_headdiff(phash)
1653 	print_darcs_diff(dsrc)
1654 	print_footer()
1655 
1656 def do_raw_headdiff(phash):
1657 	print_plain_header()
1658 	dsrc = get_darcs_headdiff(phash)
1659 	for l in dsrc:
1660 		sys.stdout.write(l)
1661 
1662 
1663 def do_filediff(phash, fname):
1664 	print_header()
1665 	print_navbar(h = phash, f = fname)
1666 	p = get_patch(phash)
1667 	dsrc = get_file_diff(phash, fname)
1668 	print """
1669 <div>
1670   <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1671 </div>
1672 <div class="page_path"><b>%(fname)s</b></div>
1673 	""" % {
1674 		'myreponame': config.myreponame,
1675 		'hash': p.hash,
1676 		'name': escape(p.name),
1677 		'fname': escape(fname),
1678 	}
1679 
1680 	print_diff(dsrc)
1681 	print_footer()
1682 
1683 def do_plain_filediff(phash, fname):
1684 	print_plain_header()
1685 	dsrc = get_file_diff(phash, fname)
1686 	for l in dsrc:
1687 		sys.stdout.write(fixu8(l))
1688 
1689 def do_darcs_filediff(phash, fname):
1690 	print_header()
1691 	print_navbar(h = phash, f = fname)
1692 	p = get_patch(phash)
1693 	print """
1694 <div>
1695   <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1696 </div>
1697 <div class="page_path"><b>%(fname)s</b></div>
1698 	""" % {
1699 		'myreponame': config.myreponame,
1700 		'hash': p.hash,
1701 		'name': escape(p.name),
1702 		'fname': escape(fname),
1703 	}
1704 
1705 	dsrc = get_darcs_diff(phash, fname)
1706 	print_darcs_diff(dsrc)
1707 	print_footer()
1708 
1709 
1710 def do_file_headdiff(phash, fname):
1711 	print_header()
1712 	print_navbar(h = phash, f = fname)
1713 	p = get_patch(phash)
1714 	dsrc = get_file_headdiff(phash, fname)
1715 	print """
1716 <div>
1717   <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1718     %(name)s --&gt; to head</a>
1719 </div>
1720 <div class="page_path"><b>%(fname)s</b></div>
1721 	""" % {
1722 		'myreponame': config.myreponame,
1723 		'hash': p.hash,
1724 		'name': escape(p.name),
1725 		'fname': escape(fname),
1726 	}
1727 
1728 	print_diff(dsrc)
1729 	print_footer()
1730 
1731 def do_plain_fileheaddiff(phash, fname):
1732 	print_plain_header()
1733 	dsrc = get_file_headdiff(phash, fname)
1734 	for l in dsrc:
1735 		sys.stdout.write(fixu8(l))
1736 
1737 def do_darcs_fileheaddiff(phash, fname):
1738 	print_header()
1739 	print_navbar(h = phash, f = fname)
1740 	p = get_patch(phash)
1741 	print """
1742 <div>
1743   <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1744     %(name)s --&gt; to head</a>
1745 </div>
1746 <div class="page_path"><b>%(fname)s</b></div>
1747 	""" % {
1748 		'myreponame': config.myreponame,
1749 		'hash': p.hash,
1750 		'name': escape(p.name),
1751 		'fname': escape(fname),
1752 	}
1753 
1754 	dsrc = get_darcs_headdiff(phash, fname)
1755 	print_darcs_diff(dsrc)
1756 	print_footer()
1757 
1758 	print_plain_header()
1759 	print "Not yet implemented"
1760 
1761 
1762 def do_commit(phash):
1763 	print_header()
1764 	print_navbar(h = phash)
1765 	p = get_patch(phash)
1766 
1767 	print """
1768 <div>
1769   <a class="title" href="%(myreponame)s;a=commitdiff;h=%(hash)s">%(name)s</a>
1770 </div>
1771 
1772 <div class="title_text">
1773 <table cellspacing="0">
1774 <tr><td>author</td><td>%(author)s</td></tr>
1775 <tr><td>local date</td><td>%(local_date)s</td></tr>
1776 <tr><td>date</td><td>%(date)s</td></tr>
1777 <tr><td>hash</td><td style="font-family: monospace">%(hash)s</td></tr>
1778 </table>
1779 </div>
1780 	""" % {
1781 		'myreponame': config.myreponame,
1782 		'author': gen_authorlink(p.author),
1783 		'local_date': p.local_date_str,
1784 		'date': p.date_str,
1785 		'hash': p.hash,
1786 		'name': escape(p.name),
1787 	}
1788 	if p.comment:
1789 		comment = replace_links(escape(p.comment))
1790 		c = comment.replace('\n', '<br/>\n')
1791 		print '<div class="page_body">'
1792 		print replace_links(escape(p.name)), '<br/><br/>'
1793 		print c
1794 		print '</div>'
1795 
1796 	changed = p.adds + p.removes + p.modifies.keys() + p.moves.keys() + \
1797 			p.diradds + p.dirremoves + p.replaces.keys()
1798 
1799 	if changed or p.moves:
1800 		n = len(changed)
1801 		print '<div class="list_head">%d file(s) changed:</div>' % n
1802 
1803 	print '<table cellspacing="0">'
1804 	changed.sort()
1805 	alt = True
1806 	for f in changed:
1807 		if alt:
1808 			print '<tr class="dark">'
1809 		else:
1810 			print '<tr class="light">'
1811 		alt = not alt
1812 
1813 		show_diff = 1
1814 		if p.moves.has_key(f):
1815 			# don't show diffs for moves, they're broken as of
1816 			# darcs 1.0.3
1817 			show_diff = 0
1818 
1819 		if show_diff:
1820 			print """
1821 <td>
1822   <a class="list" href="%(myreponame)s;a=filediff;h=%(hash)s;f=%(file)s">
1823     %(fname)s</a>
1824 </td>
1825 			""" % {
1826 				'myreponame': config.myreponame,
1827 				'hash': p.hash,
1828 				'file': urllib.quote(f),
1829 				'fname': escape(f),
1830 			}
1831 		else:
1832 			print "<td>%s</td>" % f
1833 
1834 		show_diff = 1
1835 		if f in p.adds:
1836 			print '<td><span style="color:#008000">',
1837 			print '[added]',
1838 			print '</span></td>'
1839 		elif f in p.diradds:
1840 			print '<td><span style="color:#008000">',
1841 			print '[added dir]',
1842 			print '</span></td>'
1843 		elif f in p.removes:
1844 			print '<td><span style="color:#800000">',
1845 			print '[removed]',
1846 			print '</span></td>'
1847 		elif f in p.dirremoves:
1848 			print '<td><span style="color:#800000">',
1849 			print '[removed dir]',
1850 			print '</span></td>'
1851 		elif p.replaces.has_key(f):
1852 			print '<td><span style="color:#800000">',
1853 			print '[replaced %d tokens]' % p.replaces[f],
1854 			print '</span></td>'
1855 		elif p.moves.has_key(f):
1856 			print '<td><span style="color:#000080">',
1857 			print '[moved to "%s"]' % p.moves[f]
1858 			print '</span></td>'
1859 			show_diff = 0
1860 		else:
1861 			print '<td><span style="color:#000080">',
1862 			if p.modifies[f].has_key('b'):
1863 				# binary modification
1864 				print '(binary)'
1865 			else:
1866 				print '+%(+)d  -%(-)d' % p.modifies[f],
1867 			print '</span></td>'
1868 
1869 		if show_diff:
1870 			print """
1871 <td class="link">
1872   <a href="%(myreponame)s;a=filediff;h=%(hash)s;f=%(file)s">diff</a> |
1873   <a href="%(myreponame)s;a=filehistory;f=%(file)s">history</a> |
1874   <a href="%(myreponame)s;a=annotate_shade;h=%(hash)s;f=%(file)s">annotate</a>
1875 </td>
1876 			""" % {
1877 				'myreponame': config.myreponame,
1878 				'hash': p.hash,
1879 				'file': urllib.quote(f)
1880 			}
1881 		print '</tr>'
1882 	print '</table>'
1883 	print_footer()
1884 
1885 
1886 def do_tree(dname):
1887 	print_header()
1888 	print_navbar()
1889 
1890 	# the head
1891 	print """
1892 <div><a class="title" href="%s;a=tree">Current tree</a></div>
1893 <div class="page_path"><b>
1894 	""" % config.myreponame
1895 
1896 	# and the linked, with links
1897 	parts = dname.split('/')
1898 	print '/ '
1899 	sofar = '/'
1900 	for p in parts:
1901 		if not p: continue
1902 		sofar += '/' + p
1903 		print '<a href="%s;a=tree;f=%s">%s</a> /' % \
1904 				(config.myreponame, urllib.quote(sofar), p)
1905 
1906 	print """
1907   </b></div>
1908 <div class="page_body">
1909 <table cellspacing="0">
1910 	"""
1911 
1912 	path = realpath(dname) + '/'
1913 
1914 	alt = True
1915 	files = os.listdir(path)
1916 	files.sort()
1917 
1918 	# list directories first
1919 	dlist = []
1920 	flist = []
1921 	for f in files:
1922 		if f == "_darcs":
1923 			continue
1924 		realfile = path + f
1925 		if os.path.isdir(realfile):
1926 			dlist.append(f)
1927 		else:
1928 			flist.append(f)
1929 	files = dlist + flist
1930 
1931 	for f in files:
1932 		if alt:
1933 			print '<tr class="dark">'
1934 		else:
1935 			print '<tr class="light">'
1936 		alt = not alt
1937 		realfile = path + f
1938 		fullf = filter_file(dname + '/' + f)
1939 		print '<td style="font-family:monospace">', fperms(realfile),
1940 		print '</td>'
1941 		print '<td style="font-family:monospace">', fsize(realfile),
1942 		print '</td>'
1943 
1944 		if f in dlist:
1945 			print """
1946   <td>
1947     <a class="link" href="%(myrname)s;a=tree;f=%(fullf)s">%(f)s/</a>
1948   </td>
1949   <td class="link">
1950     <a href="%(myrname)s;a=filehistory;f=%(fullf)s">history</a> |
1951     <a href="%(myrname)s;a=tree;f=%(fullf)s">tree</a>
1952   </td>
1953 			""" % {
1954 				'myrname': config.myreponame,
1955 				'f': escape(f),
1956 				'fullf': urllib.quote(fullf),
1957 			}
1958 		else:
1959 			print """
1960   <td><a class="list" href="%(myrname)s;a=headblob;f=%(fullf)s">%(f)s</a></td>
1961   <td class="link">
1962     <a href="%(myrname)s;a=filehistory;f=%(fullf)s">history</a> |
1963     <a href="%(myrname)s;a=headblob;f=%(fullf)s">headblob</a> |
1964     <a href="%(myrname)s;a=annotate_shade;f=%(fullf)s">annotate</a>
1965   </td>
1966 			""" % {
1967 				'myrname': config.myreponame,
1968 				'f': escape(f),
1969 				'fullf': urllib.quote(fullf),
1970 			}
1971 		print '</tr>'
1972 	print '</table></div>'
1973 	print_footer()
1974 
1975 
1976 def do_headblob(fname):
1977 	print_header()
1978 	print_navbar(f = fname)
1979 	filepath = os.path.dirname(fname)
1980 
1981 	if filepath == '/':
1982 		print '<div><a class="title" href="%s;a=tree">/</a></div>' % \
1983 			(config.myreponame)
1984 	else:
1985 		print '<div class="title"><b>'
1986 
1987 		# and the linked, with links
1988 		parts = filepath.split('/')
1989 		print '/ '
1990 		sofar = '/'
1991 		for p in parts:
1992 			if not p: continue
1993 			sofar += '/' + p
1994 			print '<a href="%s;a=tree;f=%s">%s</a> /' % \
1995 					(config.myreponame, sofar, p)
1996 
1997 		print '</b></div>'
1998 
1999 	print_blob(fname)
2000 	print_footer()
2001 
2002 
2003 def do_plainblob(fname):
2004 	f = open(realpath(fname), 'r')
2005 
2006 	if isbinary(fname):
2007 		print_binary_header(os.path.basename(fname))
2008 		for l in f:
2009 			sys.stdout.write(l)
2010 	else:
2011 		print_plain_header()
2012 		for l in f:
2013 			sys.stdout.write(fixu8(l))
2014 
2015 
2016 def do_annotate(fname, phash, style):
2017 	print_header()
2018 	ann = get_annotate(fname, phash)
2019 	if not ann:
2020 		print """
2021 <i>The annotate feature has been disabled</i>
2022 </div>
2023 		"""
2024 		print_footer()
2025 		return
2026 	print_navbar(f = fname, h = ann.lastchange_hash)
2027 
2028 	print """
2029 <div>
2030   <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
2031 </div>
2032 <div class="page_path"><b>
2033   Annotate for file %(fname)s
2034 </b></div>
2035 	""" % {
2036 		'myreponame': config.myreponame,
2037 		'hash': ann.lastchange_hash,
2038 		'name': escape(ann.lastchange_name),
2039 		'fname': escape(fname),
2040 	}
2041 
2042 	print_annotate(ann, style)
2043 	print_footer()
2044 
2045 def do_annotate_plain(fname, phash):
2046 	print_plain_header()
2047 	ann = get_annotate(fname, phash)
2048 	for l in ann.lines:
2049 		sys.stdout.write(l.text)
2050 
2051 
2052 def do_shortlog(topi, last=PATCHES_PER_PAGE):
2053 	print_header()
2054 	print_navbar()
2055 	print_shortlog(topi = topi, last = last)
2056 	print_footer()
2057 
2058 def do_filehistory(topi, f, last=PATCHES_PER_PAGE):
2059 	print_header()
2060 	print_navbar(f = fname)
2061 	print_shortlog(topi = topi, fname = fname, last = last)
2062 	print_footer()
2063 
2064 def do_log(topi, last=PATCHES_PER_PAGE):
2065 	print_header()
2066 	print_navbar()
2067 	print_log(topi = topi, last = last)
2068 	print_footer()
2069 
2070 def do_atom():
2071 	print "Content-type: application/atom+xml; charset=utf-8\n"
2072 	print '<?xml version="1.0" encoding="utf-8"?>'
2073 	inv = config.repodir + '/_darcs/patches'
2074 	repo_lastmod = os.stat(inv).st_mtime
2075 	str_lastmod = time.strftime(iso_datetime,
2076 			time.localtime(repo_lastmod))
2077 
2078 	print """
2079 <feed xmlns="http://www.w3.org/2005/Atom">
2080   <title>%(reponame)s darcs repository</title>
2081   <link rel="alternate" type="text/html" href="%(url)s"/>
2082   <link rel="self" type="application/atom+xml" href="%(url)s;a=atom"/>
2083   <id>%(url)s</id> <!-- TODO: find a better <id>, see RFC 4151 -->
2084   <author><name>darcs repository (several authors)</name></author>
2085   <generator>darcsweb.cgi</generator>
2086   <updated>%(lastmod)s</updated> 
2087   <subtitle>%(desc)s</subtitle>
2088   	""" % {
2089 		'reponame': config.reponame,
2090 		'url': config.myurl + '/' + config.myreponame,
2091 		'desc': escape(config.repodesc),
2092 		'lastmod': str_lastmod,
2093 	}
2094 
2095 	ps = get_last_patches(20)
2096 	for p in ps:
2097 		title = time.strftime('%d %b %H:%M', time.localtime(p.date))
2098 		title += ' - ' + p.name
2099 		pdate = time.strftime(iso_datetime,
2100 				time.localtime(p.date))
2101 		link = '%s/%s;a=commit;h=%s' % (config.myurl,
2102 				config.myreponame, p.hash)
2103 
2104 		import email.Utils
2105 		addr, author = email.Utils.parseaddr(p.author)
2106 		if not addr:
2107 			addr = "unknown_email@example.com"
2108 		if not author:
2109 			author = addr
2110 
2111 		print """
2112   <entry>
2113     <title>%(title)s</title>
2114     <author>
2115       <name>%(author)s</name>
2116       <email>%(email)s</email>
2117     </author>
2118     <updated>%(pdate)s</updated>
2119     <id>%(link)s</id>
2120     <link rel="alternate" href="%(link)s"/>
2121     <summary>%(desc)s</summary>
2122     <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
2123 	   	""" % {
2124 			'title': escape(title),
2125 			'author': author,
2126 			'email': addr,
2127 			'url': config.myurl + '/' + config.myreponame,
2128 			'pdate': pdate,
2129 			'myrname': config.myreponame,
2130 			'hash': p.hash,
2131 			'pname': escape(p.name),
2132 			'link': link,
2133 			'desc': escape(p.name),
2134 		}
2135 
2136                 # TODO: allow to get plain text, not HTML?
2137 		print escape(p.name) + '<br/>'
2138 		if p.comment:
2139 			print '<br/>'
2140 			print escape(p.comment).replace('\n', '<br/>\n')
2141 			print '<br/>'
2142 		print '<br/>'
2143 		changed = p.adds + p.removes + p.modifies.keys() + \
2144 				p.moves.keys() + p.diradds + p.dirremoves + \
2145 				p.replaces.keys()
2146 		for i in changed: # TODO: link to the file 
2147 			print '<code>%s</code><br/>' % i
2148 		print '</p></div>'
2149 		print '</content></entry>'
2150 	print '</feed>'
2151 
2152 def do_rss():
2153 	print "Content-type: text/xml; charset=utf-8\n"
2154 	print '<?xml version="1.0" encoding="utf-8"?>'
2155 	print """
2156 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
2157 <channel>
2158  <title>%(reponame)s</title>
2159   <link>%(url)s</link>
2160   <description>%(desc)s</description>
2161   <language>en</language>
2162   	""" % {
2163 		'reponame': config.reponame,
2164 		'url': config.myurl + '/' + config.myreponame,
2165 		'desc': escape(config.repodesc),
2166 	}
2167 
2168 	ps = get_last_patches(20)
2169 	for p in ps:
2170 		title = time.strftime('%d %b %H:%M', time.localtime(p.date))
2171 		title += ' - ' + p.name
2172 		pdate = time.strftime("%a, %d %b %Y %H:%M:%S +0000",
2173 				time.localtime(p.date))
2174 		link = '%s/%s;a=commit;h=%s' % (config.myurl,
2175 				config.myreponame, p.hash)
2176 
2177 		# the author field is tricky because the standard requires it
2178 		# has an email address; so we need to check that and lie
2179 		# otherwise; there's more info at
2180 		# http://feedvalidator.org/docs/error/InvalidContact.html
2181 		if "@" in p.author:
2182 			author = p.author
2183 		else:
2184 			author = "%s &lt;unknown@email&gt;" % p.author
2185 
2186 		print """
2187   <item>
2188     <title>%(title)s</title>
2189     <author>%(author)s</author>
2190     <pubDate>%(pdate)s</pubDate>
2191     <link>%(link)s</link>
2192     <description>%(desc)s</description>
2193 	   	""" % {
2194 			'title': escape(title),
2195 			'author': author,
2196 			'pdate': pdate,
2197 			'link': link,
2198 			'desc': escape(p.name),
2199 		}
2200 		print '    <content:encoded><![CDATA['
2201 		print escape(p.name) + '<br/>'
2202 		if p.comment:
2203 			print '<br/>'
2204 			print escape(p.comment).replace('\n', '<br/>\n')
2205 			print '<br/>'
2206 		print '<br/>'
2207 		changed = p.adds + p.removes + p.modifies.keys() + \
2208 				p.moves.keys() + p.diradds + p.dirremoves + \
2209 				p.replaces.keys()
2210 		for i in changed:
2211 			print '%s<br/>' % i
2212 		print ']]>'
2213 		print '</content:encoded></item>'
2214 
2215 	print '</channel></rss>'
2216 
2217 
2218 def do_search(s):
2219 	print_header()
2220 	print_navbar()
2221 	ps = get_last_patches(config.searchlimit)
2222 
2223 	print '<div class="title">Search last %d commits for "%s"</div>' \
2224 			% (config.searchlimit, escape(s))
2225 	print '<table cellspacing="0">'
2226 
2227 	alt = True
2228 	for p in ps:
2229 		match = p.matches(s)
2230 		if not match:
2231 			continue
2232 
2233 		if alt:
2234 			print '<tr class="dark">'
2235 		else:
2236 			print '<tr class="light">'
2237 		alt = not alt
2238 
2239 		print """
2240   <td><i>%(age)s</i></td>
2241   <td>%(author)s</td>
2242   <td>
2243     <a class="list" title="%(fullname)s" href="%(myrname)s;a=commit;h=%(hash)s">
2244       <b>%(name)s</b>
2245     </a><br/>
2246     %(match)s
2247   </td>
2248   <td class="link">
2249     <a href="%(myrname)s;a=commit;h=%(hash)s">commit</a> |
2250     <a href="%(myrname)s;a=commitdiff;h=%(hash)s">commitdiff</a>
2251   </td>
2252 		""" % {
2253 			'age': how_old(p.local_date),
2254 			'author': gen_authorlink(p.author, shorten_str(p.shortauthor, 26)),
2255 			'myrname': config.myreponame,
2256 			'hash': p.hash,
2257 			'name': escape(shorten_str(p.name)),
2258 			'fullname': escape(p.name),
2259 			'match': highlight(s, shorten_str(match)),
2260 		}
2261 		print "</tr>"
2262 
2263 	print '</table>'
2264 	print_footer()
2265 
2266 
2267 def do_die():
2268 	print_header()
2269 	print "<p><font color=red>Error! Malformed query</font></p>"
2270 	print_footer()
2271 
2272 
2273 def do_listrepos():
2274 	import config as all_configs
2275 	expand_multi_config(all_configs)
2276 
2277 	# the header here is special since we don't have a repo
2278 	print "Content-type: text/html; charset=utf-8\n"
2279 	print '<?xml version="1.0" encoding="utf-8"?>'
2280 	print """
2281 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2282 		"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2283 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2284 <head>
2285 <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
2286 <meta name="robots" content="index, nofollow"/>
2287 <title>darcs - Repositories</title>
2288 <link rel="stylesheet" type="text/css" href="%(css)s"/>
2289 <link rel="shortcut icon" href="%(fav)s"/>
2290 <link rel="icon" href="%(fav)s"/>
2291 </head>
2292 
2293 <body>
2294 <div class="page_header">
2295 <a href="http://darcs.net" title="darcs">
2296 <img src="%(logo)s" alt="darcs logo" style="float:right; border-width:0px;"/>
2297 </a>
2298 <a href="%(myname)s">repos</a> / index
2299 </div>
2300 <div class="index_include">
2301 %(summary)s
2302 </div>
2303 <table cellspacing="0">
2304 <tr>
2305 <th>Project</th>
2306 <th>Description</th>
2307 <th>Owner</th>
2308 <th>Last Change</th>
2309 <th></th>
2310 </tr>
2311 	""" % {
2312 		'myname': config.myname,
2313 		'css': config.cssfile,
2314 		'fav': config.darcsfav,
2315 		'logo': config.darcslogo,
2316 		'summary': config.summary
2317 	}
2318 
2319 	# some python magic
2320 	alt = True
2321 	confs = dir(all_configs)
2322 	confs.sort(key=str.lower)
2323 	for conf in confs:
2324 		if conf.startswith('__'):
2325 			continue
2326 		c = all_configs.__getattribute__(conf)
2327 		if 'reponame' not in dir(c):
2328 			continue
2329 		name = escape(c.reponame)
2330 		desc = escape(c.repodesc)
2331 
2332 		if alt: print '<tr class="dark">'
2333 		else: print '<tr class="light">'
2334 		alt = not alt
2335 		try: orig_repodir = config.repodir
2336 		except: orig_repodir = None
2337 		config.repodir = c.repodir
2338 		print """
2339 <td><a class="list" href="%(myname)s?r=%(name)s;a=summary">%(dname)s</a></td>
2340 <td>%(desc)s</td>
2341 <td>%(owner)s</td>
2342 <td>%(lastchange)s</td>
2343 <td class="link"><a href="%(myname)s?r=%(name)s;a=summary">summary</a> |
2344 <a href="%(myname)s?r=%(name)s;a=shortlog">shortlog</a> |
2345 <a href="%(myname)s?r=%(name)s;a=log">log</a> |
2346 <a href="%(myname)s?r=%(name)s;a=tree">tree</a>
2347 </td>
2348 </tr>
2349 		""" % {
2350 			'myname': config.myname,
2351 			'dname': name,
2352 			'name': urllib.quote(name),
2353 			'desc': shorten_str(desc, 60),
2354 			'owner': escape(repo_get_owner() or ''),
2355 			'lastchange': how_old(os.stat(c.repodir + '/_darcs/patches').st_mtime),
2356 		}
2357 		config.repodir = orig_repodir
2358 	print "</table>"
2359 	print_footer(put_rss = 0)
2360 
2361 def expand_multi_config(config):
2362 	"""Expand configuration entries that serve as "template" to others;
2363 	this make it easier to have a single directory with all the repos,
2364 	because they don't need specific entries in the configuration anymore.
2365 	"""
2366 
2367 	for conf in dir(config):
2368 		if conf.startswith('__'):
2369 			continue
2370 		c = config.__getattribute__(conf)
2371 		if 'multidir' not in dir(c):
2372 			continue
2373 
2374 		if not os.path.isdir(c.multidir):
2375 			continue
2376 
2377 		if 'exclude' not in dir(c):
2378 			c.exclude = []
2379 
2380 		entries = []
2381 		if 'multidir_deep' in dir(c) and c.multidir_deep:
2382 			for (root, dirs, files) in os.walk(c.multidir):
2383 				# do not visit hidden directories
2384 				dirs[:] = [d for d in dirs \
2385 						if not d.startswith('.')]
2386 				if '_darcs' in dirs:
2387 					p = root[1 + len(c.multidir):]
2388 					entries.append(p)
2389 		else:
2390 			entries = os.listdir(c.multidir)
2391 
2392 		entries.sort()
2393 		for name in entries:
2394 			name = name.replace('\\', '/')
2395 			if name.startswith('.'):
2396 				continue
2397 			fulldir = c.multidir + '/' + name
2398 			if not os.path.isdir(fulldir + '/_darcs'):
2399 				continue
2400 			if name in c.exclude:
2401 				continue
2402 
2403 			# set the display name at the beginning, so it can be
2404 			# used by the other replaces
2405 			if 'displayname' in dir(c):
2406 				dname = c.displayname % { 'name': name }
2407 			else:
2408 				dname = name
2409 
2410 			rep_dict = { 'name': name, 'dname': dname }
2411 
2412 			if 'autoexclude' in dir(c) and c.autoexclude:
2413 				dpath = fulldir + \
2414 					'/_darcs/third_party/darcsweb'
2415 				if not os.path.isdir(dpath):
2416 					continue
2417 
2418 			if 'autodesc' in dir(c) and c.autodesc:
2419 				dpath = fulldir + \
2420 					'/_darcs/third_party/darcsweb/desc'
2421 				if os.access(dpath, os.R_OK):
2422 					desc = open(dpath).readline().rstrip("\n")
2423 				else:
2424 					desc = c.repodesc % rep_dict
2425 			else:
2426 				desc = c.repodesc % rep_dict
2427 
2428 			if 'autourl' in dir(c) and c.autourl:
2429 				dpath = fulldir + \
2430 					'/_darcs/third_party/darcsweb/url'
2431 				if os.access(dpath, os.R_OK):
2432 					url = open(dpath).readline().rstrip("\n")
2433 				else:
2434 					url = c.repourl % rep_dict
2435 			else:
2436 				url = c.repourl % rep_dict
2437 
2438 			if 'autoprojurl' in dir(c) and c.autoprojurl:
2439 				dpath = fulldir + \
2440 					'/_darcs/third_party/darcsweb/projurl'
2441 				if os.access(dpath, os.R_OK):
2442 					projurl = open(dpath).readline().rstrip("\n")
2443 				elif 'repoprojurl' in dir(c):
2444 					projurl = c.repoprojurl % rep_dict
2445 				else:
2446 					projurl = None
2447 			elif 'repoprojurl' in dir(c):
2448 				projurl = c.repoprojurl % rep_dict
2449 			else:
2450 				projurl = None
2451 
2452 			if 'autolisturl' in dir(c) and c.autolisturl:
2453 				dpath = fulldir + \
2454 					'/_darcs/third_party/darcsweb/listurl'
2455 				if os.access(dpath, os.R_OK):
2456 					listurl = open(dpath).readline().rstrip("\n")
2457 				elif 'repolisturl' in dir(c):
2458 					listurl = c.repolisturl % rep_dict
2459 				else:
2460 					listurl = None
2461 			elif 'repolisturl' in dir(c):
2462 				listurl = c.repolisturl % rep_dict
2463 			else:
2464 				listurl = None
2465 
2466 			rdir = fulldir
2467 			class tmp_config:
2468 				reponame = dname
2469 				repodir = rdir
2470 				repodesc = desc
2471 				repourl = url
2472 				repoencoding = c.repoencoding
2473 				repoprojurl = projurl
2474 				repolisturl = listurl
2475 
2476 				if 'footer' in dir(c):
2477 					footer = c.footer
2478 
2479 			# index by display name to avoid clashes
2480 			config.__setattr__(dname, tmp_config)
2481 
2482 def fill_config(name = None):
2483 	import config as all_configs
2484 	expand_multi_config(all_configs)
2485 
2486 	if name:
2487 		# we only care about setting some configurations if a repo was
2488 		# specified; otherwise we only set the common configuration
2489 		# directives
2490 		for conf in dir(all_configs):
2491 			if conf.startswith('__'):
2492 				continue
2493 			c = all_configs.__getattribute__(conf)
2494 			if 'reponame' not in dir(c):
2495 				continue
2496 			if c.reponame == name:
2497 				break
2498 		else:
2499 			# not found
2500 			raise Exception, "Repo not found: " + repr(name)
2501 
2502 	# fill the configuration
2503 	base = all_configs.base
2504 	if 'myname' not in dir(base):
2505 		# SCRIPT_NAME has the full path, we only take the file name
2506 		config.myname = os.path.basename(os.environ['SCRIPT_NAME'])
2507 	else:
2508 		config.myname = base.myname
2509 
2510 	if 'myurl' not in dir(base):
2511 		n = os.environ['SERVER_NAME']
2512 		p = os.environ['SERVER_PORT']
2513 		s = os.path.dirname(os.environ['SCRIPT_NAME'])
2514 		u = os.environ.get('HTTPS', 'off') in ('on', '1')
2515 		if not u and p == '80' or u and p == '443':
2516 			p = ''
2517 		else:
2518 			p = ':' + p
2519 		config.myurl = 'http%s://%s%s%s' % (u and 's' or '', n, p, s)
2520 	else:
2521 		config.myurl = base.myurl
2522 
2523 	config.darcslogo = base.darcslogo
2524 	config.darcsfav = base.darcsfav
2525 	config.cssfile = base.cssfile
2526 	if name:
2527 		config.myreponame = config.myname + '?r=' + urllib.quote(name)
2528 		config.reponame = c.reponame
2529 		config.repodesc = c.repodesc
2530 		config.repodir = c.repodir
2531 		config.repourl = c.repourl
2532 
2533 		config.repoprojurl = None
2534 		if 'repoprojurl' in dir(c):
2535 			config.repoprojurl = c.repoprojurl
2536 
2537 		config.repolisturl = None
2538 		if 'repolisturl' in dir(c):
2539 			config.repolisturl = c.repolisturl
2540 
2541 		# repoencoding must be a tuple
2542 		if isinstance(c.repoencoding, str):
2543 			config.repoencoding = (c.repoencoding, )
2544 		else:
2545 			config.repoencoding = c.repoencoding
2546 
2547 	# optional parameters
2548 	if "darcspath" in dir(base):
2549 		config.darcspath = base.darcspath + '/'
2550 	else:
2551 		config.darcspath = ""
2552 
2553 	if "summary" in dir(base):
2554 		config.summary = base.summary
2555 	else:
2556 		config.summary = """
2557 This is the repository index for a darcsweb site.<br/>
2558 These are all the available repositories.<br/>
2559 		"""
2560 
2561 	if "cachedir" in dir(base):
2562 		config.cachedir = base.cachedir
2563 	else:
2564 		config.cachedir = None
2565 
2566 	if "searchlimit" in dir(base):
2567 		config.searchlimit = base.searchlimit
2568 	else:
2569 		config.searchlimit = 100
2570 
2571 	if "logtimes" in dir(base):
2572 		config.logtimes = base.logtimes
2573 	else:
2574 		config.logtimes = None
2575 
2576 	if "url_links" in dir(base):
2577 		config.url_links = base.url_links
2578 	else:
2579 		config.url_links = ()
2580 
2581 	if name and "footer" in dir(c):
2582 		config.footer = c.footer
2583 	elif "footer" in dir(base):
2584 		config.footer = base.footer
2585 	else:
2586 		config.footer = "Crece desde el pueblo el futuro / " \
2587 				+ "crece desde el pie"
2588 	if "author_links" in dir(base):
2589 		config.author_links = base.author_links
2590 	else:
2591 		config.author_links = None
2592 	if "disable_annotate" in dir(base):
2593 		config.disable_annotate = base.disable_annotate
2594 	else:
2595 		config.disable_annotate = False
2596 
2597 	if "readme_converter" in dir(base):
2598 		config.readme_converter = base.readme_converter
2599 	else:
2600 		config.readme_converter = False
2601 
2602 
2603 
2604 #
2605 # main
2606 #
2607 
2608 if sys.version_info < (2, 3):
2609 	print "Sorry, but Python 2.3 or above is required to run darcsweb."
2610 	sys.exit(1)
2611 
2612 form = cgi.FieldStorage()
2613 
2614 # if they don't specify a repo, print the list and exit
2615 if not form.has_key('r'):
2616 	fill_config()
2617 	do_listrepos()
2618 	log_times(cache_hit = 0, event = 'index')
2619 	sys.exit(0)
2620 
2621 # get the repo configuration and fill the config class
2622 current_repo = urllib.unquote(form['r'].value)
2623 fill_config(current_repo)
2624 
2625 
2626 # get the action, or default to summary
2627 if not form.has_key("a"):
2628 	action = "summary"
2629 else:
2630 	action = filter_act(form["a"].value)
2631 
2632 # check if we have the page in the cache
2633 if config.cachedir:
2634 	url_request = os.environ['QUERY_STRING']
2635 	# create a string representation of the request, ignoring all the
2636 	# unused parameters to avoid DoS
2637 	params = ['r', 'a', 'f', 'h', 'topi', 'last']
2638 	params = [ x for x in form.keys() if x in params ]
2639 	url_request = [ (x, form[x].value) for x in params ]
2640 	url_request.sort()
2641 	cache = Cache(config.cachedir, url_request)
2642 	if cache.open():
2643 		# we have a hit, dump and run
2644 		cache.dump()
2645 		cache.close()
2646 		log_times(cache_hit = 1, repo = config.reponame)
2647 		sys.exit(0)
2648 	# if there is a miss, the cache will step over stdout, intercepting
2649 	# all "print"s and writing them to the cache file automatically
2650 
2651 
2652 # see what should we do according to the received action
2653 if action == "summary":
2654 	do_summary()
2655 
2656 elif action == "commit":
2657 	phash = filter_hash(form["h"].value)
2658 	do_commit(phash)
2659 elif action == "commitdiff":
2660 	phash = filter_hash(form["h"].value)
2661 	do_commitdiff(phash)
2662 elif action == "plain_commitdiff":
2663 	phash = filter_hash(form["h"].value)
2664 	do_plain_commitdiff(phash)
2665 elif action == "darcs_commitdiff":
2666 	phash = filter_hash(form["h"].value)
2667 	do_darcs_commitdiff(phash)
2668 elif action == "raw_commitdiff":
2669 	phash = filter_hash(form["h"].value)
2670 	do_raw_commitdiff(phash)
2671 
2672 elif action == 'headdiff':
2673 	phash = filter_hash(form["h"].value)
2674 	do_headdiff(phash)
2675 elif action == "plain_headdiff":
2676 	phash = filter_hash(form["h"].value)
2677 	do_plain_headdiff(phash)
2678 elif action == "darcs_headdiff":
2679         phash = filter_hash(form["h"].value)
2680         do_darcs_headdiff(phash)
2681 
2682 elif action == "filediff":
2683 	phash = filter_hash(form["h"].value)
2684 	fname = filter_file(form["f"].value)
2685 	do_filediff(phash, fname)
2686 elif action == "plain_filediff":
2687 	phash = filter_hash(form["h"].value)
2688 	fname = filter_file(form["f"].value)
2689 	do_plain_filediff(phash, fname)
2690 elif action == "darcs_filediff":
2691         phash = filter_hash(form["h"].value)
2692 	fname = filter_file(form["f"].value)
2693 	do_darcs_filediff(phash, fname)
2694 
2695 elif action == 'headfilediff':
2696 	phash = filter_hash(form["h"].value)
2697 	fname = filter_file(form["f"].value)
2698 	do_file_headdiff(phash, fname)
2699 elif action == "plain_headfilediff":
2700 	phash = filter_hash(form["h"].value)
2701 	fname = filter_file(form["f"].value)
2702 	do_plain_fileheaddiff(phash, fname)
2703 elif action == "darcs_headfilediff":
2704         phash = filter_hash(form["h"].value)
2705 	fname = filter_file(form["f"].value)
2706         do_darcs_fileheaddiff(phash, fname)
2707 
2708 elif action == "annotate_normal":
2709 	fname = filter_file(form["f"].value)
2710 	if form.has_key("h"):
2711 		phash = filter_hash(form["h"].value)
2712 	else:
2713 		phash = None
2714 	do_annotate(fname, phash, "normal")
2715 elif action == "annotate_plain":
2716 	fname = filter_file(form["f"].value)
2717 	if form.has_key("h"):
2718 		phash = filter_hash(form["h"].value)
2719 	else:
2720 		phash = None
2721         do_annotate_plain(fname, phash)
2722 elif action == "annotate_zebra":
2723 	fname = filter_file(form["f"].value)
2724 	if form.has_key("h"):
2725 		phash = filter_hash(form["h"].value)
2726 	else:
2727 		phash = None
2728 	do_annotate(fname, phash, "zebra")
2729 elif action == "annotate_shade":
2730 	fname = filter_file(form["f"].value)
2731 	if form.has_key("h"):
2732 		phash = filter_hash(form["h"].value)
2733 	else:
2734 		phash = None
2735 	do_annotate(fname, phash, "shade")
2736 
2737 elif action == "shortlog":
2738 	if form.has_key("topi"):
2739 		topi = int(filter_num(form["topi"].value))
2740 	else:
2741 		topi = 0
2742 	if form.has_key("last"):
2743 		last = int(filter_num(form["last"].value))
2744 	else:
2745 		last = PATCHES_PER_PAGE
2746 	do_shortlog(topi=topi,last=last)
2747 
2748 elif action == "filehistory":
2749 	if form.has_key("topi"):
2750 		topi = int(filter_num(form["topi"].value))
2751 	else:
2752 		topi = 0
2753 	fname = filter_file(form["f"].value)
2754 	if form.has_key("last"):
2755 		last = int(filter_num(form["last"].value))
2756 	else:
2757 		last = PATCHES_PER_PAGE
2758 	do_filehistory(topi, fname, last=last)
2759 
2760 elif action == "log":
2761 	if form.has_key("topi"):
2762 		topi = int(filter_num(form["topi"].value))
2763 	else:
2764 		topi = 0
2765 	if form.has_key("last"):
2766 		last = int(filter_num(form["last"].value))
2767 	else:
2768 		last = PATCHES_PER_PAGE
2769 	do_log(topi, last=last)
2770 
2771 elif action == 'headblob':
2772 	fname = filter_file(form["f"].value)
2773 	do_headblob(fname)
2774 
2775 elif action == 'plainblob':
2776 	fname = filter_file(form["f"].value)
2777 	do_plainblob(fname)
2778 
2779 elif action == 'tree':
2780 	if form.has_key('f'):
2781 		fname = filter_file(form["f"].value)
2782 	else:
2783 		fname = '/'
2784 	do_tree(fname)
2785 
2786 elif action == 'rss':
2787 	do_rss()
2788 
2789 elif action == 'atom':
2790 	do_atom()
2791 
2792 elif action == 'search':
2793 	if form.has_key('s'):
2794 		s = form["s"].value
2795 	else:
2796 		s = ''
2797 	do_search(s)
2798 	if config.cachedir:
2799 		cache.cancel()
2800 
2801 else:
2802 	action = "invalid query"
2803 	do_die()
2804 	if config.cachedir:
2805 		cache.cancel()
2806 
2807 
2808 if config.cachedir:
2809 	cache.close()
2810 
2811 log_times(cache_hit = 0, repo = config.reponame)