Mon Jan  9 19:25:04 ART 2006  Alberto Bertogli <albertogli@telpin.com.ar>
  tagged 0.14
Mon Jan  9 02:58:01 ART 2006  Alberto Bertogli <albertogli@telpin.com.ar>
  * Fix cache behaviour on uncaught exceptions.
  This patch make darcsweb cancel the cache when there is an uncaught exception,
  avoiding leaving dot-files around the cache directory.
  
  Also minimize DoS by not taking into account unused form parameters, and make
  the hash independant of the position. This doesn't eliminate all the
  opportunities for DoS, but reduces them significatively.
Mon Jan  9 02:27:44 ART 2006  Alberto Bertogli <albertogli@telpin.com.ar>
  * Replace the darcs logo with a smaller one.
  Now that we have a search box, the logo becomes really big and distractive.
  Replace it with a smaller version, only with the ball.
Mon Jan  9 02:24:26 ART 2006  Alberto Bertogli <albertogli@telpin.com.ar>
  * Implement a search box.
  This implements a small search box that appears on the right top besides the
  logo (which will be changed to a small version in a following patch).
Sat Jan  7 22:45:32 ART 2006  Alberto Bertogli <albertogli@telpin.com.ar>
  * Add the BOLA license.
  darcsweb has been too long without an explicit license, it's time to add one.
  Enjoy!
Sat Dec 31 01:16:51 ART 2005  Leandro Lucarella <luca@llucax.hn.org>
  * Silly changes to avoid HTTP redirects.
Fri Dec 30 20:44:19 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Add autodesc and autoexclude options.
  This patch implements two optional options for multidir repos: autodesc and
  autoexclude.
  
  If autodesc is enabled, the description will be taken from the file named
  "_darcs/third_party/darcsweb/desc", inside the repository. If it doesn't
  exist, the default will be used.
  
  If autoexclude is enabled, only repositories with a directory named
  "_darcs/third_party/darcsweb/" will be listed.
  
  The path "_darcs/third_party/darcsweb/" for darcsweb-exclusive preferences was
  chosen following a suggestion by David Roundy in the mail with Subject
  "[darcs-users] darcsweb 0.12 and some questions", Message-ID
  20051109123238.GB17288@abridgegame.org, date "9 Nov 2005 07:32:44".
Fri Dec 30 18:08:43 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Escape filenames, just in case.
  In the same spirit as the last patch, add escape() around prints of file
  names. It's highly improbable, but it could happen for weird cases and it
  seems worth the effort.
Tue Dec 27 00:19:14 ART 2005  Michael Allan <mike@zelea.com>
  * Escape patch names.
  When patch names used in HTML, escape characters like '<'.
Sat Dec 24 12:06:57 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Move the mimetypes import to the function.
  While it's a nice practise to put all imports above, in darcsweb's case it
  also increases startup latency.
  
  Because the mimetypes module is only used inside print_binary_header(), move
  the import inside the function.
Wed Dec 21 19:24:47 ART 2005  gaetan.lehmann@jouy.inra.fr
  * enhance mime type for binary files
Wed Dec 21 12:00:33 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Identify binary files in plainblob.
  If we want to do a plainblob of a binary file, it's better to offer a proper
  download than to display it as text/plain.
  
  This patch identifies binary files (looking at darcs' information) and offers
  them for download with the proper name.
Wed Dec 21 11:29:46 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Indentation and comment changes to multidir_deep.
Tue Dec 20 13:30:22 ART 2005  nils@ndecker.de
  * Add deep recursion option to multidir configuration
Wed Dec 21 10:47:50 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  * Indentation changes to the uncompression patch.
Tue Dec 20 08:36:01 ART 2005  nils@ndecker.de
  * show raw for noncompressed patches
Mon Dec 19 14:42:02 ART 2005  Alberto Bertogli <albertogli@telpin.com.ar>
  tagged 0.13
diff -rN -u old-darcsweb/config.py.sample new-darcsweb/config.py.sample
--- old-darcsweb/config.py.sample	2006-01-09 19:26:06.000000000 -0300
+++ new-darcsweb/config.py.sample	2006-01-09 19:26:06.000000000 -0300
@@ -40,6 +40,12 @@
 	# otherwise the directory is assumed to exist and be writeable.
 	#cachedir = '/tmp/darcsweb-cache'
 
+	# By default, darcsweb's search looks in the last 100 commits; you can
+	# change that number by specifying it here.
+	# Note that search are not cached, so if you have tons of commits and
+	# set the limit to a very high number, they will take time.
+	#searchlimit = 100
+
 
 #
 # From now on, every class is a repo configuration, with the same format
@@ -62,7 +68,7 @@
 	repodir = '/usr/src/repo1'
 
 	# an url so people know where to do "darcs get" from
-	repourl = 'http://auriga.wearlab.de/~alb/repos/repo1'
+	repourl = 'http://auriga.wearlab.de/~alb/repos/repo1/'
 
 	# the encoding used in the repo
 	# NOTE: if you use utf8, you _must_ write 'utf8' (and not the variants
@@ -82,7 +88,7 @@
 	reponame = 'repo2'
 	repodesc = 'Second example repository'
 	repodir = '/usr/src/repo2'
-	repourl = 'http://auriga.wearlab.de/~alb/repos/repo2'
+	repourl = 'http://auriga.wearlab.de/~alb/repos/repo2/'
 	repoencoding = 'latin1'
 
 
@@ -93,15 +99,32 @@
 # The name is taken from the directory, and inside the variables the string
 # "%(name)s" gets expanded to the it.
 #
+# If you set multidir_deep to True (note the capitalization) then all
+# subdirectories are searched for darcs repositories. Subdirectories starting
+# with a dot (.) are not searched. This may be slow, if huge directory trees
+# must be searched. It's unnecesary unless you have a multidir with several
+# nested repositories. It defaults to False, and it's optional.
+#
 
 class multi1:
 	multidir = '/usr/local/src'
+	#multidir_deep = False
 	repodesc = 'Repository for %(name)s'
-	repourl = 'http://auriga.wearlab.de/~alb/repos/%(name)s'
+	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'
 
+	# if you want the descriptions to be picked up automatically from the
+	# file named "_darcs/third_party/darcsweb/desc" (one line only), set
+	# this to True. It defaults to False
+	#autodesc = True
+
+	# if you want to exclude all the repositories which do NOT have a
+	# directory named "_darcs/third_party/darcsweb/" inside, set this to
+	# True. It defaults to False.
+	#autoexclude = True
+
 
Files old-darcsweb/darcs.png and new-darcsweb/darcs.png differ
diff -rN -u old-darcsweb/darcsweb.cgi new-darcsweb/darcsweb.cgi
--- old-darcsweb/darcsweb.cgi	2006-01-09 19:26:06.000000000 -0300
+++ new-darcsweb/darcsweb.cgi	2006-01-09 19:26:06.000000000 -0300
@@ -27,6 +27,14 @@
 class config:
 	pass
 
+# exception handling
+def exc_handle(t, v, tb):
+	try:
+		cache.cancel()
+	except:
+		pass
+	cgitb.handler((t, v, tb))
+sys.excepthook = exc_handle
 
 #
 # utility functions
@@ -163,6 +171,22 @@
 		pos = s.find("\t")
 	return s
 
+def highlight(s, l):
+	"Highlights appearences of s in l"
+	import re
+	# build the regexp by leaving "(s)", replacing '(' and ') first
+	s = s.replace('\\', '\\\\')
+	s = s.replace('(', '\\(')
+	s = s.replace(')', '\\)')
+	s = '(' + escape(s) + ')'
+	try:
+		pat = re.compile(s, re.I)
+		repl = '<span style="color:#e00000">\\1</span>'
+		l = re.sub(pat, repl, l)
+	except:
+		pass
+	return l
+
 def fperms(fname):
 	m = os.stat(fname)[stat.ST_MODE]
 	m = m & 0777
@@ -242,21 +266,31 @@
 
 <body>
 <div class="page_header">
-<a href="http://darcs.net" title="darcs">
-<img src="%(logo)s" alt="darcs logo" style="float:right; border-width:0px;"/>
-</a>
+  <div class="search_box">
+    <form action="%(myname)s" method="get"><div>
+      <input type="hidden" name="r" value="%(reponame)s"/>
+      <input type="hidden" name="a" value="search"/>
+      <input type="text" name="s" size="20" class="search_text"/>
+      <input type="submit" value="search" class="search_button"/>
+      <a href="http://darcs.net" title="darcs">
+        <img src="%(logo)s" alt="darcs logo" class="logo"/>
+      </a>
+    </div></form>
+  </div>
+  <a href="%(myname)s">repos</a> /
+  <a href="%(myreponame)s;a=summary">%(reponame)s</a> /
+  %(action)s
+</div>
 	""" % {
 		'reponame': config.reponame,
 		'css': config.cssfile,
 		'url': config.myurl + '/' + config.myreponame,
 		'fav': config.darcsfav,
 		'logo': config.darcslogo,
+		'myname': config.myname,
+		'myreponame': config.myreponame,
+		'action': action
 	}
-	print '<a href="%s">repos</a> /' % config.myname
-	print '<a href="%s;a=summary">%s</a>' % (config.myreponame,
-			config.reponame),
-	print '/ ' + action
-	print "</div>"
 
 
 def print_footer(put_rss = 1):
@@ -395,6 +429,19 @@
 def print_plain_header():
 	print "Content-type: text/plain; charset=utf-8\n"
 
+def print_binary_header(fname = None):
+	import mimetypes
+	if fname :
+		(mime, enc) = mimetypes.guess_type(fname)
+	else :
+		mime = None
+	if mime :
+		print "Content-type: %s" % mime
+	else :
+		print "Content-type: application/octet-stream"
+	if fname:
+		print "Content-Disposition:attachment;filename=%s" % fname
+	print
 
 
 #
@@ -405,7 +452,7 @@
 	def __init__(self, basedir, url):
 		self.basedir = basedir
 		self.url = url
-		self.fname = sha.sha(url).hexdigest()
+		self.fname = sha.sha(repr(url)).hexdigest()
 		self.file = None
 		self.mode = None
 		self.real_stdout = sys.stdout
@@ -533,6 +580,27 @@
 		f = run_darcs(params)
 		return f.readlines()
 
+	def matches(self, s):
+		"Defines if the patch matches a given string"
+		if s.lower() in self.comment.lower():
+			return self.comment
+		elif s.lower() in self.name.lower():
+			return self.name
+		elif s.lower() in self.author.lower():
+			return self.author
+		elif s == self.hash:
+			return self.hash
+
+		s = s.lower()
+		for l in (self.adds, self.removes, self.modifies,
+				self.diradds, self.dirremoves,
+				self.replaces.keys(), self.moves.keys(),
+				self.moves.keys() ):
+			for i in l:
+				if s in i.lower():
+					return i
+		return ''
+
 
 # patch parsing, we get them through "darcs changes --xml-output"
 class BuildPatchList(xml.sax.handler.ContentHandler):
@@ -722,7 +790,7 @@
 
 	if fname:
 		if fname[0] == '/': fname = fname[1:]
-		s = "-s " + fname
+		s = '-s "%s"' % fname
 	else:
 		s = "-s --last=%d" % toget
 
@@ -753,7 +821,14 @@
 	realf = filter_file(config.repodir + '/_darcs/patches/' + hash)
 	if not os.path.isfile(realf):
 		return None
-	dsrc = gzip.open(realf)
+	file = open(realf, 'rb')
+	if file.read(2) == '\x1f\x8b':
+		# file begins with gzip magic
+		file.close()
+		dsrc = gzip.open(realf)
+	else:
+		file.seek(0)
+		dsrc = file
 	return dsrc
 
 def get_darcs_diff(hash, fname = None):
@@ -880,7 +955,7 @@
 	cmd = 'annotate --xml-output'
 	if hash:
 		cmd += ' --match="hash %s"' % hash
-	cmd += ' %s' % fname
+	cmd += ' "%s"' % fname
 	out = run_darcs(cmd)
 	return parse_annotate(out)
 
@@ -942,7 +1017,7 @@
 	if fname:
 		title = '<a class="title" href="%s;a=filehistory;f=%s">' % \
 				(config.myreponame, fname)
-		title += 'History for path %s' % fname
+		title += 'History for path %s' % escape(fname)
 		title += '</a>'
 	else:
 		title = '<a class="title" href="%s;a=shortlog">shortlog</a>' \
@@ -992,7 +1067,7 @@
 			'author': shorten_str(p.shortauthor, 26),
 			'myrname': config.myreponame,
 			'hash': p.hash,
-			'name': shorten_str(p.name),
+			'name': escape(shorten_str(p.name)),
 			'fullname': escape(p.name),
 		}
 		print "</tr>"
@@ -1062,7 +1137,7 @@
 
 
 def print_blob(fname):
-	print '<div class="page_path"><b>%s</b></div>' % fname
+	print '<div class="page_path"><b>%s</b></div>' % escape(fname)
 	print '<div class="page_body">'
 	if isbinary(fname):
 		print """
@@ -1243,7 +1318,7 @@
 	""" % {
 		'myreponame': config.myreponame,
 		'hash': p.hash,
-		'name': p.name,
+		'name': escape(p.name),
 	}
 
 	dsrc = p.getdiff()
@@ -1267,7 +1342,7 @@
 	""" % {
 		'myreponame': config.myreponame,
 		'hash': p.hash,
-		'name': p.name,
+		'name': escape(p.name),
 	}
 
 	dsrc = get_darcs_diff(phash)
@@ -1296,7 +1371,7 @@
 	""" % {
 		'myreponame': config.myreponame,
 		'hash': p.hash,
-		'name': p.name,
+		'name': escape(p.name),
 	}
 
 	dsrc = get_patch_headdiff(phash)
@@ -1321,7 +1396,7 @@
 	""" % {
 		'myreponame': config.myreponame,
 		'hash': p.hash,
-		'name': p.name,
+		'name': escape(p.name),
 	}
 
 	dsrc = get_darcs_headdiff(phash)
@@ -1348,8 +1423,8 @@
 	""" % {
 		'myreponame': config.myreponame,
 		'hash': p.hash,
-		'name': p.name,
-		'fname': fname,
+		'name': escape(p.name),
+		'fname': escape(fname),
 	}
 
 	print_diff(dsrc)
@@ -1373,8 +1448,8 @@
 	""" % {
 		'myreponame': config.myreponame,
 		'hash': p.hash,
-		'name': p.name,
-		'fname': fname,
+		'name': escape(p.name),
+		'fname': escape(fname),
 	}
 
 	dsrc = get_darcs_diff(phash, fname)
@@ -1396,8 +1471,8 @@
 	""" % {
 		'myreponame': config.myreponame,
 		'hash': p.hash,
-		'name': p.name,
-		'fname': fname,
+		'name': escape(p.name),
+		'fname': escape(fname),
 	}
 
 	print_diff(dsrc)
@@ -1422,8 +1497,8 @@
 	""" % {
 		'myreponame': config.myreponame,
 		'hash': p.hash,
-		'name': p.name,
-		'fname': fname,
+		'name': escape(p.name),
+		'fname': escape(fname),
 	}
 
 	dsrc = get_darcs_headdiff(phash, fname)
@@ -1458,12 +1533,13 @@
 		'local_date': p.local_date_str,
 		'date': p.date_str,
 		'hash': p.hash,
-		'name': p.name,
+		'name': escape(p.name),
 	}
 	if p.comment:
-		c = p.comment.replace('\n', '<br/>\n')
+		comment = escape(p.comment)
+		c = comment.replace('\n', '<br/>\n')
 		print '<div class="page_body">'
-		print p.name, '<br/><br/>'
+		print escape(p.name), '<br/><br/>'
 		print c
 		print '</div>'
 
@@ -1574,7 +1650,7 @@
 		if not p: continue
 		sofar += '/' + p
 		print '<a href="%s;a=tree;f=%s">%s</a> /' % \
-				(config.myreponame, sofar, p)
+				(config.myreponame, escape(sofar), p)
 
 	print """
   </b></div>
@@ -1620,7 +1696,7 @@
   </td>
 			""" % {
 				'myrname': config.myreponame,
-				'f': f,
+				'f': escape(f),
 				'newf': filter_file(dname + '/' + f),
 			}
 		else:
@@ -1633,7 +1709,7 @@
   </td>
 			""" % {
 				'myrname': config.myreponame,
-				'f': f,
+				'f': escape(f),
 				'fullf': filter_file(dname + '/' + f),
 			}
 		print '</tr>'
@@ -1666,10 +1742,16 @@
 
 
 def do_plainblob(fname):
-	print_plain_header()
 	f = open(realpath(fname), 'r')
-	for l in f:
-		sys.stdout.write(fixu8(l))
+
+	if isbinary(fname):
+		print_binary_header(os.path.basename(fname))
+		for l in f:
+			sys.stdout.write(l)
+	else:
+		print_plain_header()
+		for l in f:
+			sys.stdout.write(fixu8(l))
 
 
 def do_annotate(fname, phash, style):
@@ -1866,6 +1948,55 @@
 	print '</channel></rss>'
 
 
+def do_search(s):
+	print_header()
+	print_navbar()
+	ps = get_last_patches(config.searchlimit)
+
+	print '<div class="title">Search last %d commits for "%s"</div>' \
+			% (config.searchlimit, escape(s))
+	print '<table cellspacing="0">'
+
+	alt = False
+	for p in ps:
+		match = p.matches(s)
+		if not match:
+			continue
+
+		if alt:
+			print '<tr class="dark">'
+		else:
+			print '<tr class="light">'
+		alt = not alt
+
+		print """
+  <td><i>%(age)s</i></td>
+  <td>%(author)s</td>
+  <td>
+    <a class="list" title="%(fullname)s" href="%(myrname)s;a=commit;h=%(hash)s">
+      <b>%(name)s</b>
+    </a><br/>
+    %(match)s
+  </td>
+  <td class="link">
+    <a href="%(myrname)s;a=commit;h=%(hash)s">commit</a> |
+    <a href="%(myrname)s;a=commitdiff;h=%(hash)s">commitdiff</a>
+  </td>
+		""" % {
+			'age': how_old(p.local_date),
+			'author': shorten_str(p.shortauthor, 26),
+			'myrname': config.myreponame,
+			'hash': p.hash,
+			'name': escape(shorten_str(p.name)),
+			'fullname': escape(p.name),
+			'match': highlight(s, shorten_str(match)),
+		}
+		print "</tr>"
+
+	print '</table>'
+	print_footer()
+
+
 def do_die():
 	print_header()
 	print "<p><font color=red>Error! Malformed query</font></p>"
@@ -1963,7 +2094,19 @@
 
 		if 'exclude' not in dir(c):
 			c.exclude = []
-		entries = os.listdir(c.multidir)
+
+		entries = []
+		if 'multidir_deep' in dir(c) and c.multidir_deep:
+			for (root, dirs, files) in os.walk(c.multidir):
+				# do not visit hidden directories
+				dirs[:] = [d for d in dirs \
+						if not d.startswith('.')]
+				if '_darcs' in dirs:
+					p = root[1 + len(c.multidir):]
+					entries.append(p)
+		else:
+			entries = os.listdir(c.multidir)
+
 		entries.sort()
 		for name in entries:
 			if name.startswith('.'):
@@ -1974,8 +2117,23 @@
 			if name in c.exclude:
 				continue
 
+			if 'autoexclude' in dir(c) and c.autoexclude:
+				dpath = fulldir + \
+					'/_darcs/third_party/darcsweb'
+				if not os.path.isdir(dpath):
+					continue
+
+			if 'autodesc' in dir(c) and c.autodesc:
+				dpath = fulldir + \
+					'/_darcs/third_party/darcsweb/desc'
+				if os.access(dpath, os.R_OK):
+					desc = open(dpath).read()
+				else:
+					desc = c.repodesc % { 'name': name }
+			else:
+				desc = c.repodesc % { 'name': name }
+
 			rdir = fulldir
-			desc = c.repodesc % { 'name': name }
 			url = c.repourl % { 'name': name }
 			class tmp_config:
 				reponame = name
@@ -2041,6 +2199,11 @@
 	else:
 		config.cachedir = None
 
+	if "searchlimit" in dir(base):
+		config.searchlimit = base.searchlimit
+	else:
+		config.searchlimit = 100
+
 	if name and "footer" in dir(c):
 		config.footer = c.footer
 	elif "footer" in dir(base):
@@ -2077,6 +2240,12 @@
 # check if we have the page in the cache
 if config.cachedir:
 	url_request = os.environ['QUERY_STRING']
+	# create a string representation of the request, ignoring all the
+	# unused parameters to avoid DoS
+	params = ['r', 'a', 'f', 'h', 'topi']
+	params = [ x for x in form.keys() if x in params ]
+	url_request = [ (x, form[x].value) for x in params ]
+	url_request.sort()
 	cache = Cache(config.cachedir, url_request)
 	if cache.open():
 		# we have a hit, dump and run
@@ -2215,6 +2384,15 @@
 elif action == 'atom':
 	do_atom()
 
+elif action == 'search':
+	if form.has_key('s'):
+		s = form["s"].value
+	else:
+		s = ''
+	do_search(s)
+	if config.cachedir:
+		cache.cancel()
+
 else:
 	action = "invalid query"
 	do_die()
diff -rN -u old-darcsweb/LICENSE new-darcsweb/LICENSE
--- old-darcsweb/LICENSE	1969-12-31 21:00:00.000000000 -0300
+++ new-darcsweb/LICENSE	2006-01-09 19:26:06.000000000 -0300
@@ -0,0 +1,34 @@
+
+I don't like licenses, because I don't like having to worry about all this
+legal stuff just for a simple piece of software I don't really mind anyone
+using. But I also believe that it's important that people share and give back;
+so I'm placing darcsweb under the following license, so you feel guilty if you
+don't ;)
+
+
+BOLA - Buena Onda License Agreement
+-----------------------------------
+
+This work is provided 'as-is', without any express or implied warranty. In no
+event will the authors be held liable for any damages arising from the use of
+this work.
+
+To all effects and purposes, this work is to be considered Public Domain.
+
+
+However, if you want to be "Buena onda", you should:
+
+1. Not take credit for it, and give proper recognition to the authors.
+2. Share your modifications, so everybody benefits from them.
+4. Do something nice for the authors.
+5. Help someone who needs it: sign up for some volunteer work or help your
+   neighbour paint the house.
+6. Don't waste. Anything, but specially energy that comes from natural
+   non-renovable resources. Extra points if you discover or invent something
+   to replace them.
+7. Be tolerant. Everything that's good in nature comes from cooperation.
+
+The order is important, and the further you go the more "Buena onda" you are.
+Make the world a better place: be "Buena onda".
+
+
diff -rN -u old-darcsweb/style.css new-darcsweb/style.css
--- old-darcsweb/style.css	2006-01-09 19:26:06.000000000 -0300
+++ new-darcsweb/style.css	2006-01-09 19:26:06.000000000 -0300
@@ -69,6 +69,22 @@
 	padding:8px;
 }
 
+div.search_box {
+	float:right;
+	text-align:right;
+}
+
+input.search_text {
+	font-size:xx-small;
+	background-color: #edece6;
+	vertical-align: top;
+}
+
+input.search_button {
+	font-size:xx-small;
+	vertical-align: top;
+}
+
 div.title, a.title {
 	display:block; padding:6px 8px;
 	font-weight:bold;
@@ -136,7 +152,7 @@
 }
 
 table {
-	clear:both;
+	/*clear:both;*/
 	padding:8px 4px;
 }
 
@@ -239,3 +255,10 @@
 	background-color:#ee5500;
 }
 
+img.logo {
+	border-width:0px;
+	vertical-align:top;
+	margin-left:12pt;
+	margin-right:5pt;
+}
+

