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>
12 time_begin
= time
.time()
18 import cgitb
; cgitb
.enable()
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'
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
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
47 # list of run_darcs() invocations, for performance measures
51 def exc_handle(t
, v
, tb
):
56 cgitb
.handler((t
, v
, tb
))
57 sys
.excepthook
= exc_handle
64 l
= [c
for c
in s
if c
in string
.digits
]
68 allowed_in_action
= string
.ascii_letters
+ string
.digits
+ '_'
70 l
= [c
for c
in s
if c
in allowed_in_action
]
74 allowed_in_hash
= string
.ascii_letters
+ string
.digits
+ '-.'
76 l
= [c
for c
in s
if c
in allowed_in_hash
]
81 if '..' in s
or '"' in s
:
82 raise Exception, 'FilterFile FAILED'
90 if c
== last
and c
== '/':
98 print ' '.join(params
), '<br/>'
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
107 for i
in s
.split('\n'):
112 if type(s
) == unicode:
113 return s
.encode('utf8', 'replace')
114 for e
in config
.repoencoding
:
116 return s
.decode(e
).encode('utf8', 'replace')
117 except UnicodeDecodeError:
119 raise UnicodeDecodeError, config
.repoencoding
124 s
= s
.replace('"', '"')
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
)
137 s
= time
.strftime("%d %b %Y %H:%M", t
)
139 age
= int(time
.time()) - int(epoch
)
140 if age
> 60*60*24*365*2:
141 s
= str(age
/60/60/24/365)
143 elif age
> 60*60*24*(365/12)*2:
144 s
= str(age
/60/60/24/(365/12))
146 elif age
> 60*60*24*7*2:
147 s
= str(age
/60/60/24/7)
149 elif age
> 60*60*24*2:
150 s
= str(age
/60/60/24)
165 def shorten_str(s
, max = 60):
167 s
= s
[:max - 4] + ' ...'
173 count
= 8 - (pos
% 8)
176 s
= s
.replace('\t', spaces
, 1)
180 def replace_links(s
):
181 """Replace user defined strings with links, as specified in the
182 configuration file."""
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
)
195 def strip_ignore_this(s
):
196 """Strip out darcs' Ignore-this: metadata if present."""
198 return re
.sub(r
'^Ignore-this:[^\n]*\n?','',s
)
201 "Highlights appearences of s in l"
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
) + ')'
209 pat
= re
.compile(s
, re
.I
)
210 repl
= '<span style="color:#e00000">\\1</span>'
211 l
= re
.sub(pat
, repl
, l
)
217 m
= os
.stat(fname
)[stat
.ST_MODE
]
220 if os
.path
.isdir(fname
): s
.append('d')
223 if m
& 0400: s
.append('r')
225 if m
& 0200: s
.append('w')
227 if m
& 0100: s
.append('x')
230 if m
& 0040: s
.append('r')
232 if m
& 0020: s
.append('w')
234 if m
& 0010: s
.append('x')
237 if m
& 0004: s
.append('r')
239 if m
& 0002: s
.append('w')
241 if m
& 0001: s
.append('x')
247 s
= os
.stat(fname
)[stat
.ST_SIZE
]
251 return "%sK" % (s
/ 1024)
253 return "%sM" % (s
/ 1048576)
257 bins
= open(config
.repodir
+ '/_darcs/prefs/binaries').readlines()
258 bins
= [b
[:-1] for b
in bins
if b
and b
[0] != '#']
260 if re
.compile(b
).search(fname
):
265 realf
= filter_file(config
.repodir
+ '/_darcs/pristine/' + fname
)
266 if os
.path
.exists(realf
):
268 realf
= filter_file(config
.repodir
+ '/_darcs/current/' + fname
)
269 if os
.path
.exists(realf
):
271 realf
= filter_file(config
.repodir
+ '/' + fname
)
274 def log_times(cache_hit
, repo
= None, event
= None):
275 if not config
.logtimes
:
278 time_total
= time
.time() - time_begin
279 processing
= time_total
- time_imports
283 event
= event
+ " (hit)"
287 s
+= '\trepo: %s\n' % repo
292 imports: %.3f\n""" % (time_total
, processing
, time_imports
)
296 for params
in darcs_runs
:
297 s
+= '\t\t%s\n' % params
300 lf
= open(config
.logtimes
, 'a')
305 def parse_darcs_time(s
):
306 "Try to convert a darcs' time string into a Python time tuple."
308 return time
.strptime(s
, "%Y%m%d%H%M%S")
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"
316 ns
= ' '.join(parts
[0:4]) + ' ' + parts
[-1]
317 return time
.strptime(ns
, fmt
)
322 # generic html functions
326 print "Content-type: text/html; charset=utf-8"
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">
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>
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"/>
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"/>
364 <a href="%(myname)s">repos</a> /
365 <a href="%(myreponame)s;a=summary">%(reponame)s</a> /
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
,
380 def print_footer(put_rss
= 1):
382 <div class="page_footer">
383 <div class="page_footer_text">%s</div>
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
= ""):
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
}
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
}
412 | <a href="%(myreponame)s;a=annotate_shade;f=%(fname)s;h=%(hash)s">annotate</a>
414 'myreponame': config
.myreponame
,
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
):
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
):
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
)):
435 | <a href="%(myreponame)s;a=headfilediff;h=%(hash)s;f=%(fname)s">headfilediff</a>
436 """ % { "myreponame": config
.myreponame
, 'hash': h
, 'fname': f
}
440 | <a class="link" href="%(myreponame)s;a=filehistory;f=%(fname)s">filehistory</a>
441 """ % { "myreponame": config
.myreponame
, 'fname': f
}
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('_')
452 efaction
= action
[pos
+ 1:]
453 if efaction
in ("commit", "commitdiff", "filediff", "headdiff",
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"
464 params
+= 'f=%s;' % f
468 <a class="link" href="%(myreponame)s;a=%(act)s;%(params)s">unified</a>
469 """ % { "myreponame": config
.myreponame
, "act": efaction
,
474 | <a class="link" href="%(myreponame)s;a=plain_%(act)s;%(params)s">plain</a>
475 """ % { "myreponame": config
.myreponame
, "act": efaction
,
480 | <a class="link" href="%(myreponame)s;a=darcs_%(act)s;%(params)s">darcs</a>
481 """ % { "myreponame": config
.myreponame
, "act": efaction
,
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
):
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
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"):
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>
506 "myreponame": config
.myreponame
,
514 def print_plain_header():
515 print "Content-type: text/plain; charset=utf-8\n"
517 def print_binary_header(fname
= None):
520 (mime
, enc
) = mimetypes
.guess_type(fname
)
524 print "Content-type: %s" % mime
526 print "Content-type: application/octet-stream"
528 print "Content-Disposition:attachment;filename=%s" % fname
531 def gen_authorlink(author
, shortauthor
=None):
532 if not config
.author_links
:
539 return '<a href="' + config
.author_links
% { 'author': author
} + '">%s</a>' % shortauthor
546 def __init__(self
, basedir
, url
):
548 self
.basedir
= basedir
550 self
.fname
= sha
.sha(repr(url
)).hexdigest()
553 self
.real_stdout
= sys
.stdout
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')
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
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
581 pid
= str(os
.getpid())
582 fname
= self
.basedir
+ '/.' + self
.fname
+ '-' + pid
583 self
.file = open(fname
, 'w')
588 # the entry is still valid, hit!
589 self
.file = open(fname
, 'r')
596 self
.real_stdout
.write(l
)
599 # this gets called from print, because we replaced stdout with
602 self
.real_stdout
.write(s
)
607 sys
.stdout
= self
.real_stdout
609 pid
= str(os
.getpid())
610 fname1
= self
.basedir
+ '/.' + self
.fname
+ '-' + pid
611 fname2
= self
.basedir
+ '/' + self
.fname
612 os
.rename(fname1
, fname2
)
616 "Like close() but don't save the entry."
619 sys
.stdout
= self
.real_stdout
621 pid
= str(os
.getpid())
622 fname
= self
.basedir
+ '/.' + self
.fname
+ '-' + pid
628 # darcs repo manipulation
631 def repo_get_owner():
633 fd
= open(config
.repodir
+ '/_darcs/prefs/author')
634 author
= fd
.readlines()[0].strip()
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
)
644 original_8bit_setting
= os
.environ
['DARCS_DONT_ESCAPE_8BIT']
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'])
654 os
.environ
['DARCS_DONT_ESCAPE_8BIT'] = original_8bit_setting
659 "Represents a single patch/record"
663 self
.shortauthor
= ''
668 self
.inverted
= False;
678 s
= "%s\n\tAuthor: %s\n\tDate: %s\n\tHash: %s\n" % \
679 (self
.name
, self
.author
, self
.date
, self
.hash)
683 """Returns a list of lines from the diff -u corresponding with
685 params
= 'diff --quiet -u --match "hash %s"' % self
.hash
686 f
= run_darcs(params
)
689 def matches(self
, s
):
690 "Defines if the patch matches a given string"
691 if s
.lower() in self
.comment
.lower():
693 elif s
.lower() in self
.name
.lower():
695 elif s
.lower() in self
.author
.lower():
701 for l
in (self
.adds
, self
.removes
, self
.modifies
,
702 self
.diradds
, self
.dirremoves
,
703 self
.replaces
.keys(), self
.moves
.keys(),
710 class XmlInputWrapper
:
711 def __init__(self
, fd
):
714 self
._read
= self
.read
716 def read(self
, *args
, **kwargs
):
719 return '<?xml version="1.0" encoding="utf-8"?>\n'
720 s
= self
.fd
.read(*args
, **kwargs
)
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
):
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'
748 if self
.cur_elem
== 'created_as':
751 # now parse the tags normally
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
)
770 time
.strftime("%a, %d %b %Y %H:%M:%S", td
)
772 inverted
= attrs
.get('inverted', None)
773 if inverted
and inverted
== 'True':
777 self
.current
= p
.hash
778 self
.list.append(p
.hash)
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':
797 self
.cur_file
= fixu8(self
.cur_val
.strip())
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':
806 self
.cur_file
= fixu8(self
.cur_val
.strip())
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))
813 src
= fixu8(attrs
.get('from', None))
814 dst
= fixu8(attrs
.get('to', None))
815 p
= self
.db
[self
.current
]
817 elif name
== 'replaced_tokens':
819 self
.cur_file
= fixu8(self
.cur_val
.strip())
821 p
= self
.db
[self
.current
]
822 if not p
.replaces
.has_key(cf
):
824 p
.replaces
[cf
] = int(attrs
.get('num', None))
828 def characters(self
, s
):
829 if not self
.cur_elem
:
833 def endElement(self
, name
):
834 # See the comment in startElement()
835 if name
== 'created_as':
839 if self
.cur_elem
== 'created_as':
841 if name
== 'replaced_tokens':
845 p
= self
.db
[self
.current
]
846 p
.name
= fixu8(self
.cur_val
)
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 }
879 plist
.append(self
.db
[h
])
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
))
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
915 if fname
[0] == '/': fname
= fname
[1:]
916 s
= '-s "%s"' % fname
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
]
926 handler
= get_changes_handler('-s --match "hash %s"' % hash)
927 patch
= handler
.db
[handler
.list[0]]
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):
944 realf
= filter_file(config
.repodir
+ '/_darcs/patches/' + hash)
945 if not os
.path
.isfile(realf
):
947 file = open(realf
, 'rb')
948 if file.read(2) == '\x1f\x8b':
949 # file begins with gzip magic
951 dsrc
= gzip
.open(realf
)
957 def get_darcs_diff(hash, fname
= None):
958 cmd
= 'changes -v --matches "hash %s"' % hash
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
966 cmd
+= ' "%s"' % fname
967 return run_darcs(cmd
)
973 self
.creator_hash
= ""
975 self
.lastchange_hash
= ""
976 self
.lastchange_author
= ""
977 self
.lastchange_name
= ""
978 self
.lastchange_date
= None
979 self
.firstdate
= 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
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"]:
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
)
1046 # added lines inherit the creation from the annotate
1048 phash
= annotate
.lastchange_hash
1049 pauthor
= annotate
.lastchange_author
1050 pdate
= annotate
.lastchange_date
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
1060 text
= text
.lstrip("\n")
1062 line
.text
= fixu8(text
)
1063 line
.phash
= fixu8(phash
)
1064 line
.pauthor
= fixu8(pauthor
)
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
1076 def get_annotate(fname
, hash = None):
1077 if config
.disable_annotate
:
1080 cmd
= 'annotate --xml-output'
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
1088 cmd
+= ' "%s"' % fname
1090 return parse_annotate(run_darcs(cmd
))
1095 # specific html functions
1098 def print_diff(dsrc
):
1102 # remove the trailing newline
1106 if l
.startswith('diff'):
1107 # file lines, they have their own class
1108 print '<div class="diff_info">%s</div>' % escape(l
)
1113 color
= 'style="color:#008800;"'
1115 color
= 'style="color:#cc0000;"'
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
):
1131 if not l
.startswith(" "):
1132 # comments and normal stuff
1133 print '<div class="pre">' + escape(l
) + "</div>"
1141 cl
= 'class="pre" style="color:#008800;"'
1143 cl
= 'class="pre" style="color:#cc0000;"'
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
)
1153 title
= '<a class="title" href="%s;a=filehistory;f=%s">' % \
1154 (config
.myreponame
, fname
)
1155 title
+= 'History for path %s' % escape(fname
)
1158 title
= '<a class="title" href="%s;a=shortlog">shortlog</a>' \
1161 print '<div>%s</div>' % title
1162 print '<table cellspacing="0">'
1165 # put a link to the previous page
1171 print '<a href="%s;a=filehistory;topi=%d;f=%s">...</a>' \
1172 % (config
.myreponame
, ntopi
, fname
)
1174 print '<a href="%s;a=shortlog;topi=%d">...</a>' \
1175 % (config
.myreponame
, ntopi
)
1180 if p
.name
.startswith("TAG "):
1181 print '<tr class="tag">'
1183 print '<tr class="dark">'
1185 print '<tr class="light">'
1189 <td><i>%(age)s</i></td>
1192 <a class="list" title="%(fullname)s" href="%(myrname)s;a=commit;h=%(hash)s">
1197 <a href="%(myrname)s;a=commit;h=%(hash)s">commit</a> |
1198 <a href="%(myrname)s;a=commitdiff;h=%(hash)s">commitdiff</a>
1201 'age': how_old(p
.local_date
),
1202 'author': gen_authorlink(p
.author
, shorten_str(p
.shortauthor
, 26)),
1203 'myrname': config
.myreponame
,
1205 'name': escape(shorten_str(p
.name
)),
1206 'fullname': escape(p
.name
),
1211 # only show if we've not shown them all already
1214 print '<a href="%s;a=filehistory;topi=%d;f=%s">...</a>' \
1215 % (config
.myreponame
, topi
+ last
, fname
)
1217 print '<a href="%s;a=shortlog;topi=%d">...</a>' \
1218 % (config
.myreponame
, topi
+ last
)
1223 def print_log(last
= PATCHES_PER_PAGE
, topi
= 0):
1224 ps
= get_last_patches(last
, topi
)
1227 # put a link to the previous page
1231 print '<p/><a href="%s;a=log;topi=%d"><- Prev</a><p/>' % \
1232 (config
.myreponame
, ntopi
)
1236 comment
= replace_links(escape(p
.comment
))
1237 fmt_comment
= comment
.replace('\n', '<br/>') + '\n'
1238 fmt_comment
+= '<br/><br/>'
1242 <div><a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1243 <span class="age">%(age)s</span>%(desc)s
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/>
1250 <i>%(author)s [%(date)s]</i><br/>
1252 <div class="log_body">
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
),
1262 'desc': escape(p
.name
),
1263 'comment': fmt_comment
1267 # only show if we've not shown them all already
1268 print '<p><a href="%s;a=log;topi=%d">Next -></a></p>' % \
1269 (config
.myreponame
, topi
+ last
)
1272 def print_blob(fname
):
1273 print '<div class="page_path"><b>%s</b></div>' % escape(fname
)
1276 <div class="page_body">
1277 <i>This is a binary file and its contents will not be displayed.</i>
1288 print_blob_simple(fname
)
1292 print_blob_highlighted(fname
)
1294 # pygments couldn't guess a lexer to highlight the code, try
1295 # another method with sampling the file contents.
1297 print_blob_highlighted(fname
, sample_code
=True)
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')
1308 l
= fixu8(escape(l
))
1309 if l
and l
[-1] == '\n':
1315 <a id="l%(c)d" href="#l%(c)d" class="linenr">%(c)4d</a> %(l)s\
1324 def print_blob_highlighted(fname
, sample_code
=False):
1326 import pygments
.lexers
1327 import pygments
.formatters
1329 code
= open(realpath(fname
), 'r').read()
1331 lexer
= pygments
.lexers
.guess_lexer(code
[:200],
1332 encoding
=config
.repoencoding
[0])
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'
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
):
1351 <i>This is a binary file and its contents will not be displayed.</i>
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)
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() ]
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]
1373 for i
in range(0, lenl
):
1375 n
= float(i
* lenn
) / lenl
1376 n
= max - int(round(n
))
1377 shadetable
[hash] = n
1378 elif style
== "zebra":
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
,
1395 if style
== "shade":
1396 linestyle
= 'style="background-color:#ffff%.2x"' % \
1399 elif style
== "zebra":
1401 if l
.phash
!= prevhash
:
1402 if lineclass
== 'dark':
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(" ")]
1420 shortau
= l
.pauthor
[:right
]
1424 desc
= "%12.12s" % shortau
1425 date
= "%-10.10s" % pdate
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
1434 desc
= "%12.12s" % "'"
1435 date
= "%-10.10s" % ""
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>\
1448 'desc': escape(desc
),
1467 owner
= repo_get_owner()
1469 # we should optimize this, it's a pity to go in such a mess for just
1471 ps
= get_last_patches(1)
1473 print '<div class="title"> </div>'
1474 print '<table cellspacing="0">'
1475 print ' <tr><td>description</td><td>%s</td></tr>' % \
1476 escape(config
.repodesc
)
1478 print ' <tr><td>owner</td><td>%s</td></tr>' % escape(owner
)
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
}
1494 def do_commitdiff(phash
):
1496 print_navbar(h
= phash
)
1497 p
= get_patch(phash
)
1500 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1503 'myreponame': config
.myreponame
,
1505 'name': escape(p
.name
),
1512 def do_plain_commitdiff(phash
):
1513 print_plain_header()
1514 dsrc
= get_diff(phash
)
1516 sys
.stdout
.write(fixu8(l
))
1518 def do_darcs_commitdiff(phash
):
1520 print_navbar(h
= phash
)
1521 p
= get_patch(phash
)
1524 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1527 'myreponame': config
.myreponame
,
1529 'name': escape(p
.name
),
1532 dsrc
= get_darcs_diff(phash
)
1533 print_darcs_diff(dsrc
)
1536 def do_raw_commitdiff(phash
):
1537 print_plain_header()
1538 dsrc
= get_raw_diff(phash
)
1540 print "Error opening file!"
1546 def do_headdiff(phash
):
1548 print_navbar(h
= phash
)
1549 p
= get_patch(phash
)
1552 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1553 %(name)s --> to head</a>
1556 'myreponame': config
.myreponame
,
1558 'name': escape(p
.name
),
1561 dsrc
= get_patch_headdiff(phash
)
1565 def do_plain_headdiff(phash
):
1566 print_plain_header()
1567 dsrc
= get_patch_headdiff(phash
)
1569 sys
.stdout
.write(fixu8(l
))
1571 def do_darcs_headdiff(phash
):
1573 print_navbar(h
= phash
)
1574 p
= get_patch(phash
)
1577 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1578 %(name)s --> to head</a>
1581 'myreponame': config
.myreponame
,
1583 'name': escape(p
.name
),
1586 dsrc
= get_darcs_headdiff(phash
)
1587 print_darcs_diff(dsrc
)
1590 def do_raw_headdiff(phash
):
1591 print_plain_header()
1592 dsrc
= get_darcs_headdiff(phash
)
1597 def do_filediff(phash
, fname
):
1599 print_navbar(h
= phash
, f
= fname
)
1600 p
= get_patch(phash
)
1601 dsrc
= get_file_diff(phash
, fname
)
1604 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1606 <div class="page_path"><b>%(fname)s</b></div>
1608 'myreponame': config
.myreponame
,
1610 'name': escape(p
.name
),
1611 'fname': escape(fname
),
1617 def do_plain_filediff(phash
, fname
):
1618 print_plain_header()
1619 dsrc
= get_file_diff(phash
, fname
)
1621 sys
.stdout
.write(fixu8(l
))
1623 def do_darcs_filediff(phash
, fname
):
1625 print_navbar(h
= phash
, f
= fname
)
1626 p
= get_patch(phash
)
1629 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1631 <div class="page_path"><b>%(fname)s</b></div>
1633 'myreponame': config
.myreponame
,
1635 'name': escape(p
.name
),
1636 'fname': escape(fname
),
1639 dsrc
= get_darcs_diff(phash
, fname
)
1640 print_darcs_diff(dsrc
)
1644 def do_file_headdiff(phash
, fname
):
1646 print_navbar(h
= phash
, f
= fname
)
1647 p
= get_patch(phash
)
1648 dsrc
= get_file_headdiff(phash
, fname
)
1651 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1652 %(name)s --> to head</a>
1654 <div class="page_path"><b>%(fname)s</b></div>
1656 'myreponame': config
.myreponame
,
1658 'name': escape(p
.name
),
1659 'fname': escape(fname
),
1665 def do_plain_fileheaddiff(phash
, fname
):
1666 print_plain_header()
1667 dsrc
= get_file_headdiff(phash
, fname
)
1669 sys
.stdout
.write(fixu8(l
))
1671 def do_darcs_fileheaddiff(phash
, fname
):
1673 print_navbar(h
= phash
, f
= fname
)
1674 p
= get_patch(phash
)
1677 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">
1678 %(name)s --> to head</a>
1680 <div class="page_path"><b>%(fname)s</b></div>
1682 'myreponame': config
.myreponame
,
1684 'name': escape(p
.name
),
1685 'fname': escape(fname
),
1688 dsrc
= get_darcs_headdiff(phash
, fname
)
1689 print_darcs_diff(dsrc
)
1692 print_plain_header()
1693 print "Not yet implemented"
1696 def do_commit(phash
):
1698 print_navbar(h
= phash
)
1699 p
= get_patch(phash
)
1703 <a class="title" href="%(myreponame)s;a=commitdiff;h=%(hash)s">%(name)s</a>
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>
1715 'myreponame': config
.myreponame
,
1716 'author': gen_authorlink(p
.author
),
1717 'local_date': p
.local_date_str
,
1720 'name': escape(p
.name
),
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/>'
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
:
1735 print '<div class="list_head">%d file(s) changed:</div>' % n
1737 print '<table cellspacing="0">'
1742 print '<tr class="dark">'
1744 print '<tr class="light">'
1748 if p
.moves
.has_key(f
):
1749 # don't show diffs for moves, they're broken as of
1756 <a class="list" href="%(myreponame)s;a=filediff;h=%(hash)s;f=%(file)s">
1760 'myreponame': config
.myreponame
,
1762 'file': urllib
.quote(f
),
1766 print "<td>%s</td>" % f
1770 print '<td><span style="color:#008000">',
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">',
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>'
1795 print '<td><span style="color:#000080">',
1796 if p
.modifies
[f
].has_key('b'):
1797 # binary modification
1800 print '+%(+)d -%(-)d' % p
.modifies
[f
],
1801 print '</span></td>'
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>
1811 'myreponame': config
.myreponame
,
1813 'file': urllib
.quote(f
)
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('/')
1837 print '<a href="%s;a=tree;f=%s">%s</a> /' % \
1838 (config
.myreponame
, urllib
.quote(sofar
), p
)
1842 <div class="page_body">
1843 <table cellspacing="0">
1846 path
= realpath(dname
) + '/'
1849 files
= os
.listdir(path
)
1852 # list directories first
1859 if os
.path
.isdir(realfile
):
1863 files
= dlist
+ flist
1867 print '<tr class="dark">'
1869 print '<tr class="light">'
1872 fullf
= filter_file(dname
+ '/' + f
)
1873 print '<td style="font-family:monospace">', fperms(realfile
),
1875 print '<td style="font-family:monospace">', fsize(realfile
),
1881 <a class="link" href="%(myrname)s;a=tree;f=%(fullf)s">%(f)s/</a>
1884 <a href="%(myrname)s;a=filehistory;f=%(fullf)s">history</a> |
1885 <a href="%(myrname)s;a=tree;f=%(fullf)s">tree</a>
1888 'myrname': config
.myreponame
,
1890 'fullf': urllib
.quote(fullf
),
1894 <td><a class="list" href="%(myrname)s;a=headblob;f=%(fullf)s">%(f)s</a></td>
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>
1901 'myrname': config
.myreponame
,
1903 'fullf': urllib
.quote(fullf
),
1906 print '</table></div>'
1910 def do_headblob(fname
):
1912 print_navbar(f
= fname
)
1913 filepath
= os
.path
.dirname(fname
)
1916 print '<div><a class="title" href="%s;a=tree">/</a></div>' % \
1919 print '<div class="title"><b>'
1921 # and the linked, with links
1922 parts
= filepath
.split('/')
1928 print '<a href="%s;a=tree;f=%s">%s</a> /' % \
1929 (config
.myreponame
, sofar
, p
)
1937 def do_plainblob(fname
):
1938 f
= open(realpath(fname
), 'r')
1941 print_binary_header(os
.path
.basename(fname
))
1945 print_plain_header()
1947 sys
.stdout
.write(fixu8(l
))
1950 def do_annotate(fname
, phash
, style
):
1952 ann
= get_annotate(fname
, phash
)
1955 <i>The annotate feature has been disabled</i>
1960 print_navbar(f
= fname
, h
= ann
.lastchange_hash
)
1964 <a class="title" href="%(myreponame)s;a=commit;h=%(hash)s">%(name)s</a>
1966 <div class="page_path"><b>
1967 Annotate for file %(fname)s
1970 'myreponame': config
.myreponame
,
1971 'hash': ann
.lastchange_hash
,
1972 'name': escape(ann
.lastchange_name
),
1973 'fname': escape(fname
),
1976 print_annotate(ann
, style
)
1979 def do_annotate_plain(fname
, phash
):
1980 print_plain_header()
1981 ann
= get_annotate(fname
, phash
)
1983 sys
.stdout
.write(l
.text
)
1986 def do_shortlog(topi
, last
=PATCHES_PER_PAGE
):
1989 print_shortlog(topi
= topi
, last
= last
)
1992 def do_filehistory(topi
, f
, last
=PATCHES_PER_PAGE
):
1994 print_navbar(f
= fname
)
1995 print_shortlog(topi
= topi
, fname
= fname
, last
= last
)
1998 def do_log(topi
, last
=PATCHES_PER_PAGE
):
2001 print_log(topi
= topi
, last
= last
)
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
))
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>
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)
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)
2039 addr
, author
= email
.Utils
.parseaddr(p
.author
)
2041 addr
= "unknown_email@example.com"
2047 <title>%(title)s</title>
2049 <name>%(author)s</name>
2050 <email>%(email)s</email>
2052 <updated>%(pdate)s</updated>
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>
2058 'title': escape(title
),
2061 'url': config
.myurl
+ '/' + config
.myreponame
,
2063 'myrname': config
.myreponame
,
2065 'pname': escape(p
.name
),
2067 'desc': escape(p
.name
),
2070 # TODO: allow to get plain text, not HTML?
2071 print escape(p
.name
) + '<br/>'
2074 print escape(p
.comment
).replace('\n', '<br/>\n')
2077 changed
= p
.adds
+ p
.removes
+ p
.modifies
.keys() + \
2078 p
.moves
.keys() + p
.diradds
+ p
.dirremoves
+ \
2080 for i
in changed
: # TODO: link to the file
2081 print '<code>%s</code><br/>' % i
2083 print '</content></entry>'
2087 print "Content-type: text/xml; charset=utf-8\n"
2088 print '<?xml version="1.0" encoding="utf-8"?>'
2090 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
2092 <title>%(reponame)s</title>
2093 <link>%(url)s</link>
2094 <description>%(desc)s</description>
2095 <language>en</language>
2097 'reponame': config
.reponame
,
2098 'url': config
.myurl
+ '/' + config
.myreponame
,
2099 'desc': escape(config
.repodesc
),
2102 ps
= get_last_patches(20)
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
2118 author
= "%s <unknown@email>" % p
.author
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>
2128 'title': escape(title
),
2132 'desc': escape(p
.name
),
2134 print ' <content:encoded><![CDATA['
2135 print escape(p
.name
) + '<br/>'
2138 print escape(p
.comment
).replace('\n', '<br/>\n')
2141 changed
= p
.adds
+ p
.removes
+ p
.modifies
.keys() + \
2142 p
.moves
.keys() + p
.diradds
+ p
.dirremoves
+ \
2147 print '</content:encoded></item>'
2149 print '</channel></rss>'
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">'
2163 match
= p
.matches(s
)
2168 print '<tr class="dark">'
2170 print '<tr class="light">'
2174 <td><i>%(age)s</i></td>
2177 <a class="list" title="%(fullname)s" href="%(myrname)s;a=commit;h=%(hash)s">
2183 <a href="%(myrname)s;a=commit;h=%(hash)s">commit</a> |
2184 <a href="%(myrname)s;a=commitdiff;h=%(hash)s">commitdiff</a>
2187 'age': how_old(p
.local_date
),
2188 'author': gen_authorlink(p
.author
, shorten_str(p
.shortauthor
, 26)),
2189 'myrname': config
.myreponame
,
2191 'name': escape(shorten_str(p
.name
)),
2192 'fullname': escape(p
.name
),
2193 'match': highlight(s
, shorten_str(match
)),
2203 print "<p><font color=red>Error! Malformed query</font></p>"
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"?>'
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">
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"/>
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;"/>
2232 <a href="%(myname)s">repos</a> / index
2234 <div class="index_include">
2237 <table cellspacing="0">
2240 <th>Description</th>
2244 'myname': config
.myname
,
2245 'css': config
.cssfile
,
2246 'fav': config
.darcsfav
,
2247 'logo': config
.darcslogo
,
2248 'summary': config
.summary
2253 for conf
in dir(all_configs
):
2254 if conf
.startswith('__'):
2256 c
= all_configs
.__getattribute
__(conf
)
2257 if 'reponame' not in dir(c
):
2259 name
= escape(c
.reponame
)
2260 desc
= escape(c
.repodesc
)
2262 if alt
: print '<tr class="dark">'
2263 else: print '<tr class="light">'
2266 <td><a class="list" href="%(myname)s?r=%(name)s;a=summary">%(dname)s</a></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>
2275 'myname': config
.myname
,
2277 'name': urllib
.quote(name
),
2278 'desc': shorten_str(desc
, 60)
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('__'):
2292 c
= config
.__getattribute
__(conf
)
2293 if 'multidir' not in dir(c
):
2296 if not os
.path
.isdir(c
.multidir
):
2299 if 'exclude' not in dir(c
):
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
):]
2312 entries
= os
.listdir(c
.multidir
)
2315 for name
in entries
:
2316 name
= name
.replace('\\', '/')
2317 if name
.startswith('.'):
2319 fulldir
= c
.multidir
+ '/' + name
2320 if not os
.path
.isdir(fulldir
+ '/_darcs'):
2322 if name
in c
.exclude
:
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
}
2332 rep_dict
= { 'name': name
, 'dname': dname
}
2334 if 'autoexclude' in dir(c
) and c
.autoexclude
:
2336 '/_darcs/third_party/darcsweb'
2337 if not os
.path
.isdir(dpath
):
2340 if 'autodesc' in dir(c
) and c
.autodesc
:
2342 '/_darcs/third_party/darcsweb/desc'
2343 if os
.access(dpath
, os
.R_OK
):
2344 desc
= open(dpath
).readline().rstrip("\n")
2346 desc
= c
.repodesc
% rep_dict
2348 desc
= c
.repodesc
% rep_dict
2350 if 'autourl' in dir(c
) and c
.autourl
:
2352 '/_darcs/third_party/darcsweb/url'
2353 if os
.access(dpath
, os
.R_OK
):
2354 url
= open(dpath
).readline().rstrip("\n")
2356 url
= c
.repourl
% rep_dict
2358 url
= c
.repourl
% rep_dict
2360 if 'autoprojurl' in dir(c
) and c
.autoprojurl
:
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
2369 elif 'repoprojurl' in dir(c
):
2370 projurl
= c
.repoprojurl
% rep_dict
2380 repoencoding
= c
.repoencoding
2381 repoprojurl
= projurl
2383 if 'footer' in dir(c
):
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
)
2394 # we only care about setting some configurations if a repo was
2395 # specified; otherwise we only set the common configuration
2397 for conf
in dir(all_configs
):
2398 if conf
.startswith('__'):
2400 c
= all_configs
.__getattribute
__(conf
)
2401 if 'reponame' not in dir(c
):
2403 if c
.reponame
== name
:
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'])
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':
2426 config
.myurl
= 'http%s://%s%s%s' % (u
and 's' or '', n
, p
, s
)
2428 config
.myurl
= base
.myurl
2430 config
.darcslogo
= base
.darcslogo
2431 config
.darcsfav
= base
.darcsfav
2432 config
.cssfile
= base
.cssfile
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
, )
2448 config
.repoencoding
= c
.repoencoding
2450 # optional parameters
2451 if "darcspath" in dir(base
):
2452 config
.darcspath
= base
.darcspath
+ '/'
2454 config
.darcspath
= ""
2456 if "summary" in dir(base
):
2457 config
.summary
= base
.summary
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
2467 config
.cachedir
= None
2469 if "searchlimit" in dir(base
):
2470 config
.searchlimit
= base
.searchlimit
2472 config
.searchlimit
= 100
2474 if "logtimes" in dir(base
):
2475 config
.logtimes
= base
.logtimes
2477 config
.logtimes
= None
2479 if "url_links" in dir(base
):
2480 config
.url_links
= base
.url_links
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
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
2494 config
.author_links
= None
2495 if "disable_annotate" in dir(base
):
2496 config
.disable_annotate
= base
.disable_annotate
2498 config
.disable_annotate
= False
2506 if sys
.version_info
< (2, 3):
2507 print "Sorry, but Python 2.3 or above is required to run darcsweb."
2510 form
= cgi
.FieldStorage()
2512 # if they don't specify a repo, print the list and exit
2513 if not form
.has_key('r'):
2516 log_times(cache_hit
= 0, event
= 'index')
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"):
2528 action
= filter_act(form
["a"].value
)
2530 # check if we have the page in the cache
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
]
2539 cache
= Cache(config
.cachedir
, url_request
)
2541 # we have a hit, dump and run
2544 log_times(cache_hit
= 1, repo
= config
.reponame
)
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":
2554 elif action
== "commit":
2555 phash
= filter_hash(form
["h"].value
)
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
)
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
)
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
)
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
)
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
)
2633 do_annotate(fname
, phash
, "shade")
2635 elif action
== "shortlog":
2636 if form
.has_key("topi"):
2637 topi
= int(filter_num(form
["topi"].value
))
2640 if form
.has_key("last"):
2641 last
= int(filter_num(form
["last"].value
))
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
))
2651 fname
= filter_file(form
["f"].value
)
2652 if form
.has_key("last"):
2653 last
= int(filter_num(form
["last"].value
))
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
))
2663 if form
.has_key("last"):
2664 last
= int(filter_num(form
["last"].value
))
2666 last
= PATCHES_PER_PAGE
2667 do_log(topi
, last
=last
)
2669 elif action
== 'headblob':
2670 fname
= filter_file(form
["f"].value
)
2673 elif action
== 'plainblob':
2674 fname
= filter_file(form
["f"].value
)
2677 elif action
== 'tree':
2678 if form
.has_key('f'):
2679 fname
= filter_file(form
["f"].value
)
2684 elif action
== 'rss':
2687 elif action
== 'atom':
2690 elif action
== 'search':
2691 if form
.has_key('s'):
2700 action
= "invalid query"
2709 log_times(cache_hit
= 0, repo
= config
.reponame
)