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('"', '"') 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"><- 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 -></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"> </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 --> 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 --> 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 --> 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 --> 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 <unknown@email>" % 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)