Mon Dec 19 14:42:02 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  tagged 0.13
Mon Dec 19 14:30:50 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Shorten the author string in shortlog.
Tue Dec 13 11:12:44 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Small changes to do_atom(), make it validate.
Thu Dec  1 19:07:08 ART 2005  stephane@sources.org
  * ATOM support for syndication
Tue Dec 13 10:28:49 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Use darcsweb.cgi's modification time to invalidate cache entries.
  
  Besides the repository inventory modification time, we also want to check the
  script's modification time, so if we make changes to darcsweb (updating it,
  for instance), there's no possibility of serving stale pages if the user
  forgets to clean the cache. You still need to clean it for configuration
  changes.
Wed Nov 30 08:36:26 ART 2005  Kirill Smelkov <kirr@mns.spb.ru>
  * fixu8: honour config.repoencoding when decoding characters like [_\e3]
Sat Nov 12 20:59:16 ART 2005  Alexandre Rossi <niol@sousmonlit.dyndns.org>
  * another basic validation issue
  The issue was that there was an empty line before the XML declaration, which
  is not valid.
Thu Nov 17 11:41:33 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Make how_old() return a fixed date when caching is enabled.
  
  how_old() becomes a problem when we have a cache, because the relative dates
  will get stalled in the cache.
  
  This is makes a workaround by making how_old() return a string containing the
  fixed date, which isn't quite nice but it will do the trick until a better
  solution comes up.
Thu Nov 17 11:40:27 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Don't close the file in a cache miss.
Thu Nov 17 00:27:57 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Call cache.cancel() only if we have a cache.
Thu Nov 17 00:10:28 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Implement a simple cache.
  
  This patch implements a very simple but effective cache, so darcsweb can avoid
  regenerating everything all the time.
  
  Based on an idea from Alexandre Rossi.
Thu Nov 10 12:22:39 ART 2005  Leandro Lucarella <luca@llucax.hn.org>
  * Use a more human-friendly format por list configuration variables.
Wed Nov  9 21:34:34 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Highlight tags in the shortlog/summary.
Wed Nov  9 20:59:47 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Add exclusion lists to multidir configuration.
Tue Nov  8 22:10:31 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  tagged 0.12
diff -rN -u old-darcsweb/config.py.sample new-darcsweb/config.py.sample
--- old-darcsweb/config.py.sample	2005-12-19 20:33:22.000000000 -0300
+++ new-darcsweb/config.py.sample	2005-12-19 20:33:22.000000000 -0300
@@ -29,6 +29,17 @@
 	# alternative footer here; it's optional, of course
 	#footer = "I don't like shoes"
 
+	# It is possible to have a cache where darcsweb will store the pages
+	# it generates; entries are automatically updated when the repository
+	# changes. This will speed things up significatively, specially for
+	# popular sites.
+	# It's recommended that you clean the directory with some regularity,
+	# to avoid having too many unused files. A simple rm will do just
+	# fine.
+	# If you leave the entry commented, no cache will be ever used;
+	# otherwise the directory is assumed to exist and be writeable.
+	#cachedir = '/tmp/darcsweb-cache'
+
 
 #
 # From now on, every class is a repo configuration, with the same format
@@ -89,4 +100,8 @@
 	repourl = 'http://auriga.wearlab.de/~alb/repos/%(name)s'
 	repoencoding = 'latin1'
 
+	# if you want to exclude some directories, add them to this list (note
+	# they're relative to multidir, not absolute)
+	#exclude = 'dir1', 'dir2'
+
 
diff -rN -u old-darcsweb/darcsweb.cgi new-darcsweb/darcsweb.cgi
--- old-darcsweb/darcsweb.cgi	2005-12-19 20:33:22.000000000 -0300
+++ new-darcsweb/darcsweb.cgi	2005-12-19 20:33:22.000000000 -0300
@@ -13,12 +13,15 @@
 import string
 import time
 import stat
+import sha
 import cgi
 import cgitb; cgitb.enable()
 import urllib
 import xml.sax
 from xml.sax.saxutils import escape as xml_escape
+import email.Utils
 
+iso_datetime = '%Y-%m-%dT%H:%M:%SZ'
 
 # empty configuration class, we will fill it in later depending on the repo
 class config:
@@ -95,7 +98,7 @@
 
 			# finally, replace s with our new improved string, and
 			# repeat the ugly procedure
-			char = char.decode('raw_unicode_escape')
+			char = char.decode(config.repoencoding)
 			mn = '[_\\' + middle + '_]'
 			s = s.replace(mn, char, 1)
 		openpos = s.find('[_', openpos + 1)
@@ -112,6 +115,13 @@
 	return s
 
 def how_old(epoch):
+	if config.cachedir:
+		# when we have a cache, the how_old() becomes a problem since
+		# the cached entries will have old data; so in this case just
+		# return a nice string
+		t = time.localtime(epoch)
+		s = time.strftime("%d %b %H:%M", t)
+		return s
 	age = int(time.time()) - int(epoch)
 	if age > 60*60*24*365*2:
 		s = str(age/60/60/24/365)
@@ -205,7 +215,7 @@
 #
 
 def print_header():
-	print "Content-type: text/html; charset=utf-8\n"
+	print "Content-type: text/html; charset=utf-8"
 	print """
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
@@ -224,6 +234,8 @@
 <link rel="stylesheet" type="text/css" href="%(css)s"/>
 <link rel="alternate" title="%(reponame)s" href="%(url)s;a=rss"
 		type="application/rss+xml"/>
+<link rel="alternate" title="%(reponame)s" href="%(url)s;a=atom"
+		type='application/atom+xml'/>
 <link rel="shortcut icon" href="%(fav)s"/>
 <link rel="icon" href="%(fav)s"/>
 </head>
@@ -384,6 +396,91 @@
 	print "Content-type: text/plain; charset=utf-8\n"
 
 
+
+#
+# basic caching
+#
+
+class Cache:
+	def __init__(self, basedir, url):
+		self.basedir = basedir
+		self.url = url
+		self.fname = sha.sha(url).hexdigest()
+		self.file = None
+		self.mode = None
+		self.real_stdout = sys.stdout
+
+	def open(self):
+		"Returns 1 on hit, 0 on miss"
+		fname = self.basedir + '/' + self.fname
+
+		if not os.access(fname, os.R_OK):
+			# the file doesn't exist, direct miss
+			pid = str(os.getpid())
+			fname = self.basedir + '/.' + self.fname + '-' + pid
+			self.file = open(fname, 'w')
+			self.mode = 'w'
+
+			# step over stdout so when "print" tries to write
+			# output, we get it first
+			sys.stdout = self
+			return 0
+
+		inv = config.repodir + '/_darcs/inventory'
+		cache_lastmod = os.stat(fname).st_mtime
+		repo_lastmod = os.stat(inv).st_mtime
+		dw_lastmod = os.stat(sys.argv[0]).st_mtime
+
+		if repo_lastmod > cache_lastmod or dw_lastmod > cache_lastmod:
+			# the entry is too old, remove it and return a miss
+			os.unlink(fname)
+
+			pid = str(os.getpid())
+			fname = self.basedir + '/.' + self.fname + '-' + pid
+			self.file = open(fname, 'w')
+			self.mode = 'w'
+			sys.stdout = self
+			return 0
+
+		# the entry is still valid, hit!
+		self.file = open(fname, 'r')
+		self.mode = 'r'
+		return 1
+
+
+	def dump(self):
+		for l in self.file:
+			self.real_stdout.write(l)
+
+	def write(self, s):
+		# this gets called from print, because we replaced stdout with
+		# ourselves
+		self.file.write(s)
+		self.real_stdout.write(s)
+
+	def close(self):
+		if self.file:
+			self.file.close()
+		sys.stdout = self.real_stdout
+		if self.mode == 'w':
+			pid = str(os.getpid())
+			fname1 = self.basedir + '/.' + self.fname + '-' + pid
+			fname2 = self.basedir + '/' + self.fname
+			os.rename(fname1, fname2)
+			self.mode = 'c'
+
+	def cancel(self):
+		"Like close() but don't save the entry."
+		if self.file:
+			self.file.close()
+		sys.stdout = self.real_stdout
+		if self.mode == 'w':
+			pid = str(os.getpid())
+			fname = self.basedir + '/.' + self.fname + '-' + pid
+			os.unlink(fname)
+			self.mode = 'c'
+
+
 #
 # darcs repo manipulation
 #
@@ -870,7 +967,9 @@
 
 	alt = False
 	for p in ps:
-		if alt:
+		if p.name.startswith("TAG "):
+			print '<tr class="tag">'
+		elif alt:
 			print '<tr class="dark">'
 		else:
 			print '<tr class="light">'
@@ -890,7 +989,7 @@
   </td>
 		""" % {
 			'age': how_old(p.local_date),
-			'author': p.shortauthor,
+			'author': shorten_str(p.shortauthor, 26),
 			'myrname': config.myreponame,
 			'hash': p.hash,
 			'name': shorten_str(p.name),
@@ -1620,6 +1719,86 @@
 	print_log(topi = topi)
 	print_footer()
 
+def do_atom():
+	print "Content-type: application/atom+xml; charset=utf-8\n"
+	print '<?xml version="1.0" encoding="utf-8"?>'
+	inv = config.repodir + '/_darcs/inventory'
+	repo_lastmod = os.stat(inv).st_mtime
+	str_lastmod = time.strftime(iso_datetime,
+			time.localtime(repo_lastmod))
+
+	print """
+<feed xmlns="http://www.w3.org/2005/Atom">
+  <title>%(reponame)s darcs repository</title>
+  <link rel="alternate" type="text/html" href="%(url)s"/>
+  <link rel="self" type="application/atom+xml" href="%(url)s;a=atom"/>
+  <id>%(url)s</id> <!-- TODO: find a better <id>, see RFC 4151 -->
+  <author><name>darcs repository (several authors)</name></author>
+  <generator>darcsweb.cgi</generator>
+  <updated>%(lastmod)s</updated> 
+  <subtitle>%(desc)s</subtitle>
+  	""" % {
+		'reponame': config.reponame,
+		'url': config.myurl + '/' + config.myreponame,
+		'desc': config.repodesc,
+		'lastmod': str_lastmod,
+	}
+
+	ps = get_last_patches(20)
+	for p in ps:
+		title = time.strftime('%d %b %H:%M', time.localtime(p.date))
+		title += ' - ' + p.name
+		pdate = time.strftime(iso_datetime,
+				time.localtime(p.date))
+		link = '%s/%s;a=commit;h=%s' % (config.myurl,
+				config.myreponame, p.hash)
+
+		addr, author = email.Utils.parseaddr(p.author)
+		if not addr:
+			addr = "unknown_email@example.com"
+		if not author:
+			author = addr
+
+		print """
+  <entry>
+    <title>%(title)s</title>
+    <author>
+      <name>%(author)s</name>
+      <email>%(email)s</email>
+    </author>
+    <updated>%(pdate)s</updated>
+    <id>%(link)s</id>
+    <link rel="alternate" href="%(link)s"/>
+    <summary>%(desc)s</summary>
+    <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><p>
+	   	""" % {
+			'title': escape(title),
+			'author': author,
+			'email': addr,
+			'url': config.myurl + '/' + config.myreponame,
+			'pdate': pdate,
+			'myrname': config.myreponame,
+			'hash': p.hash,
+			'pname': escape(p.name),
+			'link': link,
+			'desc': escape(p.name),
+		}
+
+                # TODO: allow to get plain text, not HTML?
+		print escape(p.name) + '<br/>'
+		if p.comment:
+			print '<br/>'
+			print escape(p.comment).replace('\n', '<br/>\n')
+			print '<br/>'
+		print '<br/>'
+		changed = p.adds + p.removes + p.modifies.keys() + \
+				p.moves.keys() + p.diradds + p.dirremoves + \
+				p.replaces.keys()
+		for i in changed: # TODO: link to the file 
+			print '<code>%s</code><br/>' % i
+		print '</p></div>'
+		print '</content></entry>'
+	print '</feed>'
 
 def do_rss():
 	print "Content-type: text/xml; charset=utf-8\n"
@@ -1691,7 +1870,6 @@
 	print_header()
 	print "<p><font color=red>Error! Malformed query</font></p>"
 	print_footer()
-	sys.exit(1)
 
 
 def do_listrepos():
@@ -1783,6 +1961,8 @@
 		if 'multidir' not in dir(c):
 			continue
 
+		if 'exclude' not in dir(c):
+			c.exclude = []
 		entries = os.listdir(c.multidir)
 		entries.sort()
 		for name in entries:
@@ -1791,6 +1971,8 @@
 			fulldir = c.multidir + '/' + name
 			if not os.path.isdir(fulldir + '/_darcs'):
 				continue
+			if name in c.exclude:
+				continue
 
 			rdir = fulldir
 			desc = c.repodesc % { 'name': name }
@@ -1854,6 +2036,11 @@
 These are all the available repositories.<br/>
 		"""
 
+	if "cachedir" in dir(base):
+		config.cachedir = base.cachedir
+	else:
+		config.cachedir = None
+
 	if name and "footer" in dir(c):
 		config.footer = c.footer
 	elif "footer" in dir(base):
@@ -1863,6 +2050,7 @@
 				+ "crece desde el pie"
 
 
+
 #
 # main
 #
@@ -1886,6 +2074,18 @@
 else:
 	action = filter_act(form["a"].value)
 
+# check if we have the page in the cache
+if config.cachedir:
+	url_request = os.environ['QUERY_STRING']
+	cache = Cache(config.cachedir, url_request)
+	if cache.open():
+		# we have a hit, dump and run
+		cache.dump()
+		cache.close()
+		sys.exit(0)
+	# if there is a miss, the cache will step over stdout, intercepting
+	# all "print"s and writing them to the cache file automatically
+
 
 # see what should we do according to the received action
 if action == "summary":
@@ -2012,9 +2212,16 @@
 elif action == 'rss':
 	do_rss()
 
+elif action == 'atom':
+	do_atom()
+
 else:
 	action = "invalid query"
 	do_die()
+	if config.cachedir:
+		cache.cancel()
 
 
+if config.cachedir:
+	cache.close()
 
diff -rN -u old-darcsweb/style.css new-darcsweb/style.css
--- old-darcsweb/style.css	2005-12-19 20:33:22.000000000 -0300
+++ new-darcsweb/style.css	2005-12-19 20:33:22.000000000 -0300
@@ -158,6 +158,14 @@
 	background-color:#edece6;
 }
 
+.tag {
+	background-color:#f0f0ff;
+}
+
+.tag:hover {
+	background-color:#e0e0ff;
+}
+
 td {
 	padding:2px 5px;
 	font-size:12px;


