summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitmodules8
-rw-r--r--Makefile102
-rw-r--r--cgit.c36
-rw-r--r--cgit.css75
-rw-r--r--cgit.h47
-rw-r--r--cgitrc9
-rwxr-xr-xgen-version.sh20
m---------git0
-rw-r--r--parsing.c2
-rw-r--r--shared.c45
-rwxr-xr-xsubmodules.sh181
-rw-r--r--ui-commit.c78
-rw-r--r--ui-diff.c66
-rw-r--r--ui-log.c26
-rw-r--r--ui-repolist.c66
-rw-r--r--ui-shared.c234
-rw-r--r--ui-snapshot.c156
-rw-r--r--ui-summary.c49
-rw-r--r--ui-tag.c74
-rw-r--r--ui-tree.c213
-rw-r--r--ui-view.c55
22 files changed, 929 insertions, 614 deletions
diff --git a/.gitignore b/.gitignore
index c4c9ac3..5664962 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 # Files I don't care to see in git-status/commit
 cgit
+VERSION
 *.o
 *~
diff --git a/.gitmodules b/.gitmodules
index 51dd1ef..1daea94 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,5 +1,3 @@
-# This file maps a submodule path to an url from where the submodule
-# can be obtained. The script "submodules.sh" finds the url in this file
-# when invoked with -i to clone the submodules.
-
-git		git://git.kernel.org/pub/scm/git/git.git
+[submodule "git"]
+	url = git://git.kernel.org/pub/scm/git/git.git
+	path = git
diff --git a/Makefile b/Makefile
index 57f80f8..fcbe3e4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,87 +1,71 @@
-CGIT_VERSION = 0.5
-
-prefix = /var/www/htdocs/cgit
-
-SHA1_HEADER = <openssl/sha.h>
-CACHE_ROOT = /var/cache/cgit
-CGIT_CONFIG = /etc/cgitrc
+CGIT_VERSION = v0.5
 CGIT_SCRIPT_NAME = cgit.cgi
+CGIT_SCRIPT_PATH = /var/www/htdocs/cgit
+CGIT_CONFIG = /etc/cgitrc
+CACHE_ROOT = /var/cache/cgit
+SHA1_HEADER = <openssl/sha.h>
+GIT_VER = 1.5.2
+GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2
 
 #
 # Let the user override the above settings.
 #
 -include cgit.conf
 
+
 EXTLIBS = git/libgit.a git/xdiff/lib.a -lz -lcrypto
 OBJECTS = shared.o cache.o parsing.o html.o ui-shared.o ui-repolist.o \
-	ui-summary.o ui-log.o ui-view.o ui-tree.o ui-commit.o ui-diff.o \
-	ui-snapshot.o ui-blob.o
+	ui-summary.o ui-log.o ui-tree.o ui-commit.o ui-diff.o \
+	ui-snapshot.o ui-blob.o ui-tag.o
+
+
+.PHONY: all git install clean distclean force-version get-git
 
-CFLAGS += -Wall
+all: cgit git
 
-ifdef DEBUG
-	CFLAGS += -g
-endif
+VERSION: force-version
+	@./gen-version.sh "$(CGIT_VERSION)"
+-include VERSION
 
-CFLAGS += -Igit
+
+CFLAGS += -g -Wall -Igit
 CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)'
 CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"'
 CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"'
 CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"'
 
 
-#
-# If make is run on a nongit platform, get the git sources as a tarball.
-#
-GITVER = $(shell git version 2>/dev/null || echo nogit)
-ifeq ($(GITVER),nogit)
-GITURL = http://www.kernel.org/pub/software/scm/git/git-1.5.2.tar.bz2
-INITGIT = test -e git/git.c || ((curl "$(GITURL)" | tar -xj) && mv git-1.5.2 git)
-else
-INITGIT = ./submodules.sh -i
-endif
-
-
-#
-# basic build rules
-#
-all: cgit
-
-cgit: cgit.c cgit.h $(OBJECTS)
+cgit: cgit.c $(OBJECTS)
 	$(CC) $(CFLAGS) cgit.c -o cgit $(OBJECTS) $(EXTLIBS)
 
-$(OBJECTS): cgit.h git/libgit.a
+$(OBJECTS): cgit.h git/xdiff/lib.a git/libgit.a VERSION
 
-git/libgit.a:
-	$(INITGIT)
-	$(MAKE) -C git
+git/xdiff/lib.a: | git
 
-#
-# phony targets
-#
-install: all clean-cache
-	mkdir -p $(prefix)
-	install cgit $(prefix)/$(CGIT_SCRIPT_NAME)
-	install cgit.css $(prefix)/cgit.css
+git/libgit.a: | git
 
-clean-cgit:
-	rm -f cgit *.o
+git:
+	cd git && $(MAKE) xdiff/lib.a
+	cd git && $(MAKE) libgit.a
 
-distclean-cgit: clean-cgit
-	git clean -d -x
-
-clean-sub:
-	$(MAKE) -C git clean
-
-distclean-sub: clean-sub
-	$(shell cd git && git clean -d -x)
-
-clean-cache:
+install: all
+	mkdir -p $(CGIT_SCRIPT_PATH)
+	install cgit $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
+	install cgit.css $(CGIT_SCRIPT_PATH)/cgit.css
 	rm -rf $(CACHE_ROOT)/*
 
-clean: clean-cgit clean-sub
+uninstall:
+	rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
+	rm -f $(CGIT_SCRIPT_PATH)/cgit.css
+	rm -rf $(CACHE_ROOT)
+
+clean:
+	rm -f cgit VERSION *.o
+	cd git && $(MAKE) clean
 
-distclean: distclean-cgit distclean-sub
+distclean: clean
+	git clean -d -x
+	cd git && git clean -d -x
 
-.PHONY: all install clean clean-cgit clean-sub clean-cache \
-	distclean distclean-cgit distclean-sub
+get-git:
+	curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git
diff --git a/cgit.c b/cgit.c
index 34e590e..c86d290 100644
--- a/cgit.c
+++ b/cgit.c
@@ -8,9 +8,6 @@
 
 #include "cgit.h"
 
-const char cgit_version[] = CGIT_VERSION;
-
-
 static int cgit_prepare_cache(struct cacheitem *item)
 {
 	if (!cgit_repo && cgit_query_repo) {
@@ -29,13 +26,15 @@ static int cgit_prepare_cache(struct cacheitem *item)
 	}
 
 	if (!cgit_cmd) {
-		item->name = xstrdup(fmt("%s/%s/index.html", cgit_cache_root,
-			   cache_safe_filename(cgit_repo->url)));
+		item->name = xstrdup(fmt("%s/%s/index.%s.html", cgit_cache_root,
+					 cache_safe_filename(cgit_repo->url),
+					 cache_safe_filename(cgit_querystring)));
 		item->ttl = cgit_cache_repo_ttl;
 	} else {
 		item->name = xstrdup(fmt("%s/%s/%s/%s.html", cgit_cache_root,
-			   cache_safe_filename(cgit_repo->url), cgit_query_page,
-			   cache_safe_filename(cgit_querystring)));
+					 cache_safe_filename(cgit_repo->url),
+					 cgit_query_page,
+					 cache_safe_filename(cgit_querystring)));
 		if (cgit_query_has_symref)
 			item->ttl = cgit_cache_dynamic_ttl;
 		else if (cgit_query_has_sha1)
@@ -69,8 +68,10 @@ static void cgit_print_repo_page(struct cacheitem *item)
 	setenv("GIT_DIR", cgit_repo->path, 1);
 
 	if ((cgit_cmd == CMD_SNAPSHOT) && cgit_repo->snapshots) {
-		cgit_print_snapshot(item, cgit_query_sha1, "zip",
-				    cgit_repo->url, cgit_query_name);
+		cgit_print_snapshot(item, cgit_query_head, cgit_query_sha1,
+				    cgit_repobasename(cgit_repo->url),
+				    cgit_query_path,
+				    cgit_repo->snapshots );
 		return;
 	}
 
@@ -92,22 +93,21 @@ static void cgit_print_repo_page(struct cacheitem *item)
 
 	switch(cgit_cmd) {
 	case CMD_LOG:
-		cgit_print_log(cgit_query_head, cgit_query_ofs,
+		cgit_print_log(cgit_query_sha1, cgit_query_ofs,
 			       cgit_max_commit_count, cgit_query_search,
 			       cgit_query_path, 1);
 		break;
 	case CMD_TREE:
-		cgit_print_tree(cgit_query_head, cgit_query_sha1, cgit_query_path);
+		cgit_print_tree(cgit_query_sha1, cgit_query_path);
 		break;
 	case CMD_COMMIT:
-		cgit_print_commit(cgit_query_head);
+		cgit_print_commit(cgit_query_sha1);
 		break;
-	case CMD_VIEW:
-		cgit_print_view(cgit_query_sha1, cgit_query_path);
+	case CMD_TAG:
+		cgit_print_tag(cgit_query_sha1);
 		break;
 	case CMD_DIFF:
-		cgit_print_diff(cgit_query_head, cgit_query_sha1, cgit_query_sha2,
-				cgit_query_path);
+		cgit_print_diff(cgit_query_sha1, cgit_query_sha2);
 		break;
 	default:
 		cgit_print_error("Invalid request");
@@ -227,6 +227,7 @@ static void cgit_parse_args(int argc, const char **argv)
 int main(int argc, const char **argv)
 {
 	struct cacheitem item;
+	const char *cgit_config_env = getenv("CGIT_CONFIG");
 
 	htmlfd = STDOUT_FILENO;
 	item.st.st_mtime = time(NULL);
@@ -234,7 +235,8 @@ int main(int argc, const char **argv)
 	cgit_repolist.count = 0;
 	cgit_repolist.repos = NULL;
 
-	cgit_read_config(CGIT_CONFIG, cgit_global_config_cb);
+	cgit_read_config(cgit_config_env ? cgit_config_env : CGIT_CONFIG,
+			 cgit_global_config_cb);
 	cgit_repo = NULL;
 	if (getenv("SCRIPT_NAME"))
 		cgit_script_name = xstrdup(getenv("SCRIPT_NAME"));
diff --git a/cgit.css b/cgit.css
index 8977533..54bbfcc 100644
--- a/cgit.css
+++ b/cgit.css
@@ -1,6 +1,7 @@
 body {
-	font-family: arial;
+	font-family: arial, sans-serif;
 	font-size: 11pt;
+	color: black;
 	background: white;
 }
 
@@ -94,6 +95,14 @@ td#header {
 	vertical-align: text-bottom;
 }
 
+td#header a {
+	color: #666;
+}
+
+td#header a:hoved {
+	text-decoration: underline;
+}
+
 td#logo {
 	text-align: right;
 	vertical-align: middle;
@@ -114,15 +123,19 @@ td#crumb {
 
 td#crumb a {
 	color: #ccc;
+	background-color: #666;
+	padding: 0em 0.5em 0em 0.5em;
 }
 
 td#crumb a:hover {
-	color: #eee;
+	color: #666;
+	background-color: #ccc;
+	text-decoration: none;
 }
 
 td#search {
 	text-align: right;
-	vertical-align: center;
+	vertical-align: middle;
 	padding-right: 0.5em;
 }
 
@@ -171,35 +184,47 @@ div.error {
 	margin: 1em 2em;
 }
 
-td.ls-blob, td.ls-dir, td.ls-mod {
+a.ls-blob, a.ls-dir, a.ls-mod {
 	font-family: monospace;
 }
 
-div.ls-dir a {
-	font-weight: bold;
+td.ls-size {
+	text-align: right;
 }
 
-th.filesize, td.filesize {
-	text-align: right;
+td.ls-size {
+	font-family: monospace;
 }
 
-td.filesize {
+td.ls-mode {
 	font-family: monospace;
 }
 
-td.links {
-	font-size: 80%;
-	padding-left: 2em;
+table.blob {
+	margin-top: 0.5em;
+	border-top: solid 1px black;
 }
 
-td.filemode {
-	font-family: monospace;
+table.blob td.no {
+	border-right: solid 1px black;
+	color: black;
+	background-color: #eee;
+	text-align: right;
+}
+
+table.blob td.no a {
+	color: black;
 }
 
-td.blob {
+table.blob td.no a:hover {
+	color: black;
+	text-decoration: none;
+}
+
+table.blob td.txt {
 	white-space: pre;
 	font-family: monospace;
-	background-color: white;
+	padding-left: 0.5em;
 }
 
 table.nowrap td {
@@ -215,6 +240,7 @@ table.commit-info th {
 	text-align: left;
 	font-weight: normal;
 	padding: 0.1em 1em 0.1em 0.1em;
+	vertical-align: top;
 }
 
 table.commit-info td {
@@ -287,7 +313,7 @@ table.diffstat td.upd a {
 
 table.diffstat td.graph {
 	width: 75%;
-	vertical-align: center;
+	vertical-align: middle;
 }
 
 table.diffstat td.graph table {
@@ -308,10 +334,6 @@ table.diffstat td.graph td.rem {
 	background-color: #c55;
 }
 
-table.diffstat td.graph td.none {
-	background-color: none;
-}
-
 div.diffstat-summary {
 	color: #888;
 	padding-top: 0.5em;
@@ -340,7 +362,7 @@ table.diff td div.del {
 }
 
 .sha1 {
-	font-family: courier;
+	font-family: monospace;
 	font-size: 90%;
 }
 
@@ -359,16 +381,17 @@ table.list td.repogroup {
 
 a.button {
 	font-size: 80%;
-	color: #333;
-	background-color: #ccc;
-	border: solid 1px #999;
+	color: #aaa;
+	background-color: #eee;
+	border: solid 1px #aaa;
 	padding: 0em 0.5em;
 	margin: 0.1em 0.25em;
 }
 
 a.button:hover {
 	text-decoration: none;
-	background-color: #eee;
+	color: #333;
+	background-color: #ccc;
 }
 
 a.primary {
diff --git a/cgit.h b/cgit.h
index 2f3fca1..e3d9cb8 100644
--- a/cgit.h
+++ b/cgit.h
@@ -25,10 +25,9 @@
 #define CMD_COMMIT   2
 #define CMD_DIFF     3
 #define CMD_TREE     4
-#define CMD_VIEW     5
-#define CMD_BLOB     6
-#define CMD_SNAPSHOT 7
-
+#define CMD_BLOB     5
+#define CMD_SNAPSHOT 6
+#define CMD_TAG      7
 
 /*
  * Dateformats used on misc. pages
@@ -99,7 +98,7 @@ struct taginfo {
 	char *msg;
 };
 
-extern const char cgit_version[];
+extern const char *cgit_version;
 
 extern struct repolist cgit_repolist;
 extern struct repoinfo *cgit_repo;
@@ -119,6 +118,7 @@ extern char *cgit_repo_group;
 
 extern int cgit_nocache;
 extern int cgit_snapshots;
+extern int cgit_enable_index_links;
 extern int cgit_enable_log_filecount;
 extern int cgit_enable_log_linecount;
 extern int cgit_max_lock_attempts;
@@ -157,8 +157,10 @@ extern void cgit_querystring_cb(const char *name, const char *value);
 
 extern int chk_zero(int result, char *msg);
 extern int chk_positive(int result, char *msg);
+extern int chk_non_negative(int result, char *msg);
 
 extern int hextoint(char c);
+extern char *trim_end(const char *str, char c);
 
 extern void *cgit_free_commitinfo(struct commitinfo *info);
 
@@ -199,9 +201,26 @@ extern int cache_exist(struct cacheitem *item);
 extern int cache_expired(struct cacheitem *item);
 
 extern char *cgit_repourl(const char *reponame);
+extern char *cgit_fileurl(const char *reponame, const char *pagename,
+			  const char *filename, const char *query);
 extern char *cgit_pageurl(const char *reponame, const char *pagename,
 			  const char *query);
 
+extern const char *cgit_repobasename(const char *reponame);
+
+extern void cgit_tree_link(char *name, char *title, char *class, char *head,
+			   char *rev, char *path);
+extern void cgit_log_link(char *name, char *title, char *class, char *head,
+			  char *rev, char *path, int ofs);
+extern void cgit_commit_link(char *name, char *title, char *class, char *head,
+			     char *rev);
+extern void cgit_snapshot_link(char *name, char *title, char *class,
+			       char *head, char *rev, char *archivename);
+extern void cgit_diff_link(char *name, char *title, char *class, char *head,
+			   char *new_rev, char *old_rev, char *path);
+
+extern void cgit_object_link(struct object *obj);
+
 extern void cgit_print_error(char *msg);
 extern void cgit_print_date(time_t secs, char *format);
 extern void cgit_print_age(time_t t, time_t max_relative, char *format);
@@ -215,14 +234,16 @@ extern void cgit_print_snapshot_start(const char *mimetype,
 extern void cgit_print_repolist(struct cacheitem *item);
 extern void cgit_print_summary();
 extern void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *path, int pager);
-extern void cgit_print_view(const char *hex, char *path);
 extern void cgit_print_blob(struct cacheitem *item, const char *hex, char *path);
-extern void cgit_print_tree(const char *rev, const char *hex, char *path);
-extern void cgit_print_commit(const char *hex);
-extern void cgit_print_diff(const char *head, const char *old_hex, const char *new_hex,
-			    char *path);
-extern void cgit_print_snapshot(struct cacheitem *item, const char *hex,
-				const char *format, const char *prefix,
-				const char *filename);
+extern void cgit_print_tree(const char *rev, char *path);
+extern void cgit_print_commit(char *hex);
+extern void cgit_print_tag(char *revname);
+extern void cgit_print_diff(const char *new_hex, const char *old_hex);
+extern void cgit_print_snapshot(struct cacheitem *item, const char *head,
+				const char *hex, const char *prefix,
+				const char *filename, int snapshot);
+extern void cgit_print_snapshot_links(const char *repo, const char *head,
+				      const char *hex, int snapshots);
+extern int cgit_parse_snapshots_mask(const char *str);
 
 #endif /* CGIT_H */
diff --git a/cgitrc b/cgitrc
index 0f602e4..1040997 100644
--- a/cgitrc
+++ b/cgitrc
@@ -8,10 +8,15 @@
 #nocache=0
 
 
-## Enable/disable snapshots by default. This can be overridden per repo
+## Set allowed snapshot types by default. Can be overridden per repo
+# can be any combination of zip/tar.gz/tar.bz2/tar
 #snapshots=0
 
 
+## Enable/disable extra links to summary/log/tree per repo on index page
+#enable-index-links=0
+
+
 ## Enable/disable display of 'number of files changed' in log view
 #enable-log-filecount=0
 
@@ -109,7 +114,7 @@
 #repo.desc=the caching cgi for git
 #repo.path=/pub/git/cgit
 #repo.owner=Lars Hjemli
-#repo.snapshots=1				# override a sitewide snapshot-setting
+#repo.snapshots=tar.bz2				# override a sitewide snapshot-setting
 #repo.enable-log-filecount=0			# override the default filecount setting
 #repo.enable-log-linecount=0			# override the default linecount setting
 #repo.module-link=/git/%s/commit/?id=%s		# override the standard module-link
diff --git a/gen-version.sh b/gen-version.sh
new file mode 100755
index 0000000..739c83e
--- /dev/null
+++ b/gen-version.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+# Get version-info specified in Makefile
+V=$1
+
+# Use `git describe` to get current version if we're inside a git repo
+if test -d .git
+then
+	V=$(git describe --abbrev=4 HEAD 2>/dev/null | sed -e 's/-/./g')
+fi
+
+new="CGIT_VERSION = $V"
+old=$(cat VERSION 2>/dev/null)
+
+# Exit if VERSION is uptodate
+test "$old" = "$new" && exit 0
+
+# Update VERSION with new version-info
+echo "$new" > VERSION
+cat VERSION
diff --git a/git b/git
-Subproject aba170cdb4874b72dd619e6f7bbc13c33295f83
+Subproject 86bab9615c3516d4ac7756ae3c1285d331b78f0
diff --git a/parsing.c b/parsing.c
index 74a2484..2c05c09 100644
--- a/parsing.c
+++ b/parsing.c
@@ -168,7 +168,7 @@ void cgit_parse_url(const char *url)
 		if (p) {
 			p[0] = '\0';
 			if (p[1])
-				cgit_query_path = xstrdup(p + 1);
+				cgit_query_path = trim_end(p + 1, '/');
 		}
 		cgit_cmd = cgit_get_cmd_index(cmd + 1);
 		cgit_query_page = xstrdup(cmd + 1);
diff --git a/shared.c b/shared.c
index b6d2fa1..077934f 100644
--- a/shared.c
+++ b/shared.c
@@ -12,6 +12,8 @@ struct repolist cgit_repolist;
 struct repoinfo *cgit_repo;
 int cgit_cmd;
 
+const char *cgit_version = CGIT_VERSION;
+
 char *cgit_root_title   = "Git repository browser";
 char *cgit_css          = "/cgit.css";
 char *cgit_logo         = "/git-logo.png";
@@ -26,6 +28,7 @@ char *cgit_repo_group   = NULL;
 
 int cgit_nocache               =  0;
 int cgit_snapshots             =  0;
+int cgit_enable_index_links    =  0;
 int cgit_enable_log_filecount  =  0;
 int cgit_enable_log_linecount  =  0;
 int cgit_max_lock_attempts     =  5;
@@ -59,7 +62,8 @@ int htmlfd = 0;
 
 int cgit_get_cmd_index(const char *cmd)
 {
-	static char *cmds[] = {"log", "commit", "diff", "tree", "view", "blob", "snapshot", NULL};
+	static char *cmds[] = {"log", "commit", "diff", "tree", "blob",
+			       "snapshot", "tag", NULL};
 	int i;
 
 	for(i = 0; cmds[i]; i++)
@@ -82,6 +86,13 @@ int chk_positive(int result, char *msg)
 	return result;
 }
 
+int chk_non_negative(int result, char *msg)
+{
+    	if (result < 0)
+	    	die("%s: %s",msg, strerror(errno));
+	return result;
+}
+
 struct repoinfo *add_repo(const char *url)
 {
 	struct repoinfo *ret;
@@ -144,7 +155,9 @@ void cgit_global_config_cb(const char *name, const char *value)
 	else if (!strcmp(name, "nocache"))
 		cgit_nocache = atoi(value);
 	else if (!strcmp(name, "snapshots"))
-		cgit_snapshots = atoi(value);
+		cgit_snapshots = cgit_parse_snapshots_mask(value);
+	else if (!strcmp(name, "enable-index-links"))
+		cgit_enable_index_links = atoi(value);
 	else if (!strcmp(name, "enable-log-filecount"))
 		cgit_enable_log_filecount = atoi(value);
 	else if (!strcmp(name, "enable-log-linecount"))
@@ -184,7 +197,7 @@ void cgit_global_config_cb(const char *name, const char *value)
 	else if (cgit_repo && !strcmp(name, "repo.defbranch"))
 		cgit_repo->defbranch = xstrdup(value);
 	else if (cgit_repo && !strcmp(name, "repo.snapshots"))
-		cgit_repo->snapshots = cgit_snapshots * atoi(value);
+		cgit_repo->snapshots = cgit_snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */
 	else if (cgit_repo && !strcmp(name, "repo.enable-log-filecount"))
 		cgit_repo->enable_log_filecount = cgit_enable_log_filecount * atoi(value);
 	else if (cgit_repo && !strcmp(name, "repo.enable-log-linecount"))
@@ -224,7 +237,7 @@ void cgit_querystring_cb(const char *name, const char *value)
 	} else if (!strcmp(name, "ofs")) {
 		cgit_query_ofs = atoi(value);
 	} else if (!strcmp(name, "path")) {
-		cgit_query_path = xstrdup(value);
+		cgit_query_path = trim_end(value, '/');
 	} else if (!strcmp(name, "name")) {
 		cgit_query_name = xstrdup(value);
 	}
@@ -253,6 +266,28 @@ int hextoint(char c)
 		return -1;
 }
 
+char *trim_end(const char *str, char c)
+{
+	int len;
+	char *s, *t;
+
+	if (str == NULL)
+		return NULL;
+	t = (char *)str;
+	len = strlen(t);
+	while(len > 0 && t[len - 1] == c)
+		len--;
+
+	if (len == 0)
+		return NULL;
+
+	c = t[len];
+	t[len] = '\0';
+	s = xstrdup(t);
+	t[len] = c;
+	return s;
+}
+
 void cgit_diff_tree_cb(struct diff_queue_struct *q,
 		       struct diff_options *options, void *data)
 {
@@ -359,7 +394,7 @@ void cgit_diff_tree(const unsigned char *old_sha1,
 	opt.format_callback_data = fn;
 	diff_setup_done(&opt);
 
-	if (old_sha1)
+	if (old_sha1 && !is_null_sha1(old_sha1))
 		ret = diff_tree_sha1(old_sha1, new_sha1, "", &opt);
 	else
 		ret = diff_root_tree_sha1(new_sha1, "", &opt);
diff --git a/submodules.sh b/submodules.sh
deleted file mode 100755
index 1d7b13f..0000000
--- a/submodules.sh
+++ /dev/null
@@ -1,181 +0,0 @@
-#!/bin/sh
-#
-# submodules.sh: init, update or list git submodules
-#
-# Copyright (C) 2006 Lars Hjemli
-#
-# Licensed under GNU General Public License v2
-#   (see COPYING for full license text)
-#
-
-
-usage="submodules.sh [-i | -u] [-q] [--cached] [path...]"
-init=
-update=
-quiet=
-cached=
-
-
-say()
-{
-	if test -z "$quiet"
-	then
-		echo -e "$@"
-	fi
-}
-
-
-die()
-{
-	echo >&2 -e "$@"
-	exit 1
-}
-
-
-
-#
-# Silently checkout specified submodule revision, return exit status of git-checkout
-#
-# $1 = local path
-# $2 = requested sha1
-#
-module_checkout()
-{
-	$(cd "$1" && git checkout "$2" 1>/dev/null 2>/dev/null)
-}
-
-
-#
-# Find all (requested) submodules, run clone + checkout on missing paths
-#
-# $@ = requested paths (default to all)
-#
-modules_init()
-{
-	git ls-files --stage -- $@ | grep -e '^160000 ' |
-	while read mode sha1 stage path
-	do
-		test -d "$path/.git" && continue
-
-		if test -d "$path"
-		then
-			rmdir "$path" 2>/dev/null ||
-			die "Directory '$path' exist, but not as a submodule"
-		fi
-
-		test -e "$path" && die "A file already exist at path '$path'"
-
-		url=$(sed -nre "s/^$path[ \t]+//p" .gitmodules)
-		test -z "$url" && die "No url found for $path in .gitmodules"
-
-		git clone "$url" "$path" || die "Clone of submodule '$path' failed"
-		module_checkout "$path" "$sha1" || die "Checkout of submodule '$path' failed"
-		say "Submodule '$path' initialized"
-	done
-}
-
-#
-# Checkout correct revision of each initialized submodule
-#
-# $@ = requested paths (default to all)
-#
-modules_update()
-{
-	git ls-files --stage -- $@ | grep -e '^160000 ' |
-	while read mode sha1 stage path
-	do
-		if ! test -d "$path/.git"
-		then
-			say "Submodule '$path' not initialized"
-			continue;
-		fi
-		subsha1=$(cd "$path" && git rev-parse --verify HEAD) ||
-		die "Unable to find current revision of submodule '$path'"
-		if test "$subsha1" != "$sha1"
-		then
-			module_checkout "$path" "$sha1" ||
-			die "Unable to checkout revision $sha1 of submodule '$path'"
-			say "Submodule '$path' reset to revision $sha1"
-		fi
-	done
-}
-
-#
-# List all registered submodules, prefixed with:
-#  - submodule not initialized
-#  + different version checked out
-#
-# If --cached was specified the revision in the index will be printed
-# instead of the currently checked out revision.
-#
-# $@ = requested paths (default to all)
-#
-modules_list()
-{
-	git ls-files --stage -- $@ | grep -e '^160000 ' |
-	while read mode sha1 stage path
-	do
-		if ! test -d "$path/.git"
-		then
-			say "-$sha1 $path"
-			continue;
-		fi
-		revname=$(cd "$path" && git describe $sha1)
-		if git diff-files --quiet -- "$path"
-		then
-			say " $sha1 $path\t($revname)"
-		else
-			if test -z "$cached"
-			then
-				sha1=$(cd "$path" && git rev-parse HEAD)
-				revname=$(cd "$path" && git describe HEAD)
-			fi
-			say "+$sha1 $path\t($revname)"
-		fi
-	done
-}
-
-
-while case "$#" in 0) break ;; esac
-do
-	case "$1" in
-	-i)
-		init=1
-		;;
-	-u)
-		update=1
-		;;
-	-q)
-		quiet=1
-		;;
-	--cached)
-		cached=1
-		;;
-	--)
-		break
-		;;
-	-*)
-		echo "Usage: $usage"
-		exit 1
-		;;
-	--*)
-		echo "Usage: $usage"
-		exit 1
-		;;
-	*)
-		break
-		;;
-	esac
-	shift
-done
-
-
-if test "$init" = "1"
-then
-	modules_init $@
-elif test "$update" = "1"
-then
-	modules_update $@
-else
-	modules_list $@
-fi
diff --git a/ui-commit.c b/ui-commit.c
index 1d12bbb..90e09ed 100644
--- a/ui-commit.c
+++ b/ui-commit.c
@@ -11,6 +11,7 @@
 static int files, slots;
 static int total_adds, total_rems, max_changes;
 static int lines_added, lines_removed;
+static char *curr_rev;
 
 static struct fileinfo {
 	char status;
@@ -27,7 +28,6 @@ static struct fileinfo {
 
 void print_fileinfo(struct fileinfo *info)
 {
-	char *query, *query2;
 	char *class;
 
 	switch (info->status) {
@@ -75,24 +75,12 @@ void print_fileinfo(struct fileinfo *info)
 		html("]</span>");
 	}
 	htmlf("</td><td class='%s'>", class);
-	query = fmt("id=%s&amp;id2=%s&amp;path=%s", sha1_to_hex(info->old_sha1),
-		    sha1_to_hex(info->new_sha1), info->new_path);
-	html_link_open(cgit_pageurl(cgit_query_repo, "diff", query),
-		       NULL, NULL);
-	if (info->status == DIFF_STATUS_COPIED ||
-	    info->status == DIFF_STATUS_RENAMED) {
-		html_txt(info->new_path);
-		htmlf("</a> (%s from ", info->status == DIFF_STATUS_COPIED ?
-		      "copied" : "renamed");
-		query2 = fmt("id=%s", sha1_to_hex(info->old_sha1));
-		html_link_open(cgit_pageurl(cgit_query_repo, "view", query2),
-			       NULL, NULL);
-		html_txt(info->old_path);
-		html("</a>)");
-	} else {
-		html_txt(info->new_path);
-		html("</a>");
-	}
+	cgit_tree_link(info->new_path, NULL, NULL, cgit_query_head, curr_rev,
+		       info->new_path);
+	if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED)
+		htmlf(" (%s from %s)",
+		      info->status == DIFF_STATUS_COPIED ? "copied" : "renamed",
+		      info->old_path);
 	html("</td><td class='right'>");
 	htmlf("%d", info->added + info->removed);
 	html("</td><td class='graph'>");
@@ -145,16 +133,19 @@ void inspect_filepair(struct diff_filepair *pair)
 }
 
 
-void cgit_print_commit(const char *hex)
+void cgit_print_commit(char *hex)
 {
 	struct commit *commit, *parent;
 	struct commitinfo *info;
 	struct commit_list *p;
 	unsigned char sha1[20];
-	char *query;
-	char *filename;
+	char *tmp;
 	int i;
 
+	if (!hex)
+		hex = cgit_query_head;
+	curr_rev = hex;
+
 	if (get_sha1(hex, sha1)) {
 		cgit_print_error(fmt("Bad object id: %s", hex));
 		return;
@@ -181,11 +172,11 @@ void cgit_print_commit(const char *hex)
 	html("</td><td class='right'>");
 	cgit_print_date(info->committer_date, FMT_LONGDATE);
 	html("</td></tr>\n");
-	html("<tr><th>tree</th><td colspan='2' class='sha1'><a href='");
-	query = fmt("h=%s&amp;id=%s", sha1_to_hex(commit->object.sha1),
-		    sha1_to_hex(commit->tree->object.sha1));
-	html_attr(cgit_pageurl(cgit_query_repo, "tree", query));
-	htmlf("'>%s</a></td></tr>\n", sha1_to_hex(commit->tree->object.sha1));
+	html("<tr><th>tree</th><td colspan='2' class='sha1'>");
+	tmp = xstrdup(hex);
+	cgit_tree_link(sha1_to_hex(commit->tree->object.sha1), NULL, NULL,
+		       cgit_query_head, tmp, NULL);
+	html("</td></tr>\n");
       	for (p = commit->parents; p ; p = p->next) {
 		parent = lookup_commit_reference(p->item->object.sha1);
 		if (!parent) {
@@ -195,23 +186,19 @@ void cgit_print_commit(const char *hex)
 			continue;
 		}
 		html("<tr><th>parent</th>"
-		     "<td colspan='2' class='sha1'>"
-		     "<a href='");
-		query = fmt("h=%s", sha1_to_hex(p->item->object.sha1));
-		html_attr(cgit_pageurl(cgit_query_repo, "commit", query));
-		htmlf("'>%s</a> (<a href='",
-		      sha1_to_hex(p->item->object.sha1));
-		query = fmt("id=%s&amp;id2=%s", sha1_to_hex(parent->tree->object.sha1),
-			    sha1_to_hex(commit->tree->object.sha1));
-		html_attr(cgit_pageurl(cgit_query_repo, "diff", query));
-		html("'>diff</a>)</td></tr>");
+		     "<td colspan='2' class='sha1'>");
+		cgit_commit_link(sha1_to_hex(p->item->object.sha1), NULL, NULL,
+				 cgit_query_head, sha1_to_hex(p->item->object.sha1));
+		html(" (");
+		cgit_diff_link("diff", NULL, NULL, cgit_query_head, hex,
+			       sha1_to_hex(p->item->object.sha1), NULL);
+		html(")</td></tr>");
 	}
 	if (cgit_repo->snapshots) {
-		htmlf("<tr><th>download</th><td colspan='2' class='sha1'><a href='");
-		filename = fmt("%s-%s.zip", cgit_query_repo, hex);
-		html_attr(cgit_pageurl(cgit_query_repo, "snapshot",
-				       fmt("id=%s&amp;name=%s", hex, filename)));
-		htmlf("'>%s</a></td></tr>", filename);
+		html("<tr><th>download</th><td colspan='2' class='sha1'>");
+		cgit_print_snapshot_links(cgit_query_repo, cgit_query_head,
+					  hex, cgit_repo->snapshots);
+		html("</td></tr>");
 	}
 	html("</table>\n");
 	html("<div class='commit-subject'>");
@@ -231,10 +218,9 @@ void cgit_print_commit(const char *hex)
 		html("<div class='diffstat-summary'>");
 		htmlf("%d files changed, %d insertions, %d deletions (",
 		      files, total_adds, total_rems);
-		query = fmt("h=%s", hex);
-		html_link_open(cgit_pageurl(cgit_query_repo, "diff", query), NULL, NULL);
-		html("show diff</a>)");
-		html("</div>");
+		cgit_diff_link("show diff", NULL, NULL, cgit_query_head, hex,
+			       NULL, NULL);
+		html(")</div>");
 	}
 	cgit_free_commitinfo(info);
 }
diff --git a/ui-diff.c b/ui-diff.c
index 4695e3a..0be845f 100644
--- a/ui-diff.c
+++ b/ui-diff.c
@@ -89,54 +89,52 @@ static void filepair_cb(struct diff_filepair *pair)
 		cgit_print_error("Error running diff");
 }
 
-void cgit_print_diff(const char *head, const char *old_hex, const char *new_hex, char *path)
+void cgit_print_diff(const char *new_rev, const char *old_rev)
 {
 	unsigned char sha1[20], sha2[20];
 	enum object_type type;
 	unsigned long size;
-	struct commit *commit;
-
-	if (head && !old_hex && !new_hex) {
-		get_sha1(head, sha1);
-		commit = lookup_commit_reference(sha1);
-		if (commit && !parse_commit(commit)) {
-			html("<table class='diff'>");
-			html("<tr><td>");
-			cgit_diff_commit(commit, filepair_cb);
-			html("</td></tr>");
-			html("</table>");
-		}
+	struct commit *commit, *commit2;
+
+	if (!new_rev)
+		new_rev = cgit_query_head;
+	get_sha1(new_rev, sha1);
+	type = sha1_object_info(sha1, &size);
+	if (type == OBJ_BAD) {
+		cgit_print_error(fmt("Bad object name: %s", new_rev));
+		return;
+	}
+	if (type != OBJ_COMMIT) {
+		cgit_print_error(fmt("Unhandled object type: %s",
+				     typename(type)));
 		return;
 	}
 
-	get_sha1(old_hex, sha1);
-	get_sha1(new_hex, sha2);
+	commit = lookup_commit_reference(sha1);
+	if (!commit || parse_commit(commit))
+		cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(sha1)));
 
-	type = sha1_object_info(sha1, &size);
-	if (type == OBJ_BAD) {
+	if (old_rev)
+		get_sha1(old_rev, sha2);
+	else if (commit->parents && commit->parents->item)
+		hashcpy(sha2, commit->parents->item->object.sha1);
+	else
+		hashclr(sha2);
+
+	if (!is_null_sha1(sha2)) {
 		type = sha1_object_info(sha2, &size);
 		if (type == OBJ_BAD) {
-			cgit_print_error(fmt("Bad object names: %s, %s", old_hex, new_hex));
+			cgit_print_error(fmt("Bad object name: %s", sha1_to_hex(sha2)));
 			return;
 		}
+		commit2 = lookup_commit_reference(sha2);
+		if (!commit2 || parse_commit(commit2))
+			cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(sha2)));
 	}
 
 	html("<table class='diff'>");
-	switch(type) {
-	case OBJ_BLOB:
-		html("<tr><td>");
-		header(sha1, path, 0644, sha2, path, 0644);
-		if (cgit_diff_files(sha1, sha2, print_line))
-			cgit_print_error("Error running diff");
-		html("</td></tr>");
-		break;
-	case OBJ_TREE:
-		cgit_diff_tree(sha1, sha2, filepair_cb);
-		break;
-	default:
-		cgit_print_error(fmt("Unhandled object type: %s",
-				     typename(type)));
-		break;
-	}
+	html("<tr><td>");
+	cgit_diff_tree(sha2, sha1, filepair_cb);
+	html("</td></tr>");
 	html("</table>");
 }
diff --git a/ui-log.c b/ui-log.c
index bb17e1d..d38e40a 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -31,11 +31,8 @@ void print_commit(struct commit *commit)
 	html("<tr><td>");
 	cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE);
 	html("</td><td>");
-	char *qry = fmt("h=%s", sha1_to_hex(commit->object.sha1));
-	char *url = cgit_pageurl(cgit_query_repo, "commit", qry);
-	html_link_open(url, NULL, NULL);
-	html_ntxt(cgit_max_msg_len, info->subject);
-	html_link_close();
+	cgit_commit_link(info->subject, NULL, NULL, cgit_query_head,
+			 sha1_to_hex(commit->object.sha1));
 	if (cgit_repo->enable_log_filecount) {
 		files = 0;
 		lines = 0;
@@ -62,6 +59,9 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *path, i
 	int argc = 2;
 	int i;
 
+	if (!tip)
+		argv[1] = cgit_query_head;
+
 	if (grep)
 		argv[argc++] = fmt("--grep=%s", grep);
 	if (path) {
@@ -113,17 +113,15 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *path, i
 	if (pager) {
 		html("<div class='pager'>");
 		if (ofs > 0) {
-			html("&nbsp;<a href='");
-			html(cgit_pageurl(cgit_query_repo, cgit_query_page,
-					  fmt("h=%s&amp;ofs=%d", tip, ofs-cnt)));
-			html("'>[prev]</a>&nbsp;");
+			cgit_log_link("[prev]", NULL, NULL, cgit_query_head,
+				      cgit_query_sha1, cgit_query_path,
+				      ofs - cnt);
+			html("&nbsp;");
 		}
-
 		if ((commit = get_revision(&rev)) != NULL) {
-			html("&nbsp;<a href='");
-			html(cgit_pageurl(cgit_query_repo, "log",
-					  fmt("h=%s&amp;ofs=%d", tip, ofs+cnt)));
-			html("'>[next]</a>&nbsp;");
+			cgit_log_link("[next]", NULL, NULL, cgit_query_head,
+				      cgit_query_sha1, cgit_query_path,
+				      ofs + cnt);
 		}
 		html("</div>");
 	}
diff --git a/ui-repolist.c b/ui-repolist.c
index e5c6c20..4c86543 100644
--- a/ui-repolist.c
+++ b/ui-repolist.c
@@ -44,16 +44,19 @@ static void print_modtime(struct repoinfo *repo)
 
 void cgit_print_repolist(struct cacheitem *item)
 {
-	struct repoinfo *repo;
-	int i;
+	int i, columns = 4;
 	char *last_group = NULL;
 
+	if (cgit_enable_index_links)
+		columns++;
+
 	cgit_print_docstart(cgit_root_title, item);
 	cgit_print_pageheader(cgit_root_title, 0);
 
 	html("<table class='list nowrap'>");
 	if (cgit_index_header) {
-		html("<tr class='nohover'><td colspan='5' class='include-block'>");
+		htmlf("<tr class='nohover'><td colspan='%d' class='include-block'>",
+		      columns);
 		html_include(cgit_index_header);
 		html("</td></tr>");
 	}
@@ -61,42 +64,45 @@ void cgit_print_repolist(struct cacheitem *item)
 	     "<th class='left'>Name</th>"
 	     "<th class='left'>Description</th>"
 	     "<th class='left'>Owner</th>"
-	     "<th class='left'>Idle</th>"
-	     "<th>Links</th></tr>\n");
+	     "<th class='left'>Idle</th>");
+	if (cgit_enable_index_links)
+		html("<th>Links</th>");
+	html("</tr>\n");
 
 	for (i=0; i<cgit_repolist.count; i++) {
-		repo = &cgit_repolist.repos[i];
-		if ((last_group == NULL && repo->group != NULL) ||
-		    (last_group != NULL && repo->group == NULL) ||
-		    (last_group != NULL && repo->group!= NULL &&
-		     strcmp(repo->group, last_group))) {
-			html("<tr class='nohover'><td colspan='4' class='repogroup'>");
-			html_txt(repo->group);
+		cgit_repo = &cgit_repolist.repos[i];
+		if ((last_group == NULL && cgit_repo->group != NULL) ||
+		    (last_group != NULL && cgit_repo->group == NULL) ||
+		    (last_group != NULL && cgit_repo->group != NULL &&
+		     strcmp(cgit_repo->group, last_group))) {
+			htmlf("<tr class='nohover'><td colspan='%d' class='repogroup'>",
+			      columns);
+			html_txt(cgit_repo->group);
 			html("</td></tr>");
-			last_group = repo->group;
+			last_group = cgit_repo->group;
 		}
 		htmlf("<tr><td class='%s'>",
-		      repo->group ? "sublevel-repo" : "toplevel-repo");
-		html_link_open(cgit_repourl(repo->url), repo->desc, NULL);
-		html_txt(repo->name);
+		      cgit_repo->group ? "sublevel-repo" : "toplevel-repo");
+		html_link_open(cgit_repourl(cgit_repo->url), NULL, NULL);
+		html_txt(cgit_repo->name);
 		html_link_close();
 		html("</td><td>");
-		html_ntxt(cgit_max_repodesc_len, repo->desc);
-		html("</td><td>");
-		html_txt(repo->owner);
+		html_ntxt(cgit_max_repodesc_len, cgit_repo->desc);
 		html("</td><td>");
-		print_modtime(repo);
+		html_txt(cgit_repo->owner);
 		html("</td><td>");
-		html_link_open(cgit_repourl(repo->url),
-			       "Summary", "button");
-		html("S</a>");
-		html_link_open(cgit_pageurl(repo->name, "log", NULL),
-			       "Log", "button");
-		html("L</a>");
-		html_link_open(cgit_pageurl(repo->name, "tree", NULL),
-			       "Files", "button");
-		html("F</a>");
-		html("</td></tr>\n");
+		print_modtime(cgit_repo);
+		html("</td>");
+		if (cgit_enable_index_links) {
+			html("<td>");
+			html_link_open(cgit_repourl(cgit_repo->url),
+				       NULL, "button");
+			html("summary</a>");
+			cgit_log_link("log", NULL, "button", NULL, NULL, NULL, 0);
+			cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL);
+			html("</td>");
+		}
+		html("</tr>\n");
 	}
 	html("</table>");
 	cgit_print_docend();
diff --git a/ui-shared.c b/ui-shared.c
index aba93e8..5c5bcf3 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -57,13 +57,13 @@ char *cgit_repourl(const char *reponame)
 	}
 }
 
-char *cgit_pageurl(const char *reponame, const char *pagename,
-		   const char *query)
+char *cgit_fileurl(const char *reponame, const char *pagename,
+		   const char *filename, const char *query)
 {
 	if (cgit_virtual_root) {
 		if (query)
-			return fmt("%s/%s/%s/?%s", cgit_virtual_root, reponame,
-				   pagename, query);
+			return fmt("%s/%s/%s/%s?%s", cgit_virtual_root, reponame,
+				   pagename, filename?filename:"", query);
 		else
 			return fmt("%s/%s/%s/", cgit_virtual_root, reponame,
 				   pagename);
@@ -75,6 +75,37 @@ char *cgit_pageurl(const char *reponame, const char *pagename,
 	}
 }
 
+char *cgit_pageurl(const char *reponame, const char *pagename,
+		   const char *query)
+{
+	return cgit_fileurl(reponame,pagename,0,query);
+}
+
+const char *cgit_repobasename(const char *reponame)
+{
+	/* I assume we don't need to store more than one repo basename */
+	static char rvbuf[1024];
+	int p;
+	const char *rv;
+	strncpy(rvbuf,reponame,sizeof(rvbuf));
+	if(rvbuf[sizeof(rvbuf)-1])
+		die("cgit_repobasename: truncated repository name '%s'", reponame);
+	p = strlen(rvbuf)-1;
+	/* strip trailing slashes */
+	while(p && rvbuf[p]=='/') rvbuf[p--]=0;
+	/* strip trailing .git */
+	if(p>=3 && !strncmp(&rvbuf[p-3],".git",4)) {
+		p -= 3; rvbuf[p--] = 0;
+	}
+	/* strip more trailing slashes if any */
+	while( p && rvbuf[p]=='/') rvbuf[p--]=0;
+	/* find last slash in the remaining string */
+	rv = strrchr(rvbuf,'/');
+	if(rv)
+		return ++rv;
+	return rvbuf;
+}
+
 char *cgit_currurl()
 {
 	if (!cgit_virtual_root)
@@ -87,6 +118,166 @@ char *cgit_currurl()
 		return fmt("%s/", cgit_virtual_root);
 }
 
+static char *repolink(char *title, char *class, char *page, char *head,
+		      char *path)
+{
+	char *delim = "?";
+
+	html("<a");
+	if (title) {
+		html(" title='");
+		html_attr(title);
+		html("'");
+	}
+	if (class) {
+		html(" class='");
+		html_attr(class);
+		html("'");
+	}
+	html(" href='");
+	if (cgit_virtual_root) {
+		html_attr(cgit_virtual_root);
+		if (cgit_virtual_root[strlen(cgit_virtual_root) - 1] != '/')
+			html("/");
+		html_attr(cgit_repo->url);
+		if (cgit_repo->url[strlen(cgit_repo->url) - 1] != '/')
+			html("/");
+		if (page) {
+			html(page);
+			html("/");
+			if (path)
+				html_attr(path);
+		}
+	} else {
+		html(cgit_script_name);
+		html("?url=");
+		html_attr(cgit_repo->url);
+		if (cgit_repo->url[strlen(cgit_repo->url) - 1] != '/')
+			html("/");
+		if (page) {
+			html(page);
+			html("/");
+			if (path)
+				html_attr(path);
+		}
+		delim = "&amp;";
+	}
+	if (head && strcmp(head, cgit_repo->defbranch)) {
+		html(delim);
+		html("h=");
+		html_attr(head);
+		delim = "&amp;";
+	}
+	return fmt("%s", delim);
+}
+
+static void reporevlink(char *page, char *name, char *title, char *class,
+			char *head, char *rev, char *path)
+{
+	char *delim;
+
+	delim = repolink(title, class, page, head, path);
+	if (rev && strcmp(rev, cgit_query_head)) {
+		html(delim);
+		html("id=");
+		html_attr(rev);
+	}
+	html("'>");
+	html_txt(name);
+	html("</a>");
+}
+
+void cgit_tree_link(char *name, char *title, char *class, char *head,
+		    char *rev, char *path)
+{
+	reporevlink("tree", name, title, class, head, rev, path);
+}
+
+void cgit_log_link(char *name, char *title, char *class, char *head,
+		   char *rev, char *path, int ofs)
+{
+	char *delim;
+
+	delim = repolink(title, class, "log", head, path);
+	if (rev && strcmp(rev, cgit_query_head)) {
+		html(delim);
+		html("id=");
+		html_attr(rev);
+		delim = "&";
+	}
+	if (ofs > 0) {
+		html(delim);
+		html("ofs=");
+		htmlf("%d", ofs);
+	}
+	html("'>");
+	html_txt(name);
+	html("</a>");
+}
+
+void cgit_commit_link(char *name, char *title, char *class, char *head,
+		      char *rev)
+{
+	if (strlen(name) > cgit_max_msg_len && cgit_max_msg_len >= 15) {
+		name[cgit_max_msg_len] = '\0';
+		name[cgit_max_msg_len - 1] = '.';
+		name[cgit_max_msg_len - 2] = '.';
+		name[cgit_max_msg_len - 3] = '.';
+	}
+	reporevlink("commit", name, title, class, head, rev, NULL);
+}
+
+void cgit_snapshot_link(char *name, char *title, char *class, char *head,
+			char *rev, char *archivename)
+{
+	reporevlink("snapshot", name, title, class, head, rev, archivename);
+}
+
+void cgit_diff_link(char *name, char *title, char *class, char *head,
+		    char *new_rev, char *old_rev, char *path)
+{
+	char *delim;
+
+	delim = repolink(title, class, "diff", head, path);
+	if (new_rev && strcmp(new_rev, cgit_query_head)) {
+		html(delim);
+		html("id=");
+		html_attr(new_rev);
+		delim = "&amp;";
+	}
+	if (old_rev) {
+		html(delim);
+		html("id2=");
+		html_attr(old_rev);
+	}
+	html("'>");
+	html_txt(name);
+	html("</a>");
+}
+
+void cgit_object_link(struct object *obj)
+{
+	char *page, *arg, *url;
+
+	if (obj->type == OBJ_COMMIT) {
+                cgit_commit_link(fmt("commit %s", sha1_to_hex(obj->sha1)), NULL, NULL,
+				 cgit_query_head, sha1_to_hex(obj->sha1));
+		return;
+	} else if (obj->type == OBJ_TREE) {
+		page = "tree";
+		arg = "id";
+	} else {
+		page = "blob";
+		arg = "id";
+	}
+
+	url = cgit_pageurl(cgit_query_repo, page,
+			   fmt("%s=%s", arg, sha1_to_hex(obj->sha1)));
+	html_link_open(url, NULL, NULL);
+	htmlf("%s %s", typename(obj->type),
+	      sha1_to_hex(obj->sha1));
+	html_link_close();
+}
 
 void cgit_print_date(time_t secs, char *format)
 {
@@ -152,7 +343,7 @@ void cgit_print_docstart(char *title, struct cacheitem *item)
 	html("<title>");
 	html_txt(title);
 	html("</title>\n");
-	htmlf("<meta name='generator' content='cgit v%s'/>\n", cgit_version);
+	htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version);
 	html("<link rel='stylesheet' type='text/css' href='");
 	html_attr(cgit_css);
 	html("'/>\n");
@@ -169,19 +360,38 @@ void cgit_print_docend()
 void cgit_print_pageheader(char *title, int show_search)
 {
 	html("<table id='layout'>");
-	html("<tr><td id='header'>");
-	html(cgit_root_title);
-	html("</td><td id='logo'>");
+	html("<tr><td id='header'><a href='");
+	html_attr(cgit_rooturl());
+	html("'>");
+	html_txt(cgit_root_title);
+	html("</a></td><td id='logo'>");
 	html("<a href='");
 	html_attr(cgit_logo_link);
 	htmlf("'><img src='%s' alt='logo'/></a>", cgit_logo);
 	html("</td></tr>");
 	html("<tr><td id='crumb'>");
-	htmlf("<a href='%s'>root</a>", cgit_rooturl());
 	if (cgit_query_repo) {
-		htmlf(" : <a href='%s'>", cgit_repourl(cgit_repo->url));
 		html_txt(cgit_repo->name);
-		htmlf("</a> : %s", title);
+		html(" (");
+		html_txt(cgit_query_head);
+		html(") : &nbsp;");
+		reporevlink(NULL, "summary", NULL, NULL, cgit_query_head,
+			    NULL, NULL);
+		html(" ");
+		cgit_log_link("log", NULL, NULL, cgit_query_head,
+			      cgit_query_sha1, cgit_query_path, 0);
+		html(" ");
+		cgit_tree_link("tree", NULL, NULL, cgit_query_head,
+			       cgit_query_sha1, NULL);
+		html(" ");
+		cgit_commit_link("commit", NULL, NULL, cgit_query_head,
+			      cgit_query_sha1);
+		html(" ");
+		cgit_diff_link("diff", NULL, NULL, cgit_query_head,
+			       cgit_query_sha1, cgit_query_sha2,
+			       cgit_query_path);
+	} else {
+		html_txt("Index of repositories");
 	}
 	html("</td>");
 	html("<td id='search'>");
@@ -219,3 +429,5 @@ void cgit_print_snapshot_start(const char *mimetype, const char *filename,
 					 ttl_seconds(item->ttl)));
 	html("\n");
 }
+
+/* vim:set sw=8: */
diff --git a/ui-snapshot.c b/ui-snapshot.c
index 2257d6b..bd34a28 100644
--- a/ui-snapshot.c
+++ b/ui-snapshot.c
@@ -8,40 +8,148 @@
 
 #include "cgit.h"
 
-static void cgit_print_zip(struct cacheitem *item, const char *hex, 
-			   const char *prefix, const char *filename)
+static int write_compressed_tar_archive(struct archiver_args *args,const char *filter)
 {
+	int rw[2];
+	pid_t gzpid;
+	int stdout2;
+	int status;
+	int rv;
+
+	stdout2 = chk_non_negative(dup(STDIN_FILENO), "Preserving STDOUT before compressing");
+	chk_zero(pipe(rw), "Opening pipe from compressor subprocess");
+	gzpid = chk_non_negative(fork(), "Forking compressor subprocess");
+	if(gzpid==0) {
+		/* child */
+		chk_zero(close(rw[1]), "Closing write end of pipe in child");
+		chk_zero(close(STDIN_FILENO), "Closing STDIN");
+		chk_non_negative(dup2(rw[0],STDIN_FILENO), "Redirecting compressor input to stdin");
+		execlp(filter,filter,NULL);
+		_exit(-1);
+	}
+	/* parent */
+	chk_zero(close(rw[0]), "Closing read end of pipe");
+	chk_non_negative(dup2(rw[1],STDOUT_FILENO), "Redirecting output to compressor");
+
+	rv = write_tar_archive(args);
+
+	chk_zero(close(STDOUT_FILENO), "Closing STDOUT redirected to compressor");
+	chk_non_negative(dup2(stdout2,STDOUT_FILENO), "Restoring uncompressed STDOUT");
+	chk_zero(close(stdout2), "Closing uncompressed STDOUT");
+	chk_zero(close(rw[1]), "Closing write end of pipe in parent");
+	chk_positive(waitpid(gzpid,&status,0), "Waiting on compressor process");
+	if(! ( WIFEXITED(status) && WEXITSTATUS(status)==0 ) )
+		cgit_print_error("Failed to compress archive");
+
+	return rv;
+}
+
+static int write_tar_gzip_archive(struct archiver_args *args)
+{
+	return write_compressed_tar_archive(args,"gzip");
+}
+
+static int write_tar_bzip2_archive(struct archiver_args *args)
+{
+	return write_compressed_tar_archive(args,"bzip2");
+}
+
+static const struct snapshot_archive_t {
+    	const char *suffix;
+	const char *mimetype;
+	write_archive_fn_t write_func;
+	int bit;
+}	snapshot_archives[] = {
+	{ ".zip", "application/x-zip", write_zip_archive, 0x1 },
+	{ ".tar.gz", "application/x-tar", write_tar_gzip_archive, 0x2 },
+	{ ".tar.bz2", "application/x-tar", write_tar_bzip2_archive, 0x4 },
+	{ ".tar", "application/x-tar", write_tar_archive, 0x8 }
+};
+
+#define snapshot_archives_len (sizeof(snapshot_archives) / sizeof(*snapshot_archives))
+
+void cgit_print_snapshot(struct cacheitem *item, const char *head,
+			 const char *hex, const char *prefix,
+			 const char *filename, int snapshots)
+{
+	const struct snapshot_archive_t* sat;
 	struct archiver_args args;
 	struct commit *commit;
 	unsigned char sha1[20];
+	int f, sl, fnl = strlen(filename);
 
-	if (get_sha1(hex, sha1)) {
-		cgit_print_error(fmt("Bad object id: %s", hex));
+	for(f=0; f<snapshot_archives_len; f++) {
+		sat = &snapshot_archives[f];
+		if(!(snapshots & sat->bit))
+			continue;
+		sl = strlen(sat->suffix);
+		if(fnl<sl || strcmp(&filename[fnl-sl],sat->suffix))
+			continue;
+		if (!hex)
+			hex = head;
+		if(get_sha1(hex, sha1)) {
+			cgit_print_error(fmt("Bad object id: %s", hex));
+			return;
+		}
+		commit = lookup_commit_reference(sha1);
+		if(!commit) {
+			cgit_print_error(fmt("Not a commit reference: %s", hex));
+			return;;
+		}
+		memset(&args,0,sizeof(args));
+		args.base = fmt("%s/", prefix);
+		args.tree = commit->tree;
+		cgit_print_snapshot_start(sat->mimetype, filename, item);
+		(*sat->write_func)(&args);
 		return;
 	}
-	commit = lookup_commit_reference(sha1);
+	cgit_print_error(fmt("Unsupported snapshot format: %s", filename));
+}
 
-	if (!commit) {
-		cgit_print_error(fmt("Not a commit reference: %s", hex));
-		return;
-	}
+void cgit_print_snapshot_links(const char *repo, const char *head,
+			       const char *hex, int snapshots)
+{
+	const struct snapshot_archive_t* sat;
+    	char *filename;
+	int f;
 
-	memset(&args, 0, sizeof(args));
-	args.base = fmt("%s/", prefix);
-	args.tree = commit->tree;
-	
-	cgit_print_snapshot_start("application/x-zip", filename, item);
-	write_zip_archive(&args);
+	for(f=0; f<snapshot_archives_len; f++) {
+		sat = &snapshot_archives[f];
+		if(!(snapshots & sat->bit))
+			continue;
+		filename = fmt("%s-%s%s", cgit_repobasename(repo), hex,
+			       sat->suffix);
+		cgit_snapshot_link(filename, NULL, NULL, (char *)head,
+				   (char *)hex, filename);
+		html("<br/>");
+	}
 }
 
-
-void cgit_print_snapshot(struct cacheitem *item, const char *hex, 
-			 const char *format, const char *prefix,
-			 const char *filename)
+int cgit_parse_snapshots_mask(const char *str)
 {
-	if (!strcmp(format, "zip"))
-		cgit_print_zip(item, hex, prefix, filename);
-	else
-		cgit_print_error(fmt("Unsupported snapshot format: %s", 
-				     format));
+	const struct snapshot_archive_t* sat;
+	static const char *delim = " \t,:/|;";
+	int f, tl, rv = 0;
+
+	/* favor legacy setting */
+	if(atoi(str))
+		return 1;
+	for(;;) {
+		str += strspn(str,delim);
+		tl = strcspn(str,delim);
+		if(!tl)
+			break;
+		for(f=0; f<snapshot_archives_len; f++) {
+			sat = &snapshot_archives[f];
+			if(!(strncmp(sat->suffix, str, tl) &&
+			     strncmp(sat->suffix+1, str, tl-1))) {
+				rv |= sat->bit;
+				break;
+			}
+		}
+		str += tl;
+	}
+	return rv;
 }
+
+/* vim:set sw=8: */
diff --git a/ui-summary.c b/ui-summary.c
index 4bda4c2..de8a180 100644
--- a/ui-summary.c
+++ b/ui-summary.c
@@ -15,8 +15,10 @@ static int cgit_print_branch_cb(const char *refname, const unsigned char *sha1,
 {
 	struct commit *commit;
 	struct commitinfo *info;
-	char buf[256], *url;
+	char buf[256];
+	char *ref;
 
+	ref = xstrdup(refname);
 	strncpy(buf, refname, sizeof(buf));
 	commit = lookup_commit(sha1);
 	// object is not really parsed at this point, because of some fallout
@@ -25,21 +27,13 @@ static int cgit_print_branch_cb(const char *refname, const unsigned char *sha1,
 	if (commit && !parse_commit(commit)){
 		info = cgit_parse_commit(commit);
 		html("<tr><td>");
-		url = cgit_pageurl(cgit_query_repo, "log",
-				   fmt("h=%s", refname));
-		html_link_open(url, NULL, NULL);
-		html_txt(buf);
-		html_link_close();
+		cgit_log_link(ref, NULL, NULL, ref, NULL, NULL, 0);
 		html("</td><td>");
 		cgit_print_age(commit->date, -1, NULL);
 		html("</td><td>");
 		html_txt(info->author);
 		html("</td><td>");
-		url = cgit_pageurl(cgit_query_repo, "commit",
-				   fmt("h=%s", sha1_to_hex(sha1)));
-		html_link_open(url, NULL, NULL);
-		html_ntxt(cgit_max_msg_len, info->subject);
-		html_link_close();
+		cgit_commit_link(info->subject, NULL, NULL, ref, NULL);
 		html("</td></tr>\n");
 		cgit_free_commitinfo(info);
 	} else {
@@ -49,33 +43,10 @@ static int cgit_print_branch_cb(const char *refname, const unsigned char *sha1,
 		htmlf("*** bad ref %s ***", sha1_to_hex(sha1));
 		html("</td></tr>\n");
 	}
+	free(ref);
 	return 0;
 }
 
-
-static void cgit_print_object_ref(struct object *obj)
-{
-	char *page, *arg, *url;
-
-	if (obj->type == OBJ_COMMIT) {
-		page = "commit";
-		arg = "h";
-	} else if (obj->type == OBJ_TREE) {
-		page = "tree";
-		arg = "id";
-	} else {
-		page = "view";
-		arg = "id";
-	}
-
-	url = cgit_pageurl(cgit_query_repo, page,
-			   fmt("%s=%s", arg, sha1_to_hex(obj->sha1)));
-	html_link_open(url, NULL, NULL);
-	htmlf("%s %s", typename(obj->type),
-	      sha1_to_hex(obj->sha1));
-	html_link_close();
-}
-
 static void print_tag_header()
 {
 	html("<tr class='nohover'><th class='left'>Tag</th>"
@@ -104,8 +75,8 @@ static int cgit_print_tag_cb(const char *refname, const unsigned char *sha1,
 		if (!header)
 			print_tag_header();
 		html("<tr><td>");
-		url = cgit_pageurl(cgit_query_repo, "view",
-				   fmt("id=%s", sha1_to_hex(sha1)));
+		url = cgit_pageurl(cgit_query_repo, "tag",
+				   fmt("id=%s", refname));
 		html_link_open(url, NULL, NULL);
 		html_txt(buf);
 		html_link_close();
@@ -116,7 +87,7 @@ static int cgit_print_tag_cb(const char *refname, const unsigned char *sha1,
 		if (info->tagger)
 			html(info->tagger);
 		html("</td><td>");
-		cgit_print_object_ref(tag->tagged);
+		cgit_object_link(tag->tagged);
 		html("</td></tr>\n");
 	} else {
 		if (!header)
@@ -124,7 +95,7 @@ static int cgit_print_tag_cb(const char *refname, const unsigned char *sha1,
 		html("<tr><td>");
 		html_txt(buf);
 		html("</td><td colspan='2'/><td>");
-		cgit_print_object_ref(obj);
+		cgit_object_link(obj);
 		html("</td></tr>\n");
 	}
 	return 0;
diff --git a/ui-tag.c b/ui-tag.c
new file mode 100644
index 0000000..6d761f3
--- /dev/null
+++ b/ui-tag.c
@@ -0,0 +1,74 @@
+/* ui-tag.c: display a tag
+ *
+ * Copyright (C) 2007 Lars Hjemli
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+
+
+static void print_tag_content(char *buf)
+{
+	char *p;
+
+	if (!buf)
+		return;
+
+	html("<div class='commit-subject'>");
+	p = strchr(buf, '\n');
+	if (p)
+		*p = '\0';
+	html_txt(buf);
+	html("</div>");
+	if (p) {
+		html("<div class='commit-msg'>");
+		html_txt(++p);
+		html("</div>");
+	}
+}
+
+void cgit_print_tag(char *revname)
+{
+	unsigned char sha1[20];
+	struct object *obj;
+	struct tag *tag;
+	struct taginfo *info;
+
+	if (get_sha1(revname, sha1)) {
+		cgit_print_error(fmt("Bad tag reference: %s", revname));
+		return;
+	}
+	obj = parse_object(sha1);
+	if (!obj) {
+		cgit_print_error(fmt("Bad object id: %s", sha1_to_hex(sha1)));
+		return;
+	}
+	if (obj->type == OBJ_TAG) {
+		tag = lookup_tag(sha1);
+		if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) {
+			cgit_print_error(fmt("Bad tag object: %s", revname));
+			return;
+		}
+		html("<table class='commit-info'>\n");
+		htmlf("<tr><td>Tag name</td><td>%s (%s)</td></tr>\n",
+		      revname, sha1_to_hex(sha1));
+		if (info->tagger_date > 0) {
+			html("<tr><td>Tag date</td><td>");
+			cgit_print_date(info->tagger_date, FMT_LONGDATE);
+			html("</td></tr>\n");
+		}
+		if (info->tagger) {
+			html("<tr><td>Tagged by</td><td>");
+			html_txt(info->tagger);
+			html("</td></tr>\n");
+		}
+		html("<tr><td>Tagged object</td><td>");
+		cgit_object_link(tag->tagged);
+		html("</td></tr>\n");
+		html("</table>\n");
+		print_tag_content(info->msg);
+	}
+	return;
+}
diff --git a/ui-tree.c b/ui-tree.c
index 21dd533..1cb09f7 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -9,16 +9,64 @@
 #include "cgit.h"
 
 char *curr_rev;
+char *match_path;
+int header = 0;
 
-static int print_entry(const unsigned char *sha1, const char *base,
-		       int baselen, const char *pathname, unsigned int mode,
-		       int stage)
+static void print_object(const unsigned char *sha1, char *path)
+{
+	enum object_type type;
+	unsigned char *buf;
+	unsigned long size, lineno, start, idx;
+
+	type = sha1_object_info(sha1, &size);
+	if (type == OBJ_BAD) {
+		cgit_print_error(fmt("Bad object name: %s",
+				     sha1_to_hex(sha1)));
+		return;
+	}
+
+	buf = read_sha1_file(sha1, &type, &size);
+	if (!buf) {
+		cgit_print_error(fmt("Error reading object %s",
+				     sha1_to_hex(sha1)));
+		return;
+	}
+
+	html(" blob: <a href='");
+	html_attr(cgit_pageurl(cgit_query_repo, "blob", fmt("id=%s", sha1_to_hex(sha1))));
+	htmlf("'>%s</a>",sha1_to_hex(sha1));
+
+	html("<table class='blob'>\n");
+	idx = 0;
+	start = 0;
+	lineno = 0;
+	while(idx < size) {
+		if (buf[idx] == '\n') {
+			buf[idx] = '\0';
+			htmlf("<tr><td class='no'><a name='%d'>%1$d</a></td><td class='txt'>",
+			      ++lineno);
+			html_txt(buf + start);
+			html("</td></tr>\n");
+			start = idx + 1;
+		}
+		idx++;
+	}
+	html("</table>\n");
+}
+
+
+static int ls_item(const unsigned char *sha1, const char *base, int baselen,
+		   const char *pathname, unsigned int mode, int stage)
 {
 	char *name;
+	char *fullpath;
 	enum object_type type;
 	unsigned long size = 0;
 
 	name = xstrdup(pathname);
+	fullpath = fmt("%s%s%s", cgit_query_path ? cgit_query_path : "",
+		       cgit_query_path ? "/" : "", name);
+
 	type = sha1_object_info(sha1, &size);
 	if (type == OBJ_BAD && !S_ISGITLINK(mode)) {
 		htmlf("<tr><td colspan='3'>Bad object: %s %s</td></tr>",
@@ -26,79 +74,140 @@ static int print_entry(const unsigned char *sha1, const char *base,
 		      sha1_to_hex(sha1));
 		return 0;
 	}
-	html("<tr><td class='filemode'>");
+
+	html("<tr><td class='ls-mode'>");
 	html_filemode(mode);
-	html("</td><td ");
+	html("</td><td>");
 	if (S_ISGITLINK(mode)) {
-		htmlf("class='ls-mod'><a href='");
+		htmlf("<a class='ls-mod' href='");
 		html_attr(fmt(cgit_repo->module_link,
 			      name,
 			      sha1_to_hex(sha1)));
+		html("'>");
+		html_txt(name);
+		html("</a>");
 	} else if (S_ISDIR(mode)) {
-		html("class='ls-dir'><a href='");
-		html_attr(cgit_pageurl(cgit_query_repo, "tree",
-				       fmt("h=%s&amp;id=%s&amp;path=%s%s/",
-					   curr_rev,
-					   sha1_to_hex(sha1),
-					   cgit_query_path ? cgit_query_path : "",
-					   pathname)));
+		cgit_tree_link(name, NULL, "ls-dir", cgit_query_head,
+			       curr_rev, fullpath);
 	} else {
-		html("class='ls-blob'><a href='");
-		html_attr(cgit_pageurl(cgit_query_repo, "view",
-				      fmt("h=%s&amp;id=%s&amp;path=%s%s", curr_rev,
-					  sha1_to_hex(sha1),
-					  cgit_query_path ? cgit_query_path : "",
-					  pathname)));
+		cgit_tree_link(name, NULL, "ls-blob", cgit_query_head,
+			       curr_rev, fullpath);
 	}
-	htmlf("'>%s</a></td>", name);
-	htmlf("<td class='filesize'>%li</td>", size);
-
-	html("<td class='links'><a href='");
-	html_attr(cgit_pageurl(cgit_query_repo, "log",
-			       fmt("h=%s&amp;path=%s%s",
-				   curr_rev,
-				   cgit_query_path ? cgit_query_path : "",
-				   pathname)));
-	html("'>history</a></td>");
-	html("</tr>\n");
+	htmlf("</td><td class='ls-size'>%li</td>", size);
+
+	html("<td>");
+	cgit_log_link("log", NULL, "button", cgit_query_head, curr_rev,
+		      fullpath, 0);
+	html("</td></tr>\n");
 	free(name);
 	return 0;
 }
 
-void cgit_print_tree(const char *rev, const char *hex, char *path)
+static void ls_head()
+{
+	html("<table class='list'>\n");
+	html("<tr class='nohover'>");
+	html("<th class='left'>Mode</th>");
+	html("<th class='left'>Name</th>");
+	html("<th class='right'>Size</th>");
+	html("<th/>");
+	html("</tr>\n");
+	header = 1;
+}
+
+static void ls_tail()
+{
+	if (!header)
+		return;
+	html("</table>\n");
+	header = 0;
+}
+
+static void ls_tree(const unsigned char *sha1, char *path)
 {
 	struct tree *tree;
+
+	tree = parse_tree_indirect(sha1);
+	if (!tree) {
+		cgit_print_error(fmt("Not a tree object: %s",
+				     sha1_to_hex(sha1)));
+		return;
+	}
+
+	ls_head();
+	read_tree_recursive(tree, "", 0, 1, NULL, ls_item);
+	ls_tail();
+}
+
+
+static int walk_tree(const unsigned char *sha1, const char *base, int baselen,
+		     const char *pathname, unsigned mode, int stage)
+{
+	static int state;
+	static char buffer[PATH_MAX];
+	char *url;
+
+	if (state == 0) {
+		memcpy(buffer, base, baselen);
+		strcpy(buffer+baselen, pathname);
+		url = cgit_pageurl(cgit_query_repo, "tree",
+				   fmt("h=%s&amp;path=%s", curr_rev, buffer));
+		html("/");
+		cgit_tree_link(xstrdup(pathname), NULL, NULL, cgit_query_head,
+			       curr_rev, buffer);
+
+		if (strcmp(match_path, buffer))
+			return READ_TREE_RECURSIVE;
+
+		if (S_ISDIR(mode)) {
+			state = 1;
+			ls_head();
+			return READ_TREE_RECURSIVE;
+		} else {
+			print_object(sha1, buffer);
+			return 0;
+		}
+	}
+	ls_item(sha1, base, baselen, pathname, mode, stage);
+	return 0;
+}
+
+
+/*
+ * Show a tree or a blob
+ *   rev:  the commit pointing at the root tree object
+ *   path: path to tree or blob
+ */
+void cgit_print_tree(const char *rev, char *path)
+{
 	unsigned char sha1[20];
 	struct commit *commit;
+	const char *paths[] = {path, NULL};
+
+	if (!rev)
+		rev = cgit_query_head;
 
 	curr_rev = xstrdup(rev);
-	get_sha1(rev, sha1);
+	if (get_sha1(rev, sha1)) {
+		cgit_print_error(fmt("Invalid revision name: %s", rev));
+		return;
+	}
 	commit = lookup_commit_reference(sha1);
 	if (!commit || parse_commit(commit)) {
-		cgit_print_error(fmt("Invalid head: %s", rev));
+		cgit_print_error(fmt("Invalid commit reference: %s", rev));
 		return;
 	}
-	if (!hex)
-		hex = sha1_to_hex(commit->tree->object.sha1);
 
-	if (get_sha1_hex(hex, sha1)) {
-		cgit_print_error(fmt("Invalid object id: %s", hex));
-		return;
-	}
-	tree = parse_tree_indirect(sha1);
-	if (!tree) {
-		cgit_print_error(fmt("Not a tree object: %s", hex));
+	html("path: <a href='");
+	html_attr(cgit_pageurl(cgit_query_repo, "tree", fmt("h=%s", rev)));
+	html("'>root</a>");
+
+	if (path == NULL) {
+		ls_tree(commit->tree->object.sha1, NULL);
 		return;
 	}
 
-	html_txt(path);
-	html("<table class='list'>\n");
-	html("<tr class='nohover'>");
-	html("<th class='left'>Mode</th>");
-	html("<th class='left'>Name</th>");
-	html("<th class='right'>Size</th>");
-	html("<th/>");
-	html("</tr>\n");
-	read_tree_recursive(tree, "", 0, 1, NULL, print_entry);
-	html("</table>\n");
+	match_path = path;
+	read_tree_recursive(commit->tree, NULL, 0, 0, paths, walk_tree);
+	ls_tail();
 }
diff --git a/ui-view.c b/ui-view.c
deleted file mode 100644
index 8873415..0000000
--- a/ui-view.c
+++ /dev/null
@@ -1,55 +0,0 @@
-/* ui-view.c: functions to output _any_ object, given it's sha1
- *
- * Copyright (C) 2006 Lars Hjemli
- *
- * Licensed under GNU General Public License v2
- *   (see COPYING for full license text)
- */
-
-#include "cgit.h"
-
-void cgit_print_view(const char *hex, char *path)
-{
-	unsigned char sha1[20];
-	enum object_type type;
-	unsigned char *buf;
-	unsigned long size;
-
-	if (get_sha1_hex(hex, sha1)){
-		cgit_print_error(fmt("Bad hex value: %s", hex));
-	        return;
-	}
-
-	type = sha1_object_info(sha1, &size);
-	if (type == OBJ_BAD) {
-		cgit_print_error(fmt("Bad object name: %s", hex));
-		return;
-	}
-
-	buf = read_sha1_file(sha1, &type, &size);
-	if (!buf) {
-		cgit_print_error(fmt("Error reading object %s", hex));
-		return;
-	}
-
-	buf[size] = '\0';
-	html("<table class='list'>\n");
-	html("<tr class='nohover'><th class='left'>");
-	if (path)
-		htmlf("%s (", path);
-	htmlf("%s %s, %li bytes", typename(type), hex, size);
-	if (path)
-		html(")");
-
-	html(" <a href='");
-	html_attr(cgit_pageurl(cgit_query_repo, "blob", 
-			       fmt("id=%s&amp;path=%s", 
-				   hex,
-				   path)));
-	html("'>download</a>");
-	html("</th></tr>\n");
-	html("<tr><td class='blob'>\n");
-	html_txt(buf);
-	html("\n</td></tr>\n");
-	html("</table>\n");
-}