Fix hashlib import
[darcsweb.git] / darcsweb.cgi
blob35c1661f2336736d08616e646ca9c232076b65d7
1 #!/usr/bin/env python
3 """
4 darcsweb - A web interface for darcs
5 Alberto Bertogli (albertito@blitiri.com.ar)
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 """
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
24 iso_datetime = '%Y-%m-%dT%H:%M:%SZ'
26 PATCHES_PER_PAGE = 50
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')
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'])
43 # empty configuration class, we will fill it in later depending on the repo
44 class config:
45 pass
47 # list of run_darcs() invocations, for performance measures
48 darcs_runs = []
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
60 # utility functions
63 def filter_num(s):
64 l = [c for c in s if c in string.digits]
65 return ''.join(l)
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)
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)
80 def filter_file(s):
81 if '..' in s or '"' in s:
82 raise Exception, 'FilterFile FAILED'
83 if s == '/':
84 return s
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
97 def printd(*params):
98 print ' '.join(params), '<br/>'
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)
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
122 def escape(s):
123 s = xml_escape(s)
124 s = s.replace('"', '&quot;')
125 return s
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
165 def shorten_str(s, max = 60):
166 if len(s) > max:
167 s = s[:max - 4] + ' ...'
168 return s
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
180 def replace_links(s):
181 """Replace user defined strings with links, as specified in the
182 configuration file."""
183 import re
185 vardict = {
186 "myreponame": config.myreponame,
187 "reponame": config.reponame,
190 for link_pat, link_dst in config.url_links:
191 s = re.sub(link_pat, link_dst % vardict, s)
193 return s
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)
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
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('-')
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('-')
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('-')
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('-')
244 return ''.join(s)
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)
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
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
274 def log_times(cache_hit, repo = None, event = None):
275 if not config.logtimes:
276 return
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
286 if repo:
287 s += '\trepo: %s\n' % repo
289 s += """\
290 total: %.3f
291 processing: %.3f
292 imports: %.3f\n""" % (time_total, processing, time_imports)
294 if darcs_runs:
295 s += "\truns:\n"
296 for params in darcs_runs:
297 s += '\t\t%s\n' % params
298 s += '\n'
300 lf = open(config.logtimes, 'a')
301 lf.write(s)
302 lf.close()
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)
322 # generic html functions
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).
335 Based on gitweb, which is written by Kay Sievers <kay.sievers@vrfy.org>
336 and Christian Gierke <ch@gierke.de>
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>
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
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>"
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 }
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 }
407 realf = realpath(f)
408 f = urllib.quote(f)
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
418 elif f:
419 print """
420 | <a href="%(myreponame)s;a=annotate_shade;f=%(fname)s">annotate</a>
421 """ % { "myreponame": config.myreponame, 'fname': f }
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 }
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 }
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 }
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 }
443 print "<br/>"
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"):
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"
462 params = 'h=%s;' % h
463 if f:
464 params += 'f=%s;' % f
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 }
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 }
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 }
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 }
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 }
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
511 print '<br/>'
512 print '</div>'
514 def print_plain_header():
515 print "Content-type: text/plain; charset=utf-8\n"
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
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
542 # basic caching
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
555 def open(self):
556 "Returns 1 on hit, 0 on miss"
557 fname = self.basedir + '/' + self.fname
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)
567 # step over stdout so when "print" tries to write
568 # output, we get it first
569 sys.stdout = self
570 return 0
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
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)
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
588 # the entry is still valid, hit!
589 self.file = open(fname, 'r')
590 self.mode = 'r'
591 return 1
594 def dump(self):
595 for l in self.file:
596 self.real_stdout.write(l)
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)
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'
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'
628 # darcs repo manipulation
631 def repo_get_owner():
632 try:
633 fd = open(config.repodir + '/_darcs/prefs/author')
634 author = fd.readlines()[0].strip()
635 except:
636 author = None
637 return author
639 def run_darcs(params):
640 """Runs darcs on the repodir with the given params, return a file
641 object with its output."""
642 os.chdir(config.repodir)
643 try:
644 original_8bit_setting = os.environ['DARCS_DONT_ESCAPE_8BIT']
645 except KeyError:
646 original_8bit_setting = None
647 os.environ['DARCS_DONT_ESCAPE_8BIT'] = '1'
648 cmd = config.darcspath + "darcs " + params
649 inf, outf = os.popen4(cmd, 't')
650 darcs_runs.append(params)
651 if original_8bit_setting == None:
652 del(os.environ['DARCS_DONT_ESCAPE_8BIT'])
653 else:
654 os.environ['DARCS_DONT_ESCAPE_8BIT'] = original_8bit_setting
655 return outf
658 class Patch:
659 "Represents a single patch/record"
660 def __init__(self):
661 self.hash = ''
662 self.author = ''
663 self.shortauthor = ''
664 self.date = 0
665 self.local_date = 0
666 self.name = ''
667 self.comment = ''
668 self.inverted = False;
669 self.adds = []
670 self.removes = []
671 self.modifies = {}
672 self.diradds = []
673 self.dirremoves = []
674 self.replaces = {}
675 self.moves = {}
677 def tostr(self):
678 s = "%s\n\tAuthor: %s\n\tDate: %s\n\tHash: %s\n" % \
679 (self.name, self.author, self.date, self.hash)
680 return s
682 def getdiff(self):
683 """Returns a list of lines from the diff -u corresponding with
684 the patch."""
685 params = 'diff --quiet -u --match "hash %s"' % self.hash
686 f = run_darcs(params)
687 return f.readlines()
689 def matches(self, s):
690 "Defines if the patch matches a given string"
691 if s.lower() in self.comment.lower():
692 return self.comment
693 elif s.lower() in self.name.lower():
694 return self.name
695 elif s.lower() in self.author.lower():
696 return self.author
697 elif s == self.hash:
698 return self.hash
700 s = s.lower()
701 for l in (self.adds, self.removes, self.modifies,
702 self.diradds, self.dirremoves,
703 self.replaces.keys(), self.moves.keys(),
704 self.moves.keys() ):
705 for i in l:
706 if s in i.lower():
707 return i
708 return ''
710 class XmlInputWrapper:
711 def __init__(self, fd):
712 self.fd = fd
713 self.times = 0
714 self._read = self.read
716 def read(self, *args, **kwargs):
717 self.times += 1
718 if self.times == 1:
719 return '<?xml version="1.0" encoding="utf-8"?>\n'
720 s = self.fd.read(*args, **kwargs)
721 if not s:
722 return s
723 return fixu8(s)
725 def close(self, *args, **kwargs):
726 return self.fd.close(*args, **kwargs)
729 # patch parsing, we get them through "darcs changes --xml-output"
730 class BuildPatchList(xml.sax.handler.ContentHandler):
731 def __init__(self):
732 self.db = {}
733 self.list = []
734 self.cur_hash = ''
735 self.cur_elem = None
736 self.cur_val = ''
737 self.cur_file = ''
739 def startElement(self, name, attrs):
740 # When you ask for changes to a given file, the xml output
741 # begins with the patch that creates it is enclosed in a
742 # "created_as" tag; then, later, it gets shown again in its
743 # usual place. The following two "if"s take care of ignoring
744 # everything inside the "created_as" tag, since we don't care.
745 if name == 'created_as':
746 self.cur_elem = 'created_as'
747 return
748 if self.cur_elem == 'created_as':
749 return
751 # now parse the tags normally
752 if name == 'patch':
753 p = Patch()
754 p.hash = fixu8(attrs.get('hash'))
756 au = attrs.get('author', None)
757 p.author = fixu8(escape(au))
758 if au.find('<') != -1:
759 au = au[:au.find('<')].strip()
760 p.shortauthor = fixu8(escape(au))
762 td = parse_darcs_time(attrs.get('date', None))
763 p.date = time.mktime(td)
764 p.date_str = time.strftime("%a, %d %b %Y %H:%M:%S", td)
766 td = time.strptime(attrs.get('local_date', None),
767 "%a %b %d %H:%M:%S %Z %Y")
768 p.local_date = time.mktime(td)
769 p.local_date_str = \
770 time.strftime("%a, %d %b %Y %H:%M:%S", td)
772 inverted = attrs.get('inverted', None)
773 if inverted and inverted == 'True':
774 p.inverted = True
776 self.db[p.hash] = p
777 self.current = p.hash
778 self.list.append(p.hash)
779 elif name == 'name':
780 self.db[self.current].name = ''
781 self.cur_elem = 'name'
782 elif name == 'comment':
783 self.db[self.current].comment = ''
784 self.cur_elem = 'comment'
785 elif name == 'add_file':
786 self.cur_elem = 'add_file'
787 elif name == 'remove_file':
788 self.cur_elem = 'remove_file'
789 elif name == 'add_directory':
790 self.cur_elem = 'add_directory'
791 elif name == 'remove_directory':
792 self.cur_elem = 'remove_dir'
793 elif name == 'modify_file':
794 self.cur_elem = 'modify_file'
795 elif name == 'removed_lines':
796 if self.cur_val:
797 self.cur_file = fixu8(self.cur_val.strip())
798 cf = self.cur_file
799 p = self.db[self.current]
800 # the current value holds the file name at this point
801 if not p.modifies.has_key(cf):
802 p.modifies[cf] = { '+': 0, '-': 0 }
803 p.modifies[cf]['-'] = int(attrs.get('num', None))
804 elif name == 'added_lines':
805 if self.cur_val:
806 self.cur_file = fixu8(self.cur_val.strip())
807 cf = self.cur_file
808 p = self.db[self.current]
809 if not p.modifies.has_key(cf):
810 p.modifies[cf] = { '+': 0, '-': 0 }
811 p.modifies[cf]['+'] = int(attrs.get('num', None))
812 elif name == 'move':
813 src = fixu8(attrs.get('from', None))
814 dst = fixu8(attrs.get('to', None))
815 p = self.db[self.current]
816 p.moves[src] = dst
817 elif name == 'replaced_tokens':
818 if self.cur_val:
819 self.cur_file = fixu8(self.cur_val.strip())
820 cf = self.cur_file
821 p = self.db[self.current]
822 if not p.replaces.has_key(cf):
823 p.replaces[cf] = 0
824 p.replaces[cf] = int(attrs.get('num', None))
825 else:
826 self.cur_elem = None
828 def characters(self, s):
829 if not self.cur_elem:
830 return
831 self.cur_val += s
833 def endElement(self, name):
834 # See the comment in startElement()
835 if name == 'created_as':
836 self.cur_elem = None
837 self.cur_val = ''
838 return
839 if self.cur_elem == 'created_as':
840 return
841 if name == 'replaced_tokens':
842 return
844 if name == 'name':
845 p = self.db[self.current]
846 p.name = fixu8(self.cur_val)
847 if p.inverted:
848 p.name = 'UNDO: ' + p.name
849 elif name == 'comment':
850 self.db[self.current].comment = fixu8(strip_ignore_this(self.cur_val))
851 elif name == 'add_file':
852 scv = fixu8(self.cur_val.strip())
853 self.db[self.current].adds.append(scv)
854 elif name == 'remove_file':
855 scv = fixu8(self.cur_val.strip())
856 self.db[self.current].removes.append(scv)
857 elif name == 'add_directory':
858 scv = fixu8(self.cur_val.strip())
859 self.db[self.current].diradds.append(scv)
860 elif name == 'remove_directory':
861 scv = fixu8(self.cur_val.strip())
862 self.db[self.current].dirremoves.append(scv)
864 elif name == 'modify_file':
865 if not self.cur_file:
866 # binary modification appear without a line
867 # change summary, so we add it manually here
868 f = fixu8(self.cur_val.strip())
869 p = self.db[self.current]
870 p.modifies[f] = { '+': 0, '-': 0, 'b': 1 }
871 self.cur_file = ''
873 self.cur_elem = None
874 self.cur_val = ''
876 def get_list(self):
877 plist = []
878 for h in self.list:
879 plist.append(self.db[h])
880 return plist
882 def get_db(self):
883 return self.db
885 def get_list_db(self):
886 return (self.list, self.db)
888 def get_changes_handler(params):
889 "Returns a handler for the changes output, run with the given params"
890 parser = xml.sax.make_parser()
891 handler = BuildPatchList()
892 parser.setContentHandler(handler)
894 # get the xml output and parse it
895 xmlf = run_darcs("changes --xml-output " + params)
896 parser.parse(XmlInputWrapper(xmlf))
897 xmlf.close()
899 return handler
901 def get_last_patches(last = 15, topi = 0, fname = None):
902 """Gets the last N patches from the repo, returns a patch list. If
903 "topi" is specified, then it will return the N patches that preceeded
904 the patch number topi in the list. It sounds messy but it's quite
905 simple. You can optionally pass a filename and only changes that
906 affect it will be returned. FIXME: there's probably a more efficient
907 way of doing this."""
909 # darcs calculate last first, and then filters the filename,
910 # so it's not so simple to combine them; that's why we do so much
911 # special casing here
912 toget = last + topi
914 if fname:
915 if fname[0] == '/': fname = fname[1:]
916 s = '-s "%s"' % fname
917 else:
918 s = "-s --last=%d" % toget
920 handler = get_changes_handler(s)
922 # return the list of all the patch objects
923 return handler.get_list()[topi:toget]
925 def get_patch(hash):
926 handler = get_changes_handler('-s --match "hash %s"' % hash)
927 patch = handler.db[handler.list[0]]
928 return patch
930 def get_diff(hash):
931 return run_darcs('diff --quiet -u --match "hash %s"' % hash)
933 def get_file_diff(hash, fname):
934 return run_darcs('diff --quiet -u --match "hash %s" "%s"' % (hash, fname))
936 def get_file_headdiff(hash, fname):
937 return run_darcs('diff --quiet -u --from-match "hash %s" "%s"' % (hash, fname))
939 def get_patch_headdiff(hash):
940 return run_darcs('diff --quiet -u --from-match "hash %s"' % hash)
942 def get_raw_diff(hash):
943 import gzip
944 realf = filter_file(config.repodir + '/_darcs/patches/' + hash)
945 if not os.path.isfile(realf):
946 return None
947 file = open(realf, 'rb')
948 if file.read(2) == '\x1f\x8b':
949 # file begins with gzip magic
950 file.close()
951 dsrc = gzip.open(realf)
952 else:
953 file.seek(0)
954 dsrc = file
955 return dsrc
957 def get_darcs_diff(hash, fname = None):
958 cmd = 'changes -v --matches "hash %s"' % hash
959 if fname:
960 cmd += ' "%s"' % fname
961 return run_darcs(cmd)
963 def get_darcs_headdiff(hash, fname = None):
964 cmd = 'changes -v --from-match "hash %s"' % hash
965 if fname:
966 cmd += ' "%s"' % fname
967 return run_darcs(cmd)
970 class Annotate:
971 def __init__(self):
972 self.fname = ""
973 self.creator_hash = ""
974 self.created_as = ""
975 self.lastchange_hash = ""
976 self.lastchange_author = ""
977 self.lastchange_name = ""
978 self.lastchange_date = None
979 self.firstdate = None
980 self.lastdate = None
981 self.lines = []
982 self.patches = {}
984 class Line:
985 def __init__(self):
986 self.text = ""
987 self.phash = None
988 self.pauthor = None
989 self.pdate = None
991 def parse_annotate(src):
992 import xml.dom.minidom
994 annotate = Annotate()
996 # FIXME: convert the source to UTF8; it _has_ to be a way to let
997 # minidom know the source encoding
998 s = ""
999 for i in src:
1000 s += fixu8(i)
1002 dom = xml.dom.minidom.parseString(s)
1004 file = dom.getElementsByTagName("file")[0]
1005 annotate.fname = fixu8(file.getAttribute("name"))
1007 createinfo = dom.getElementsByTagName("created_as")[0]
1008 annotate.created_as = fixu8(createinfo.getAttribute("original_name"))
1010 creator = createinfo.getElementsByTagName("patch")[0]
1011 annotate.creator_hash = fixu8(creator.getAttribute("hash"))
1013 mod = dom.getElementsByTagName("modified")[0]
1014 lastpatch = mod.getElementsByTagName("patch")[0]
1015 annotate.lastchange_hash = fixu8(lastpatch.getAttribute("hash"))
1016 annotate.lastchange_author = fixu8(lastpatch.getAttribute("author"))
1018 lastname = lastpatch.getElementsByTagName("name")[0]
1019 lastname = lastname.childNodes[0].wholeText
1020 annotate.lastchange_name = fixu8(lastname)
1022 lastdate = parse_darcs_time(lastpatch.getAttribute("date"))
1023 annotate.lastchange_date = lastdate
1025 annotate.patches[annotate.lastchange_hash] = annotate.lastchange_date
1027 # these will be overriden by the real dates later
1028 annotate.firstdate = lastdate
1029 annotate.lastdate = 0
1031 file = dom.getElementsByTagName("file")[0]
1033 for l in file.childNodes:
1034 # we're only intrested in normal and added lines
1035 if l.nodeName not in ["normal_line", "added_line"]:
1036 continue
1037 line = Annotate.Line()
1039 if l.nodeName == "normal_line":
1040 patch = l.getElementsByTagName("patch")[0]
1041 phash = patch.getAttribute("hash")
1042 pauthor = patch.getAttribute("author")
1043 pdate = patch.getAttribute("date")
1044 pdate = parse_darcs_time(pdate)
1045 else:
1046 # added lines inherit the creation from the annotate
1047 # patch
1048 phash = annotate.lastchange_hash
1049 pauthor = annotate.lastchange_author
1050 pdate = annotate.lastchange_date
1052 text = ""
1053 for node in l.childNodes:
1054 if node.nodeType == node.TEXT_NODE:
1055 text += node.wholeText
1057 # strip all "\n"s at the beginning; because the way darcs
1058 # formats the xml output it makes the DOM parser to add "\n"s
1059 # in front of it
1060 text = text.lstrip("\n")
1062 line.text = fixu8(text)
1063 line.phash = fixu8(phash)
1064 line.pauthor = fixu8(pauthor)
1065 line.pdate = pdate
1066 annotate.lines.append(line)
1067 annotate.patches[line.phash] = line.pdate
1069 if pdate > annotate.lastdate:
1070 annotate.lastdate = pdate
1071 if pdate < annotate.firstdate:
1072 annotate.firstdate = pdate
1074 return annotate
1076 def get_annotate(fname, hash = None):
1077 if config.disable_annotate:
1078 return None
1080 cmd = 'annotate --xml-output'
1081 if hash:
1082 cmd += ' --match="hash %s"' % hash
1084 if fname.startswith('/'):
1085 # darcs 2 doesn't like files starting with /, and darcs 1
1086 # doesn't really care
1087 fname = fname[1:]
1088 cmd += ' "%s"' % fname
1090 return parse_annotate(run_darcs(cmd))
1095 # specific html functions
1098 def print_diff(dsrc):
1099 for l in dsrc:
1100 l = fixu8(l)
1102 # remove the trailing newline
1103 if len(l) > 1:
1104 l = l[:-1]
1106 if l.startswith('diff'):
1107 # file lines, they have their own class
1108 print '<div class="diff_info">%s</div>' % escape(l)
1109 continue
1111 color = ""
1112 if l[0] == '+':
1113 color = 'style="color:#008800;"'
1114 elif l[0] == '-':
1115 color = 'style="color:#cc0000;"'
1116 elif l[0] == '@':
1117 color = 'style="color:#990099; '
1118 color += 'border: solid #ffe0ff; '
1119 color += 'border-width: 1px 0px 0px 0px; '
1120 color += 'margin-top: 2px;"'
1121 elif l.startswith('Files'):
1122 # binary differences
1123 color = 'style="color:#666;"'
1124 print '<div class="pre" %s>' % color + escape(l) + '</div>'
1127 def print_darcs_diff(dsrc):
1128 for l in dsrc:
1129 l = fixu8(l)
1131 if not l.startswith(" "):
1132 # comments and normal stuff
1133 print '<div class="pre">' + escape(l) + "</div>"
1134 continue
1136 l = l.strip()
1137 if not l:
1138 continue
1140 if l[0] == '+':
1141 cl = 'class="pre" style="color:#008800;"'
1142 elif l[0] == '-':
1143 cl = 'class="pre" style="color:#cc0000;"'
1144 else:
1145 cl = 'class="diff_info"'
1146 print '<div %s>' % cl + escape(l) + '</div>'
1149 def print_shortlog(last = PATCHES_PER_PAGE, topi = 0, fname = None):
1150 ps = get_last_patches(last, topi, fname)
1152 if fname:
1153 title = '<a class="title" href="%s;a=filehistory;f=%s">' % \
1154 (config.myreponame, fname)
1155 title += 'History for path %s' % escape(fname)
1156 title += '</a>'
1157 else:
1158 title = '<a class="title" href="%s;a=shortlog">shortlog</a>' \
1159 % config.myreponame
1161 print '<div>%s</div>' % title
1162 print '<table cellspacing="0">'
1164 if topi != 0:
1165 # put a link to the previous page
1166 ntopi = topi - last
1167 if ntopi < 0:
1168 ntopi = 0
1169 print '<tr><td>'
1170 if fname:
1171 print '<a href="%s;a=filehistory;topi=%d;f=%s">...</a>' \
1172 % (config.myreponame, ntopi, fname)
1173 else:
1174 print '<a href="%s;a=shortlog;topi=%d">...</a>' \
1175 % (config.myreponame, ntopi)
1176 print '</td></tr>'
1178 alt = True
1179 for p in ps:
1180 if p.name.startswith("TAG "):
1181 print '<tr class="tag">'
1182 elif alt:
1183 print '<tr class="dark">'
1184 else:
1185 print '<tr class="light">'
1186 alt = not alt
1188 print """
1189 <td><i>%(age)s</i></td>
1190 <td>%(author)s</td>
1191 <td>
1192 <a class="list" title="%(fullname)s" href="%(myrname)s;a=commit;h=%(hash)s">
1193 <b>%(name)s</b>
1194 </a>
1195 </td>
1196 <td class="link">
1197 <a href="%(myrname)s;a=commit;h=%(hash)s">commit</a> |
1198 <a href="%(myrname)s;a=commitdiff;h=%(hash)s">commitdiff</a>
1199 </td>
1200 """ % {
1201 'age': how_old(p.local_date),
1202 'author': gen_authorlink(p.author, shorten_str(p.shortauthor, 26)),
1203 'myrname': config.myreponame,
1204 'hash': p.hash,
1205 'name': escape(shorten_str(p.name)),
1206 'fullname': escape(p.name),
1208 print "</tr>"
1210 if len(ps) >= last:
1211 # only show if we've not shown them all already
1212 print '<tr><td>'
1213 if fname:
1214 print '<a href="%s;a=filehistory;topi=%d;f=%s">...</a>' \
1215 % (config.myreponame, topi + last, fname)
1216 else:
1217 print '<a href="%s;a=shortlog;topi=%d">...</a>' \
1218 % (config.myreponame, topi + last)
1219 print '</td></tr>'
1220 print "</table>"
1223 def print_log(last = PATCHES_PER_PAGE, topi = 0):
1224 ps = get_last_patches(last, topi)
1226 if topi != 0:
1227 # put a link to the previous page
1228 ntopi = topi - last
1229 if ntopi < 0:
1230 ntopi = 0
1231 print '<p/><a href="%s;a=log;topi=%d">&lt;- Prev</a><p/>' % \
1232 (config.myreponame, ntopi)
1234 for p in ps:
1235 if p.comment:
1236 comment = replace_links(escape(p.comment))
1237 fmt_comment = comment.replace('\n', '<br/>') + '\n'
1238 fmt_comment += '<br/><br/>'
1239 else:
1240 fmt_comment = ''
1241 print """
1242 <div><a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1243 <span class="age">%(age)s</span>%(desc)s
1244 </a></div>
1245 <div class="title_text">
1246 <div class="log_link">
1247 <a href="%(myreponame)s;a=commit;h=%(hash)s">commit</a> |
1248 <a href="%(myreponame)s;a=commitdiff;h=%(hash)s">commitdiff</a><br/>
1249 </div>
1250 <i>%(author)s [%(date)s]</i><br/>
1251 </div>
1252 <div class="log_body">
1253 %(comment)s
1254 </div>
1256 """ % {
1257 'myreponame': config.myreponame,
1258 'age': how_old(p.local_date),
1259 'date': p.local_date_str,
1260 'author': gen_authorlink(p.author, p.shortauthor),
1261 'hash': p.hash,
1262 'desc': escape(p.name),
1263 'comment': fmt_comment
1266 if len(ps) >= last:
1267 # only show if we've not shown them all already
1268 print '<p><a href="%s;a=log;topi=%d">Next -&gt;</a></p>' % \
1269 (config.myreponame, topi + last)
1272 def print_blob(fname):
1273 print '<div class="page_path"><b>%s</b></div>' % escape(fname)
1274 if isbinary(fname):
1275 print """
1276 <div class="page_body">
1277 <i>This is a binary file and its contents will not be displayed.</i>
1278 </div>
1280 return
1282 try:
1283 import pygments
1284 except ImportError:
1285 pygments = False
1287 if not pygments:
1288 print_blob_simple(fname)
1289 return
1290 else:
1291 try:
1292 print_blob_highlighted(fname)
1293 except ValueError:
1294 # pygments couldn't guess a lexer to highlight the code, try
1295 # another method with sampling the file contents.
1296 try:
1297 print_blob_highlighted(fname, sample_code=True)
1298 except ValueError:
1299 # pygments really could not find any lexer for this file.
1300 print_blob_simple(fname)
1302 def print_blob_simple(fname):
1303 print '<div class="page_body">'
1305 f = open(realpath(fname), 'r')
1306 count = 1
1307 for l in f:
1308 l = fixu8(escape(l))
1309 if l and l[-1] == '\n':
1310 l = l[:-1]
1311 l = replace_tabs(l)
1313 print """\
1314 <div class="pre">\
1315 <a id="l%(c)d" href="#l%(c)d" class="linenr">%(c)4d</a> %(l)s\
1316 </div>\
1317 """ % {
1318 'c': count,
1319 'l': l
1321 count += 1
1322 print '</div>'
1324 def print_blob_highlighted(fname, sample_code=False):
1325 import pygments
1326 import pygments.lexers
1327 import pygments.formatters
1329 code = open(realpath(fname), 'r').read()
1330 if sample_code:
1331 lexer = pygments.lexers.guess_lexer(code[:200],
1332 encoding=config.repoencoding[0])
1333 else:
1334 lexer = pygments.lexers.guess_lexer_for_filename(fname, code[:200],
1335 encoding=config.repoencoding[0])
1337 pygments_version = map(int, pygments.__version__.split('.'))
1338 if pygments_version >= [0, 7]:
1339 linenos_method = 'inline'
1340 else:
1341 linenos_method = True
1342 formatter = pygments.formatters.HtmlFormatter(linenos=linenos_method,
1343 cssclass='page_body')
1345 print pygments.highlight(code, lexer, formatter)
1347 def print_annotate(ann, style):
1348 print '<div class="page_body">'
1349 if isbinary(ann.fname):
1350 print """
1351 <i>This is a binary file and its contents will not be displayed.</i>
1352 </div>
1354 return
1356 if style == 'shade':
1357 # here's the idea: we will assign to each patch a shade of
1358 # color from its date (newer gets darker)
1359 max = 0xff
1360 min = max - 80
1362 # to do that, we need to get a list of the patch hashes
1363 # ordered by their dates
1364 l = [ (date, hash) for (hash, date) in ann.patches.items() ]
1365 l.sort()
1366 l = [ hash for (date, hash) in l ]
1368 # now we have to map each element to a number in the range
1369 # min-max, with max being close to l[0] and min l[len(l) - 1]
1370 lenn = max - min
1371 lenl = len(l)
1372 shadetable = {}
1373 for i in range(0, lenl):
1374 hash = l[i]
1375 n = float(i * lenn) / lenl
1376 n = max - int(round(n))
1377 shadetable[hash] = n
1378 elif style == "zebra":
1379 lineclass = 'dark'
1381 count = 1
1382 prevhash = None
1383 for l in ann.lines:
1384 text = escape(l.text)
1385 text = text.rstrip()
1386 text = replace_tabs(text)
1387 plongdate = time.strftime("%Y-%m-%d %H:%M:%S", l.pdate)
1388 title = "%s by %s" % (plongdate, escape(l.pauthor) )
1390 link = "%(myrname)s;a=commit;h=%(hash)s" % {
1391 'myrname': config.myreponame,
1392 'hash': l.phash
1395 if style == "shade":
1396 linestyle = 'style="background-color:#ffff%.2x"' % \
1397 shadetable[l.phash]
1398 lineclass = ''
1399 elif style == "zebra":
1400 linestyle = ''
1401 if l.phash != prevhash:
1402 if lineclass == 'dark':
1403 lineclass = 'light'
1404 else:
1405 lineclass = 'dark'
1406 else:
1407 linestyle = ''
1408 lineclass = ''
1410 if l.phash != prevhash:
1411 pdate = time.strftime("%Y-%m-%d", l.pdate)
1413 left = l.pauthor.find('<')
1414 right = l.pauthor.find('@')
1415 if left != -1 and right != -1:
1416 shortau = l.pauthor[left + 1:right]
1417 elif l.pauthor.find(" ") != -1:
1418 shortau = l.pauthor[:l.pauthor.find(" ")]
1419 elif right != -1:
1420 shortau = l.pauthor[:right]
1421 else:
1422 shortau = l.pauthor
1424 desc = "%12.12s" % shortau
1425 date = "%-10.10s" % pdate
1426 prevhash = l.phash
1427 line = 1
1428 else:
1429 if line == 1 and style in ["shade", "zebra"]:
1430 t = "%s " % time.strftime("%H:%M:%S", l.pdate)
1431 desc = "%12.12s" % "'"
1432 date = "%-10.10s" % t
1433 else:
1434 desc = "%12.12s" % "'"
1435 date = "%-10.10s" % ""
1436 line += 1
1438 print """\
1439 <div class="pre %(class)s" %(style)s>\
1440 <a href="%(link)s" title="%(title)s" class="annotate_desc">%(date)s %(desc)s</a> \
1441 <a href="%(link)s" title="%(title)s" class="linenr">%(c)4d</a> \
1442 <a href="%(link)s" title="%(title)s" class="line">%(text)s</a>\
1443 </div>\
1444 """ % {
1445 'class': lineclass,
1446 'style': linestyle,
1447 'date': date,
1448 'desc': escape(desc),
1449 'c': count,
1450 'text': text,
1451 'title': title,
1452 'link': link
1455 count += 1
1457 print '</div>'
1461 # available actions
1464 def do_summary():
1465 print_header()
1466 print_navbar()
1467 owner = repo_get_owner()
1469 # we should optimize this, it's a pity to go in such a mess for just
1470 # one hash
1471 ps = get_last_patches(1)
1473 print '<div class="title">&nbsp;</div>'
1474 print '<table cellspacing="0">'
1475 print ' <tr><td>description</td><td>%s</td></tr>' % \
1476 escape(config.repodesc)
1477 if owner:
1478 print ' <tr><td>owner</td><td>%s</td></tr>' % escape(owner)
1479 if len(ps) > 0:
1480 print ' <tr><td>last change</td><td>%s</td></tr>' % \
1481 ps[0].local_date_str
1482 print ' <tr><td>url</td><td><a href="%(url)s">%(url)s</a></td></tr>' %\
1483 { 'url': config.repourl }
1484 if config.repoprojurl:
1485 print ' <tr><td>project url</td>'
1486 print ' <td><a href="%(url)s">%(url)s</a></td></tr>' % \
1487 { 'url': config.repoprojurl }
1488 print '</table>'
1490 print_shortlog(15)
1491 print_footer()
1494 def do_commitdiff(phash):
1495 print_header()
1496 print_navbar(h = phash)
1497 p = get_patch(phash)
1498 print """
1499 <div>
1500 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1501 </div>
1502 """ % {
1503 'myreponame': config.myreponame,
1504 'hash': p.hash,
1505 'name': escape(p.name),
1508 dsrc = p.getdiff()
1509 print_diff(dsrc)
1510 print_footer()
1512 def do_plain_commitdiff(phash):
1513 print_plain_header()
1514 dsrc = get_diff(phash)
1515 for l in dsrc:
1516 sys.stdout.write(fixu8(l))
1518 def do_darcs_commitdiff(phash):
1519 print_header()
1520 print_navbar(h = phash)
1521 p = get_patch(phash)
1522 print """
1523 <div>
1524 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1525 </div>
1526 """ % {
1527 'myreponame': config.myreponame,
1528 'hash': p.hash,
1529 'name': escape(p.name),
1532 dsrc = get_darcs_diff(phash)
1533 print_darcs_diff(dsrc)
1534 print_footer()
1536 def do_raw_commitdiff(phash):
1537 print_plain_header()
1538 dsrc = get_raw_diff(phash)
1539 if not dsrc:
1540 print "Error opening file!"
1541 return
1542 for l in dsrc:
1543 sys.stdout.write(l)
1546 def do_headdiff(phash):
1547 print_header()
1548 print_navbar(h = phash)
1549 p = get_patch(phash)
1550 print """
1551 <div>
1552 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1553 %(name)s --&gt; to head</a>
1554 </div>
1555 """ % {
1556 'myreponame': config.myreponame,
1557 'hash': p.hash,
1558 'name': escape(p.name),
1561 dsrc = get_patch_headdiff(phash)
1562 print_diff(dsrc)
1563 print_footer()
1565 def do_plain_headdiff(phash):
1566 print_plain_header()
1567 dsrc = get_patch_headdiff(phash)
1568 for l in dsrc:
1569 sys.stdout.write(fixu8(l))
1571 def do_darcs_headdiff(phash):
1572 print_header()
1573 print_navbar(h = phash)
1574 p = get_patch(phash)
1575 print """
1576 <div>
1577 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1578 %(name)s --&gt; to head</a>
1579 </div>
1580 """ % {
1581 'myreponame': config.myreponame,
1582 'hash': p.hash,
1583 'name': escape(p.name),
1586 dsrc = get_darcs_headdiff(phash)
1587 print_darcs_diff(dsrc)
1588 print_footer()
1590 def do_raw_headdiff(phash):
1591 print_plain_header()
1592 dsrc = get_darcs_headdiff(phash)
1593 for l in dsrc:
1594 sys.stdout.write(l)
1597 def do_filediff(phash, fname):
1598 print_header()
1599 print_navbar(h = phash, f = fname)
1600 p = get_patch(phash)
1601 dsrc = get_file_diff(phash, fname)
1602 print """
1603 <div>
1604 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1605 </div>
1606 <div class="page_path"><b>%(fname)s</b></div>
1607 """ % {
1608 'myreponame': config.myreponame,
1609 'hash': p.hash,
1610 'name': escape(p.name),
1611 'fname': escape(fname),
1614 print_diff(dsrc)
1615 print_footer()
1617 def do_plain_filediff(phash, fname):
1618 print_plain_header()
1619 dsrc = get_file_diff(phash, fname)
1620 for l in dsrc:
1621 sys.stdout.write(fixu8(l))
1623 def do_darcs_filediff(phash, fname):
1624 print_header()
1625 print_navbar(h = phash, f = fname)
1626 p = get_patch(phash)
1627 print """
1628 <div>
1629 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1630 </div>
1631 <div class="page_path"><b>%(fname)s</b></div>
1632 """ % {
1633 'myreponame': config.myreponame,
1634 'hash': p.hash,
1635 'name': escape(p.name),
1636 'fname': escape(fname),
1639 dsrc = get_darcs_diff(phash, fname)
1640 print_darcs_diff(dsrc)
1641 print_footer()
1644 def do_file_headdiff(phash, fname):
1645 print_header()
1646 print_navbar(h = phash, f = fname)
1647 p = get_patch(phash)
1648 dsrc = get_file_headdiff(phash, fname)
1649 print """
1650 <div>
1651 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1652 %(name)s --&gt; to head</a>
1653 </div>
1654 <div class="page_path"><b>%(fname)s</b></div>
1655 """ % {
1656 'myreponame': config.myreponame,
1657 'hash': p.hash,
1658 'name': escape(p.name),
1659 'fname': escape(fname),
1662 print_diff(dsrc)
1663 print_footer()
1665 def do_plain_fileheaddiff(phash, fname):
1666 print_plain_header()
1667 dsrc = get_file_headdiff(phash, fname)
1668 for l in dsrc:
1669 sys.stdout.write(fixu8(l))
1671 def do_darcs_fileheaddiff(phash, fname):
1672 print_header()
1673 print_navbar(h = phash, f = fname)
1674 p = get_patch(phash)
1675 print """
1676 <div>
1677 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1678 %(name)s --&gt; to head</a>
1679 </div>
1680 <div class="page_path"><b>%(fname)s</b></div>
1681 """ % {
1682 'myreponame': config.myreponame,
1683 'hash': p.hash,
1684 'name': escape(p.name),
1685 'fname': escape(fname),
1688 dsrc = get_darcs_headdiff(phash, fname)
1689 print_darcs_diff(dsrc)
1690 print_footer()
1692 print_plain_header()
1693 print "Not yet implemented"
1696 def do_commit(phash):
1697 print_header()
1698 print_navbar(h = phash)
1699 p = get_patch(phash)
1701 print """
1702 <div>
1703 <a class="title" href="%(myreponame)s;a=commitdiff;h=%(hash)s">%(name)s</a>
1704 </div>
1706 <div class="title_text">
1707 <table cellspacing="0">
1708 <tr><td>author</td><td>%(author)s</td></tr>
1709 <tr><td>local date</td><td>%(local_date)s</td></tr>
1710 <tr><td>date</td><td>%(date)s</td></tr>
1711 <tr><td>hash</td><td style="font-family: monospace">%(hash)s</td></tr>
1712 </table>
1713 </div>
1714 """ % {
1715 'myreponame': config.myreponame,
1716 'author': gen_authorlink(p.author),
1717 'local_date': p.local_date_str,
1718 'date': p.date_str,
1719 'hash': p.hash,
1720 'name': escape(p.name),
1722 if p.comment:
1723 comment = replace_links(escape(p.comment))
1724 c = comment.replace('\n', '<br/>\n')
1725 print '<div class="page_body">'
1726 print replace_links(escape(p.name)), '<br/><br/>'
1727 print c
1728 print '</div>'
1730 changed = p.adds + p.removes + p.modifies.keys() + p.moves.keys() + \
1731 p.diradds + p.dirremoves + p.replaces.keys()
1733 if changed or p.moves:
1734 n = len(changed)
1735 print '<div class="list_head">%d file(s) changed:</div>' % n
1737 print '<table cellspacing="0">'
1738 changed.sort()
1739 alt = True
1740 for f in changed:
1741 if alt:
1742 print '<tr class="dark">'
1743 else:
1744 print '<tr class="light">'
1745 alt = not alt
1747 show_diff = 1
1748 if p.moves.has_key(f):
1749 # don't show diffs for moves, they're broken as of
1750 # darcs 1.0.3
1751 show_diff = 0
1753 if show_diff:
1754 print """
1755 <td>
1756 <a class="list" href="%(myreponame)s;a=filediff;h=%(hash)s;f=%(file)s">
1757 %(fname)s</a>
1758 </td>
1759 """ % {
1760 'myreponame': config.myreponame,
1761 'hash': p.hash,
1762 'file': urllib.quote(f),
1763 'fname': escape(f),
1765 else:
1766 print "<td>%s</td>" % f
1768 show_diff = 1
1769 if f in p.adds:
1770 print '<td><span style="color:#008000">',
1771 print '[added]',
1772 print '</span></td>'
1773 elif f in p.diradds:
1774 print '<td><span style="color:#008000">',
1775 print '[added dir]',
1776 print '</span></td>'
1777 elif f in p.removes:
1778 print '<td><span style="color:#800000">',
1779 print '[removed]',
1780 print '</span></td>'
1781 elif f in p.dirremoves:
1782 print '<td><span style="color:#800000">',
1783 print '[removed dir]',
1784 print '</span></td>'
1785 elif p.replaces.has_key(f):
1786 print '<td><span style="color:#800000">',
1787 print '[replaced %d tokens]' % p.replaces[f],
1788 print '</span></td>'
1789 elif p.moves.has_key(f):
1790 print '<td><span style="color:#000080">',
1791 print '[moved to "%s"]' % p.moves[f]
1792 print '</span></td>'
1793 show_diff = 0
1794 else:
1795 print '<td><span style="color:#000080">',
1796 if p.modifies[f].has_key('b'):
1797 # binary modification
1798 print '(binary)'
1799 else:
1800 print '+%(+)d -%(-)d' % p.modifies[f],
1801 print '</span></td>'
1803 if show_diff:
1804 print """
1805 <td class="link">
1806 <a href="%(myreponame)s;a=filediff;h=%(hash)s;f=%(file)s">diff</a> |
1807 <a href="%(myreponame)s;a=filehistory;f=%(file)s">history</a> |
1808 <a href="%(myreponame)s;a=annotate_shade;h=%(hash)s;f=%(file)s">annotate</a>
1809 </td>
1810 """ % {
1811 'myreponame': config.myreponame,
1812 'hash': p.hash,
1813 'file': urllib.quote(f)
1815 print '</tr>'
1816 print '</table>'
1817 print_footer()
1820 def do_tree(dname):
1821 print_header()
1822 print_navbar()
1824 # the head
1825 print """
1826 <div><a class="title" href="%s;a=tree">Current tree</a></div>
1827 <div class="page_path"><b>
1828 """ % config.myreponame
1830 # and the linked, with links
1831 parts = dname.split('/')
1832 print '/ '
1833 sofar = '/'
1834 for p in parts:
1835 if not p: continue
1836 sofar += '/' + p
1837 print '<a href="%s;a=tree;f=%s">%s</a> /' % \
1838 (config.myreponame, urllib.quote(sofar), p)
1840 print """
1841 </b></div>
1842 <div class="page_body">
1843 <table cellspacing="0">
1846 path = realpath(dname) + '/'
1848 alt = True
1849 files = os.listdir(path)
1850 files.sort()
1852 # list directories first
1853 dlist = []
1854 flist = []
1855 for f in files:
1856 if f == "_darcs":
1857 continue
1858 realfile = path + f
1859 if os.path.isdir(realfile):
1860 dlist.append(f)
1861 else:
1862 flist.append(f)
1863 files = dlist + flist
1865 for f in files:
1866 if alt:
1867 print '<tr class="dark">'
1868 else:
1869 print '<tr class="light">'
1870 alt = not alt
1871 realfile = path + f
1872 fullf = filter_file(dname + '/' + f)
1873 print '<td style="font-family:monospace">', fperms(realfile),
1874 print '</td>'
1875 print '<td style="font-family:monospace">', fsize(realfile),
1876 print '</td>'
1878 if f in dlist:
1879 print """
1880 <td>
1881 <a class="link" href="%(myrname)s;a=tree;f=%(fullf)s">%(f)s/</a>
1882 </td>
1883 <td class="link">
1884 <a href="%(myrname)s;a=filehistory;f=%(fullf)s">history</a> |
1885 <a href="%(myrname)s;a=tree;f=%(fullf)s">tree</a>
1886 </td>
1887 """ % {
1888 'myrname': config.myreponame,
1889 'f': escape(f),
1890 'fullf': urllib.quote(fullf),
1892 else:
1893 print """
1894 <td><a class="list" href="%(myrname)s;a=headblob;f=%(fullf)s">%(f)s</a></td>
1895 <td class="link">
1896 <a href="%(myrname)s;a=filehistory;f=%(fullf)s">history</a> |
1897 <a href="%(myrname)s;a=headblob;f=%(fullf)s">headblob</a> |
1898 <a href="%(myrname)s;a=annotate_shade;f=%(fullf)s">annotate</a>
1899 </td>
1900 """ % {
1901 'myrname': config.myreponame,
1902 'f': escape(f),
1903 'fullf': urllib.quote(fullf),
1905 print '</tr>'
1906 print '</table></div>'
1907 print_footer()
1910 def do_headblob(fname):
1911 print_header()
1912 print_navbar(f = fname)
1913 filepath = os.path.dirname(fname)
1915 if filepath == '/':
1916 print '<div><a class="title" href="%s;a=tree">/</a></div>' % \
1917 (config.myreponame)
1918 else:
1919 print '<div class="title"><b>'
1921 # and the linked, with links
1922 parts = filepath.split('/')
1923 print '/ '
1924 sofar = '/'
1925 for p in parts:
1926 if not p: continue
1927 sofar += '/' + p
1928 print '<a href="%s;a=tree;f=%s">%s</a> /' % \
1929 (config.myreponame, sofar, p)
1931 print '</b></div>'
1933 print_blob(fname)
1934 print_footer()
1937 def do_plainblob(fname):
1938 f = open(realpath(fname), 'r')
1940 if isbinary(fname):
1941 print_binary_header(os.path.basename(fname))
1942 for l in f:
1943 sys.stdout.write(l)
1944 else:
1945 print_plain_header()
1946 for l in f:
1947 sys.stdout.write(fixu8(l))
1950 def do_annotate(fname, phash, style):
1951 print_header()
1952 ann = get_annotate(fname, phash)
1953 if not ann:
1954 print """
1955 <i>The annotate feature has been disabled</i>
1956 </div>
1958 print_footer()
1959 return
1960 print_navbar(f = fname, h = ann.lastchange_hash)
1962 print """
1963 <div>
1964 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1965 </div>
1966 <div class="page_path"><b>
1967 Annotate for file %(fname)s
1968 </b></div>
1969 """ % {
1970 'myreponame': config.myreponame,
1971 'hash': ann.lastchange_hash,
1972 'name': escape(ann.lastchange_name),
1973 'fname': escape(fname),
1976 print_annotate(ann, style)
1977 print_footer()
1979 def do_annotate_plain(fname, phash):
1980 print_plain_header()
1981 ann = get_annotate(fname, phash)
1982 for l in ann.lines:
1983 sys.stdout.write(l.text)
1986 def do_shortlog(topi, last=PATCHES_PER_PAGE):
1987 print_header()
1988 print_navbar()
1989 print_shortlog(topi = topi, last = last)
1990 print_footer()
1992 def do_filehistory(topi, f, last=PATCHES_PER_PAGE):
1993 print_header()
1994 print_navbar(f = fname)
1995 print_shortlog(topi = topi, fname = fname, last = last)
1996 print_footer()
1998 def do_log(topi, last=PATCHES_PER_PAGE):
1999 print_header()
2000 print_navbar()
2001 print_log(topi = topi, last = last)
2002 print_footer()
2004 def do_atom():
2005 print "Content-type: application/atom+xml; charset=utf-8\n"
2006 print '<?xml version="1.0" encoding="utf-8"?>'
2007 inv = config.repodir + '/_darcs/patches'
2008 repo_lastmod = os.stat(inv).st_mtime
2009 str_lastmod = time.strftime(iso_datetime,
2010 time.localtime(repo_lastmod))
2012 print """
2013 <feed xmlns="http://www.w3.org/2005/Atom">
2014 <title>%(reponame)s darcs repository</title>
2015 <link rel="alternate" type="text/html" href="%(url)s"/>
2016 <link rel="self" type="application/atom+xml" href="%(url)s;a=atom"/>
2017 <id>%(url)s</id> <!-- TODO: find a better <id>, see RFC 4151 -->
2018 <author><name>darcs repository (several authors)</name></author>
2019 <generator>darcsweb.cgi</generator>
2020 <updated>%(lastmod)s</updated>
2021 <subtitle>%(desc)s</subtitle>
2022 """ % {
2023 'reponame': config.reponame,
2024 'url': config.myurl + '/' + config.myreponame,
2025 'desc': escape(config.repodesc),
2026 'lastmod': str_lastmod,
2029 ps = get_last_patches(20)
2030 for p in ps:
2031 title = time.strftime('%d %b %H:%M', time.localtime(p.date))
2032 title += ' - ' + p.name
2033 pdate = time.strftime(iso_datetime,
2034 time.localtime(p.date))
2035 link = '%s/%s;a=commit;h=%s' % (config.myurl,
2036 config.myreponame, p.hash)
2038 import email.Utils
2039 addr, author = email.Utils.parseaddr(p.author)
2040 if not addr:
2041 addr = "unknown_email@example.com"
2042 if not author:
2043 author = addr
2045 print """
2046 <entry>
2047 <title>%(title)s</title>
2048 <author>
2049 <name>%(author)s</name>
2050 <email>%(email)s</email>
2051 </author>
2052 <updated>%(pdate)s</updated>
2053 <id>%(link)s</id>
2054 <link rel="alternate" href="%(link)s"/>
2055 <summary>%(desc)s</summary>
2056 <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
2057 """ % {
2058 'title': escape(title),
2059 'author': author,
2060 'email': addr,
2061 'url': config.myurl + '/' + config.myreponame,
2062 'pdate': pdate,
2063 'myrname': config.myreponame,
2064 'hash': p.hash,
2065 'pname': escape(p.name),
2066 'link': link,
2067 'desc': escape(p.name),
2070 # TODO: allow to get plain text, not HTML?
2071 print escape(p.name) + '<br/>'
2072 if p.comment:
2073 print '<br/>'
2074 print escape(p.comment).replace('\n', '<br/>\n')
2075 print '<br/>'
2076 print '<br/>'
2077 changed = p.adds + p.removes + p.modifies.keys() + \
2078 p.moves.keys() + p.diradds + p.dirremoves + \
2079 p.replaces.keys()
2080 for i in changed: # TODO: link to the file
2081 print '<code>%s</code><br/>' % i
2082 print '</p></div>'
2083 print '</content></entry>'
2084 print '</feed>'
2086 def do_rss():
2087 print "Content-type: text/xml; charset=utf-8\n"
2088 print '<?xml version="1.0" encoding="utf-8"?>'
2089 print """
2090 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
2091 <channel>
2092 <title>%(reponame)s</title>
2093 <link>%(url)s</link>
2094 <description>%(desc)s</description>
2095 <language>en</language>
2096 """ % {
2097 'reponame': config.reponame,
2098 'url': config.myurl + '/' + config.myreponame,
2099 'desc': escape(config.repodesc),
2102 ps = get_last_patches(20)
2103 for p in ps:
2104 title = time.strftime('%d %b %H:%M', time.localtime(p.date))
2105 title += ' - ' + p.name
2106 pdate = time.strftime("%a, %d %b %Y %H:%M:%S +0000",
2107 time.localtime(p.date))
2108 link = '%s/%s;a=commit;h=%s' % (config.myurl,
2109 config.myreponame, p.hash)
2111 # the author field is tricky because the standard requires it
2112 # has an email address; so we need to check that and lie
2113 # otherwise; there's more info at
2114 # http://feedvalidator.org/docs/error/InvalidContact.html
2115 if "@" in p.author:
2116 author = p.author
2117 else:
2118 author = "%s &lt;unknown@email&gt;" % p.author
2120 print """
2121 <item>
2122 <title>%(title)s</title>
2123 <author>%(author)s</author>
2124 <pubDate>%(pdate)s</pubDate>
2125 <link>%(link)s</link>
2126 <description>%(desc)s</description>
2127 """ % {
2128 'title': escape(title),
2129 'author': author,
2130 'pdate': pdate,
2131 'link': link,
2132 'desc': escape(p.name),
2134 print ' <content:encoded><![CDATA['
2135 print escape(p.name) + '<br/>'
2136 if p.comment:
2137 print '<br/>'
2138 print escape(p.comment).replace('\n', '<br/>\n')
2139 print '<br/>'
2140 print '<br/>'
2141 changed = p.adds + p.removes + p.modifies.keys() + \
2142 p.moves.keys() + p.diradds + p.dirremoves + \
2143 p.replaces.keys()
2144 for i in changed:
2145 print '%s<br/>' % i
2146 print ']]>'
2147 print '</content:encoded></item>'
2149 print '</channel></rss>'
2152 def do_search(s):
2153 print_header()
2154 print_navbar()
2155 ps = get_last_patches(config.searchlimit)
2157 print '<div class="title">Search last %d commits for "%s"</div>' \
2158 % (config.searchlimit, escape(s))
2159 print '<table cellspacing="0">'
2161 alt = True
2162 for p in ps:
2163 match = p.matches(s)
2164 if not match:
2165 continue
2167 if alt:
2168 print '<tr class="dark">'
2169 else:
2170 print '<tr class="light">'
2171 alt = not alt
2173 print """
2174 <td><i>%(age)s</i></td>
2175 <td>%(author)s</td>
2176 <td>
2177 <a class="list" title="%(fullname)s" href="%(myrname)s;a=commit;h=%(hash)s">
2178 <b>%(name)s</b>
2179 </a><br/>
2180 %(match)s
2181 </td>
2182 <td class="link">
2183 <a href="%(myrname)s;a=commit;h=%(hash)s">commit</a> |
2184 <a href="%(myrname)s;a=commitdiff;h=%(hash)s">commitdiff</a>
2185 </td>
2186 """ % {
2187 'age': how_old(p.local_date),
2188 'author': gen_authorlink(p.author, shorten_str(p.shortauthor, 26)),
2189 'myrname': config.myreponame,
2190 'hash': p.hash,
2191 'name': escape(shorten_str(p.name)),
2192 'fullname': escape(p.name),
2193 'match': highlight(s, shorten_str(match)),
2195 print "</tr>"
2197 print '</table>'
2198 print_footer()
2201 def do_die():
2202 print_header()
2203 print "<p><font color=red>Error! Malformed query</font></p>"
2204 print_footer()
2207 def do_listrepos():
2208 import config as all_configs
2209 expand_multi_config(all_configs)
2211 # the header here is special since we don't have a repo
2212 print "Content-type: text/html; charset=utf-8\n"
2213 print '<?xml version="1.0" encoding="utf-8"?>'
2214 print """
2215 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2216 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2217 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2218 <head>
2219 <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
2220 <meta name="robots" content="index, nofollow"/>
2221 <title>darcs - Repositories</title>
2222 <link rel="stylesheet" type="text/css" href="%(css)s"/>
2223 <link rel="shortcut icon" href="%(fav)s"/>
2224 <link rel="icon" href="%(fav)s"/>
2225 </head>
2227 <body>
2228 <div class="page_header">
2229 <a href="http://darcs.net" title="darcs">
2230 <img src="%(logo)s" alt="darcs logo" style="float:right; border-width:0px;"/>
2231 </a>
2232 <a href="%(myname)s">repos</a> / index
2233 </div>
2234 <div class="index_include">
2235 %(summary)s
2236 </div>
2237 <table cellspacing="0">
2238 <tr>
2239 <th>Project</th>
2240 <th>Description</th>
2241 <th></th>
2242 </tr>
2243 """ % {
2244 'myname': config.myname,
2245 'css': config.cssfile,
2246 'fav': config.darcsfav,
2247 'logo': config.darcslogo,
2248 'summary': config.summary
2251 # some python magic
2252 alt = True
2253 for conf in dir(all_configs):
2254 if conf.startswith('__'):
2255 continue
2256 c = all_configs.__getattribute__(conf)
2257 if 'reponame' not in dir(c):
2258 continue
2259 name = escape(c.reponame)
2260 desc = escape(c.repodesc)
2262 if alt: print '<tr class="dark">'
2263 else: print '<tr class="light">'
2264 alt = not alt
2265 print """
2266 <td><a class="list" href="%(myname)s?r=%(name)s;a=summary">%(dname)s</a></td>
2267 <td>%(desc)s</td>
2268 <td class="link"><a href="%(myname)s?r=%(name)s;a=summary">summary</a> |
2269 <a href="%(myname)s?r=%(name)s;a=shortlog">shortlog</a> |
2270 <a href="%(myname)s?r=%(name)s;a=log">log</a> |
2271 <a href="%(myname)s?r=%(name)s;a=tree">tree</a>
2272 </td>
2273 </tr>
2274 """ % {
2275 'myname': config.myname,
2276 'dname': name,
2277 'name': urllib.quote(name),
2278 'desc': shorten_str(desc, 60)
2280 print "</table>"
2281 print_footer(put_rss = 0)
2283 def expand_multi_config(config):
2284 """Expand configuration entries that serve as "template" to others;
2285 this make it easier to have a single directory with all the repos,
2286 because they don't need specific entries in the configuration anymore.
2289 for conf in dir(config):
2290 if conf.startswith('__'):
2291 continue
2292 c = config.__getattribute__(conf)
2293 if 'multidir' not in dir(c):
2294 continue
2296 if not os.path.isdir(c.multidir):
2297 continue
2299 if 'exclude' not in dir(c):
2300 c.exclude = []
2302 entries = []
2303 if 'multidir_deep' in dir(c) and c.multidir_deep:
2304 for (root, dirs, files) in os.walk(c.multidir):
2305 # do not visit hidden directories
2306 dirs[:] = [d for d in dirs \
2307 if not d.startswith('.')]
2308 if '_darcs' in dirs:
2309 p = root[1 + len(c.multidir):]
2310 entries.append(p)
2311 else:
2312 entries = os.listdir(c.multidir)
2314 entries.sort()
2315 for name in entries:
2316 name = name.replace('\\', '/')
2317 if name.startswith('.'):
2318 continue
2319 fulldir = c.multidir + '/' + name
2320 if not os.path.isdir(fulldir + '/_darcs'):
2321 continue
2322 if name in c.exclude:
2323 continue
2325 # set the display name at the beginning, so it can be
2326 # used by the other replaces
2327 if 'displayname' in dir(c):
2328 dname = c.displayname % { 'name': name }
2329 else:
2330 dname = name
2332 rep_dict = { 'name': name, 'dname': dname }
2334 if 'autoexclude' in dir(c) and c.autoexclude:
2335 dpath = fulldir + \
2336 '/_darcs/third_party/darcsweb'
2337 if not os.path.isdir(dpath):
2338 continue
2340 if 'autodesc' in dir(c) and c.autodesc:
2341 dpath = fulldir + \
2342 '/_darcs/third_party/darcsweb/desc'
2343 if os.access(dpath, os.R_OK):
2344 desc = open(dpath).readline().rstrip("\n")
2345 else:
2346 desc = c.repodesc % rep_dict
2347 else:
2348 desc = c.repodesc % rep_dict
2350 if 'autourl' in dir(c) and c.autourl:
2351 dpath = fulldir + \
2352 '/_darcs/third_party/darcsweb/url'
2353 if os.access(dpath, os.R_OK):
2354 url = open(dpath).readline().rstrip("\n")
2355 else:
2356 url = c.repourl % rep_dict
2357 else:
2358 url = c.repourl % rep_dict
2360 if 'autoprojurl' in dir(c) and c.autoprojurl:
2361 dpath = fulldir + \
2362 '/_darcs/third_party/darcsweb/projurl'
2363 if os.access(dpath, os.R_OK):
2364 projurl = open(dpath).readline().rstrip("\n")
2365 elif 'repoprojurl' in dir(c):
2366 projurl = c.repoprojurl % rep_dict
2367 else:
2368 projurl = None
2369 elif 'repoprojurl' in dir(c):
2370 projurl = c.repoprojurl % rep_dict
2371 else:
2372 projurl = None
2374 rdir = fulldir
2375 class tmp_config:
2376 reponame = dname
2377 repodir = rdir
2378 repodesc = desc
2379 repourl = url
2380 repoencoding = c.repoencoding
2381 repoprojurl = projurl
2383 if 'footer' in dir(c):
2384 footer = c.footer
2386 # index by display name to avoid clashes
2387 config.__setattr__(dname, tmp_config)
2389 def fill_config(name = None):
2390 import config as all_configs
2391 expand_multi_config(all_configs)
2393 if name:
2394 # we only care about setting some configurations if a repo was
2395 # specified; otherwise we only set the common configuration
2396 # directives
2397 for conf in dir(all_configs):
2398 if conf.startswith('__'):
2399 continue
2400 c = all_configs.__getattribute__(conf)
2401 if 'reponame' not in dir(c):
2402 continue
2403 if c.reponame == name:
2404 break
2405 else:
2406 # not found
2407 raise Exception, "Repo not found: " + repr(name)
2409 # fill the configuration
2410 base = all_configs.base
2411 if 'myname' not in dir(base):
2412 # SCRIPT_NAME has the full path, we only take the file name
2413 config.myname = os.path.basename(os.environ['SCRIPT_NAME'])
2414 else:
2415 config.myname = base.myname
2417 if 'myurl' not in dir(base) and 'cachedir' not in dir(base):
2418 n = os.environ['SERVER_NAME']
2419 p = os.environ['SERVER_PORT']
2420 s = os.path.dirname(os.environ['SCRIPT_NAME'])
2421 u = os.environ.get('HTTPS', 'off') in ('on', '1')
2422 if not u and p == '80' or u and p == '443':
2423 p = ''
2424 else:
2425 p = ':' + p
2426 config.myurl = 'http%s://%s%s%s' % (u and 's' or '', n, p, s)
2427 else:
2428 config.myurl = base.myurl
2430 config.darcslogo = base.darcslogo
2431 config.darcsfav = base.darcsfav
2432 config.cssfile = base.cssfile
2433 if name:
2434 config.myreponame = config.myname + '?r=' + urllib.quote(name)
2435 config.reponame = c.reponame
2436 config.repodesc = c.repodesc
2437 config.repodir = c.repodir
2438 config.repourl = c.repourl
2440 config.repoprojurl = None
2441 if 'repoprojurl' in dir(c):
2442 config.repoprojurl = c.repoprojurl
2444 # repoencoding must be a tuple
2445 if isinstance(c.repoencoding, str):
2446 config.repoencoding = (c.repoencoding, )
2447 else:
2448 config.repoencoding = c.repoencoding
2450 # optional parameters
2451 if "darcspath" in dir(base):
2452 config.darcspath = base.darcspath + '/'
2453 else:
2454 config.darcspath = ""
2456 if "summary" in dir(base):
2457 config.summary = base.summary
2458 else:
2459 config.summary = """
2460 This is the repository index for a darcsweb site.<br/>
2461 These are all the available repositories.<br/>
2464 if "cachedir" in dir(base):
2465 config.cachedir = base.cachedir
2466 else:
2467 config.cachedir = None
2469 if "searchlimit" in dir(base):
2470 config.searchlimit = base.searchlimit
2471 else:
2472 config.searchlimit = 100
2474 if "logtimes" in dir(base):
2475 config.logtimes = base.logtimes
2476 else:
2477 config.logtimes = None
2479 if "url_links" in dir(base):
2480 config.url_links = base.url_links
2481 else:
2482 config.url_links = ()
2484 if name and "footer" in dir(c):
2485 config.footer = c.footer
2486 elif "footer" in dir(base):
2487 config.footer = base.footer
2488 else:
2489 config.footer = "Crece desde el pueblo el futuro / " \
2490 + "crece desde el pie"
2491 if "author_links" in dir(base):
2492 config.author_links = base.author_links
2493 else:
2494 config.author_links = None
2495 if "disable_annotate" in dir(base):
2496 config.disable_annotate = base.disable_annotate
2497 else:
2498 config.disable_annotate = False
2503 # main
2506 if sys.version_info < (2, 3):
2507 print "Sorry, but Python 2.3 or above is required to run darcsweb."
2508 sys.exit(1)
2510 form = cgi.FieldStorage()
2512 # if they don't specify a repo, print the list and exit
2513 if not form.has_key('r'):
2514 fill_config()
2515 do_listrepos()
2516 log_times(cache_hit = 0, event = 'index')
2517 sys.exit(0)
2519 # get the repo configuration and fill the config class
2520 current_repo = urllib.unquote(form['r'].value)
2521 fill_config(current_repo)
2524 # get the action, or default to summary
2525 if not form.has_key("a"):
2526 action = "summary"
2527 else:
2528 action = filter_act(form["a"].value)
2530 # check if we have the page in the cache
2531 if config.cachedir:
2532 url_request = os.environ['QUERY_STRING']
2533 # create a string representation of the request, ignoring all the
2534 # unused parameters to avoid DoS
2535 params = ['r', 'a', 'f', 'h', 'topi', 'last']
2536 params = [ x for x in form.keys() if x in params ]
2537 url_request = [ (x, form[x].value) for x in params ]
2538 url_request.sort()
2539 cache = Cache(config.cachedir, url_request)
2540 if cache.open():
2541 # we have a hit, dump and run
2542 cache.dump()
2543 cache.close()
2544 log_times(cache_hit = 1, repo = config.reponame)
2545 sys.exit(0)
2546 # if there is a miss, the cache will step over stdout, intercepting
2547 # all "print"s and writing them to the cache file automatically
2550 # see what should we do according to the received action
2551 if action == "summary":
2552 do_summary()
2554 elif action == "commit":
2555 phash = filter_hash(form["h"].value)
2556 do_commit(phash)
2557 elif action == "commitdiff":
2558 phash = filter_hash(form["h"].value)
2559 do_commitdiff(phash)
2560 elif action == "plain_commitdiff":
2561 phash = filter_hash(form["h"].value)
2562 do_plain_commitdiff(phash)
2563 elif action == "darcs_commitdiff":
2564 phash = filter_hash(form["h"].value)
2565 do_darcs_commitdiff(phash)
2566 elif action == "raw_commitdiff":
2567 phash = filter_hash(form["h"].value)
2568 do_raw_commitdiff(phash)
2570 elif action == 'headdiff':
2571 phash = filter_hash(form["h"].value)
2572 do_headdiff(phash)
2573 elif action == "plain_headdiff":
2574 phash = filter_hash(form["h"].value)
2575 do_plain_headdiff(phash)
2576 elif action == "darcs_headdiff":
2577 phash = filter_hash(form["h"].value)
2578 do_darcs_headdiff(phash)
2580 elif action == "filediff":
2581 phash = filter_hash(form["h"].value)
2582 fname = filter_file(form["f"].value)
2583 do_filediff(phash, fname)
2584 elif action == "plain_filediff":
2585 phash = filter_hash(form["h"].value)
2586 fname = filter_file(form["f"].value)
2587 do_plain_filediff(phash, fname)
2588 elif action == "darcs_filediff":
2589 phash = filter_hash(form["h"].value)
2590 fname = filter_file(form["f"].value)
2591 do_darcs_filediff(phash, fname)
2593 elif action == 'headfilediff':
2594 phash = filter_hash(form["h"].value)
2595 fname = filter_file(form["f"].value)
2596 do_file_headdiff(phash, fname)
2597 elif action == "plain_headfilediff":
2598 phash = filter_hash(form["h"].value)
2599 fname = filter_file(form["f"].value)
2600 do_plain_fileheaddiff(phash, fname)
2601 elif action == "darcs_headfilediff":
2602 phash = filter_hash(form["h"].value)
2603 fname = filter_file(form["f"].value)
2604 do_darcs_fileheaddiff(phash, fname)
2606 elif action == "annotate_normal":
2607 fname = filter_file(form["f"].value)
2608 if form.has_key("h"):
2609 phash = filter_hash(form["h"].value)
2610 else:
2611 phash = None
2612 do_annotate(fname, phash, "normal")
2613 elif action == "annotate_plain":
2614 fname = filter_file(form["f"].value)
2615 if form.has_key("h"):
2616 phash = filter_hash(form["h"].value)
2617 else:
2618 phash = None
2619 do_annotate_plain(fname, phash)
2620 elif action == "annotate_zebra":
2621 fname = filter_file(form["f"].value)
2622 if form.has_key("h"):
2623 phash = filter_hash(form["h"].value)
2624 else:
2625 phash = None
2626 do_annotate(fname, phash, "zebra")
2627 elif action == "annotate_shade":
2628 fname = filter_file(form["f"].value)
2629 if form.has_key("h"):
2630 phash = filter_hash(form["h"].value)
2631 else:
2632 phash = None
2633 do_annotate(fname, phash, "shade")
2635 elif action == "shortlog":
2636 if form.has_key("topi"):
2637 topi = int(filter_num(form["topi"].value))
2638 else:
2639 topi = 0
2640 if form.has_key("last"):
2641 last = int(filter_num(form["last"].value))
2642 else:
2643 last = PATCHES_PER_PAGE
2644 do_shortlog(topi=topi,last=last)
2646 elif action == "filehistory":
2647 if form.has_key("topi"):
2648 topi = int(filter_num(form["topi"].value))
2649 else:
2650 topi = 0
2651 fname = filter_file(form["f"].value)
2652 if form.has_key("last"):
2653 last = int(filter_num(form["last"].value))
2654 else:
2655 last = PATCHES_PER_PAGE
2656 do_filehistory(topi, fname, last=last)
2658 elif action == "log":
2659 if form.has_key("topi"):
2660 topi = int(filter_num(form["topi"].value))
2661 else:
2662 topi = 0
2663 if form.has_key("last"):
2664 last = int(filter_num(form["last"].value))
2665 else:
2666 last = PATCHES_PER_PAGE
2667 do_log(topi, last=last)
2669 elif action == 'headblob':
2670 fname = filter_file(form["f"].value)
2671 do_headblob(fname)
2673 elif action == 'plainblob':
2674 fname = filter_file(form["f"].value)
2675 do_plainblob(fname)
2677 elif action == 'tree':
2678 if form.has_key('f'):
2679 fname = filter_file(form["f"].value)
2680 else:
2681 fname = '/'
2682 do_tree(fname)
2684 elif action == 'rss':
2685 do_rss()
2687 elif action == 'atom':
2688 do_atom()
2690 elif action == 'search':
2691 if form.has_key('s'):
2692 s = form["s"].value
2693 else:
2694 s = ''
2695 do_search(s)
2696 if config.cachedir:
2697 cache.cancel()
2699 else:
2700 action = "invalid query"
2701 do_die()
2702 if config.cachedir:
2703 cache.cancel()
2706 if config.cachedir:
2707 cache.close()
2709 log_times(cache_hit = 0, repo = config.reponame)