about summary refs log tree commit diff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.gitignore5
-rw-r--r--Makefile21
-rw-r--r--cache.h2
-rw-r--r--cgit-doc.css3
-rw-r--r--cgit.c345
-rw-r--r--cgit.css10
-rw-r--r--cgit.h52
-rw-r--r--cgitrc.5.txt244
-rw-r--r--cmd.c2
-rwxr-xr-xfilters/commit-links.sh12
-rwxr-xr-xfilters/syntax-highlighting.sh39
m---------git0
-rw-r--r--scan-tree.c45
-rw-r--r--scan-tree.h2
-rw-r--r--shared.c62
-rw-r--r--ui-atom.c8
-rw-r--r--ui-blob.c8
-rw-r--r--ui-commit.c22
-rw-r--r--ui-log.c6
-rw-r--r--ui-patch.c6
-rw-r--r--ui-plain.c18
-rw-r--r--ui-refs.c19
-rw-r--r--ui-repolist.c65
-rw-r--r--ui-shared.c81
-rw-r--r--ui-shared.h1
-rw-r--r--ui-snapshot.c35
-rw-r--r--ui-stats.c8
-rw-r--r--ui-stats.h1
-rw-r--r--ui-summary.c28
-rw-r--r--ui-summary.h2
-rw-r--r--ui-tag.c2
-rw-r--r--ui-tree.c55
32 files changed, 942 insertions, 267 deletions
diff --git a/.gitignore b/.gitignore
index 1e016e5..487728b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,10 @@
 cgit
 cgit.conf
 VERSION
+cgitrc.5
+cgitrc.5.fo
+cgitrc.5.html
+cgitrc.5.pdf
+cgitrc.5.xml
 *.o
 *.d
diff --git a/Makefile b/Makefile
index 44138ea..da2518b 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH)
 CGIT_CONFIG = /etc/cgitrc
 CACHE_ROOT = /var/cache/cgit
 SHA1_HEADER = <openssl/sha.h>
-GIT_VER = 1.6.1.1
+GIT_VER = 1.6.4.3
 GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2
 INSTALL = install
 
@@ -100,7 +100,8 @@ ifdef NEEDS_LIBICONV
 endif
 
 
-.PHONY: all libgit test install uninstall clean force-version get-git
+.PHONY: all libgit test install uninstall clean force-version get-git \
+	doc man-doc html-doc clean-doc
 
 all: cgit
 
@@ -149,8 +150,22 @@ uninstall:
 	rm -f $(CGIT_DATA_PATH)/cgit.css
 	rm -f $(CGIT_DATA_PATH)/cgit.png
 
-clean:
+doc: man-doc html-doc pdf-doc
+
+man-doc: cgitrc.5.txt
+	a2x -f manpage cgitrc.5.txt
+
+html-doc: cgitrc.5.txt
+	a2x -f xhtml --stylesheet=cgit-doc.css cgitrc.5.txt
+
+pdf-doc: cgitrc.5.txt
+	a2x -f pdf cgitrc.5.txt
+
+clean: clean-doc
 	rm -f cgit VERSION *.o *.d
 
+clean-doc:
+	rm -f cgitrc.5 cgitrc.5.html cgitrc.5.pdf cgitrc.5.xml cgitrc.5.fo
+
 get-git:
 	curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git
diff --git a/cache.h b/cache.h
index 66cc41f..ac9276b 100644
--- a/cache.h
+++ b/cache.h
@@ -32,4 +32,6 @@ extern int cache_ls(const char *path);
 /* Print a message to stdout */
 extern void cache_log(const char *format, ...);
 
+extern unsigned long hash_str(const char *str);
+
 #endif /* CGIT_CACHE_H */
diff --git a/cgit-doc.css b/cgit-doc.css
new file mode 100644
index 0000000..5a399b6
--- /dev/null
+++ b/cgit-doc.css
@@ -0,0 +1,3 @@
+div.variablelist dt {
+	margin-top: 1em;
+}
diff --git a/cgit.c b/cgit.c
index 5301840..bd37788 100644
--- a/cgit.c
+++ b/cgit.c
@@ -17,9 +17,81 @@
 
 const char *cgit_version = CGIT_VERSION;
 
+void add_mimetype(const char *name, const char *value)
+{
+	struct string_list_item *item;
+
+	item = string_list_insert(xstrdup(name), &ctx.cfg.mimetypes);
+	item->util = xstrdup(value);
+}
+
+struct cgit_filter *new_filter(const char *cmd, int extra_args)
+{
+	struct cgit_filter *f;
+
+	if (!cmd || !cmd[0])
+		return NULL;
+
+	f = xmalloc(sizeof(struct cgit_filter));
+	f->cmd = xstrdup(cmd);
+	f->argv = xmalloc((2 + extra_args) * sizeof(char *));
+	f->argv[0] = f->cmd;
+	f->argv[1] = NULL;
+	return f;
+}
+
+static void process_cached_repolist(const char *path);
+
+void repo_config(struct cgit_repo *repo, const char *name, const char *value)
+{
+	if (!strcmp(name, "name"))
+		repo->name = xstrdup(value);
+	else if (!strcmp(name, "clone-url"))
+		repo->clone_url = xstrdup(value);
+	else if (!strcmp(name, "desc"))
+		repo->desc = xstrdup(value);
+	else if (!strcmp(name, "owner"))
+		repo->owner = xstrdup(value);
+	else if (!strcmp(name, "defbranch"))
+		repo->defbranch = xstrdup(value);
+	else if (!strcmp(name, "snapshots"))
+		repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value);
+	else if (!strcmp(name, "enable-log-filecount"))
+		repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
+	else if (!strcmp(name, "enable-log-linecount"))
+		repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
+	else if (!strcmp(name, "max-stats"))
+		repo->max_stats = cgit_find_stats_period(value, NULL);
+	else if (!strcmp(name, "module-link"))
+		repo->module_link= xstrdup(value);
+	else if (!strcmp(name, "section"))
+		repo->section = xstrdup(value);
+	else if (!strcmp(name, "readme") && value != NULL) {
+		if (*value == '/')
+			ctx.repo->readme = xstrdup(value);
+		else
+			ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
+	} else if (ctx.cfg.enable_filter_overrides) {
+		if (!strcmp(name, "about-filter"))
+			repo->about_filter = new_filter(value, 0);
+		else if (!strcmp(name, "commit-filter"))
+			repo->commit_filter = new_filter(value, 0);
+		else if (!strcmp(name, "source-filter"))
+			repo->source_filter = new_filter(value, 1);
+	}
+}
+
 void config_cb(const char *name, const char *value)
 {
-	if (!strcmp(name, "root-title"))
+	if (!strcmp(name, "section") || !strcmp(name, "repo.group"))
+		ctx.cfg.section = xstrdup(value);
+	else if (!strcmp(name, "repo.url"))
+		ctx.repo = cgit_add_repo(value);
+	else if (ctx.repo && !strcmp(name, "repo.path"))
+		ctx.repo->path = trim_end(value, '/');
+	else if (ctx.repo && !prefixcmp(name, "repo."))
+		repo_config(ctx.repo, name + 5, value);
+	else if (!strcmp(name, "root-title"))
 		ctx.cfg.root_title = xstrdup(value);
 	else if (!strcmp(name, "root-desc"))
 		ctx.cfg.root_desc = xstrdup(value);
@@ -31,6 +103,8 @@ void config_cb(const char *name, const char *value)
 		ctx.cfg.favicon = xstrdup(value);
 	else if (!strcmp(name, "footer"))
 		ctx.cfg.footer = xstrdup(value);
+	else if (!strcmp(name, "head-include"))
+		ctx.cfg.head_include = xstrdup(value);
 	else if (!strcmp(name, "header"))
 		ctx.cfg.header = xstrdup(value);
 	else if (!strcmp(name, "logo"))
@@ -49,14 +123,22 @@ void config_cb(const char *name, const char *value)
 			ctx.cfg.virtual_root = "";
 	} else if (!strcmp(name, "nocache"))
 		ctx.cfg.nocache = atoi(value);
+	else if (!strcmp(name, "noplainemail"))
+		ctx.cfg.noplainemail = atoi(value);
+	else if (!strcmp(name, "noheader"))
+		ctx.cfg.noheader = atoi(value);
 	else if (!strcmp(name, "snapshots"))
 		ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
+	else if (!strcmp(name, "enable-filter-overrides"))
+		ctx.cfg.enable_filter_overrides = atoi(value);
 	else if (!strcmp(name, "enable-index-links"))
 		ctx.cfg.enable_index_links = atoi(value);
 	else if (!strcmp(name, "enable-log-filecount"))
 		ctx.cfg.enable_log_filecount = atoi(value);
 	else if (!strcmp(name, "enable-log-linecount"))
 		ctx.cfg.enable_log_linecount = atoi(value);
+	else if (!strcmp(name, "enable-tree-linenumbers"))
+		ctx.cfg.enable_tree_linenumbers = atoi(value);
 	else if (!strcmp(name, "max-stats"))
 		ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
 	else if (!strcmp(name, "cache-size"))
@@ -67,10 +149,18 @@ void config_cb(const char *name, const char *value)
 		ctx.cfg.cache_root_ttl = atoi(value);
 	else if (!strcmp(name, "cache-repo-ttl"))
 		ctx.cfg.cache_repo_ttl = atoi(value);
+	else if (!strcmp(name, "cache-scanrc-ttl"))
+		ctx.cfg.cache_scanrc_ttl = atoi(value);
 	else if (!strcmp(name, "cache-static-ttl"))
 		ctx.cfg.cache_static_ttl = atoi(value);
 	else if (!strcmp(name, "cache-dynamic-ttl"))
 		ctx.cfg.cache_dynamic_ttl = atoi(value);
+	else if (!strcmp(name, "about-filter"))
+		ctx.cfg.about_filter = new_filter(value, 0);
+	else if (!strcmp(name, "commit-filter"))
+		ctx.cfg.commit_filter = new_filter(value, 0);
+	else if (!strcmp(name, "embedded"))
+		ctx.cfg.embedded = atoi(value);
 	else if (!strcmp(name, "max-message-length"))
 		ctx.cfg.max_msg_len = atoi(value);
 	else if (!strcmp(name, "max-repodesc-length"))
@@ -79,6 +169,13 @@ void config_cb(const char *name, const char *value)
 		ctx.cfg.max_repo_count = atoi(value);
 	else if (!strcmp(name, "max-commit-count"))
 		ctx.cfg.max_commit_count = atoi(value);
+	else if (!strcmp(name, "scan-path"))
+		if (!ctx.cfg.nocache && ctx.cfg.cache_size)
+			process_cached_repolist(value);
+		else
+			scan_tree(value, repo_config);
+	else if (!strcmp(name, "source-filter"))
+		ctx.cfg.source_filter = new_filter(value, 1);
 	else if (!strcmp(name, "summary-log"))
 		ctx.cfg.summary_log = atoi(value);
 	else if (!strcmp(name, "summary-branches"))
@@ -95,38 +192,9 @@ void config_cb(const char *name, const char *value)
 		ctx.cfg.clone_prefix = xstrdup(value);
 	else if (!strcmp(name, "local-time"))
 		ctx.cfg.local_time = atoi(value);
-	else if (!strcmp(name, "repo.group"))
-		ctx.cfg.repo_group = xstrdup(value);
-	else if (!strcmp(name, "repo.url"))
-		ctx.repo = cgit_add_repo(value);
-	else if (!strcmp(name, "repo.name"))
-		ctx.repo->name = xstrdup(value);
-	else if (ctx.repo && !strcmp(name, "repo.path"))
-		ctx.repo->path = trim_end(value, '/');
-	else if (ctx.repo && !strcmp(name, "repo.clone-url"))
-		ctx.repo->clone_url = xstrdup(value);
-	else if (ctx.repo && !strcmp(name, "repo.desc"))
-		ctx.repo->desc = xstrdup(value);
-	else if (ctx.repo && !strcmp(name, "repo.owner"))
-		ctx.repo->owner = xstrdup(value);
-	else if (ctx.repo && !strcmp(name, "repo.defbranch"))
-		ctx.repo->defbranch = xstrdup(value);
-	else if (ctx.repo && !strcmp(name, "repo.snapshots"))
-		ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */
-	else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount"))
-		ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
-	else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount"))
-		ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
-	else if (ctx.repo && !strcmp(name, "repo.max-stats"))
-		ctx.repo->max_stats = cgit_find_stats_period(value, NULL);
-	else if (ctx.repo && !strcmp(name, "repo.module-link"))
-		ctx.repo->module_link= xstrdup(value);
-	else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) {
-		if (*value == '/')
-			ctx.repo->readme = xstrdup(value);
-		else
-			ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value));
-	} else if (!strcmp(name, "include"))
+	else if (!prefixcmp(name, "mimetype."))
+		add_mimetype(name + 9, value);
+	else if (!strcmp(name, "include"))
 		parse_configfile(value, config_cb);
 }
 
@@ -173,6 +241,11 @@ static void querystring_cb(const char *name, const char *value)
 	}
 }
 
+char *xstrdupn(const char *str)
+{
+	return (str ? xstrdup(str) : NULL);
+}
+
 static void prepare_context(struct cgit_context *ctx)
 {
 	memset(ctx, 0, sizeof(ctx));
@@ -184,10 +257,12 @@ static void prepare_context(struct cgit_context *ctx)
 	ctx->cfg.cache_repo_ttl = 5;
 	ctx->cfg.cache_root = CGIT_CACHE_ROOT;
 	ctx->cfg.cache_root_ttl = 5;
+	ctx->cfg.cache_scanrc_ttl = 15;
 	ctx->cfg.cache_static_ttl = -1;
 	ctx->cfg.css = "/cgit.css";
-	ctx->cfg.logo = "/git-logo.png";
+	ctx->cfg.logo = "/cgit.png";
 	ctx->cfg.local_time = 0;
+	ctx->cfg.enable_tree_linenumbers = 1;
 	ctx->cfg.max_repo_count = 50;
 	ctx->cfg.max_commit_count = 50;
 	ctx->cfg.max_lock_attempts = 5;
@@ -200,15 +275,34 @@ static void prepare_context(struct cgit_context *ctx)
 	ctx->cfg.root_title = "Git repository browser";
 	ctx->cfg.root_desc = "a fast webinterface for the git dscm";
 	ctx->cfg.script_name = CGIT_SCRIPT_NAME;
+	ctx->cfg.section = "";
 	ctx->cfg.summary_branches = 10;
 	ctx->cfg.summary_log = 10;
 	ctx->cfg.summary_tags = 10;
+	ctx->env.cgit_config = xstrdupn(getenv("CGIT_CONFIG"));
+	ctx->env.http_host = xstrdupn(getenv("HTTP_HOST"));
+	ctx->env.https = xstrdupn(getenv("HTTPS"));
+	ctx->env.no_http = xstrdupn(getenv("NO_HTTP"));
+	ctx->env.path_info = xstrdupn(getenv("PATH_INFO"));
+	ctx->env.query_string = xstrdupn(getenv("QUERY_STRING"));
+	ctx->env.request_method = xstrdupn(getenv("REQUEST_METHOD"));
+	ctx->env.script_name = xstrdupn(getenv("SCRIPT_NAME"));
+	ctx->env.server_name = xstrdupn(getenv("SERVER_NAME"));
+	ctx->env.server_port = xstrdupn(getenv("SERVER_PORT"));
 	ctx->page.mimetype = "text/html";
 	ctx->page.charset = PAGE_ENCODING;
 	ctx->page.filename = NULL;
 	ctx->page.size = 0;
 	ctx->page.modified = time(NULL);
 	ctx->page.expires = ctx->page.modified;
+	ctx->page.etag = NULL;
+	memset(&ctx->cfg.mimetypes, 0, sizeof(struct string_list));
+	if (ctx->env.script_name)
+		ctx->cfg.script_name = ctx->env.script_name;
+	if (ctx->env.query_string)
+		ctx->qry.raw = ctx->env.query_string;
+	if (!ctx->env.cgit_config)
+		ctx->env.cgit_config = CGIT_CONFIG;
 }
 
 struct refmatch {
@@ -288,6 +382,8 @@ static int prepare_repo_cmd(struct cgit_context *ctx)
 	if (get_sha1(ctx->qry.head, sha1)) {
 		tmp = xstrdup(ctx->qry.head);
 		ctx->qry.head = ctx->repo->defbranch;
+		ctx->page.status = 404;
+		ctx->page.statusmsg = "not found";
 		cgit_print_http_headers(ctx);
 		cgit_print_docstart(ctx);
 		cgit_print_pageheader(ctx);
@@ -344,28 +440,151 @@ int cmp_repos(const void *a, const void *b)
 	return strcmp(ra->url, rb->url);
 }
 
-void print_repo(struct cgit_repo *repo)
+char *build_snapshot_setting(int bitmap)
 {
-	printf("repo.url=%s\n", repo->url);
-	printf("repo.name=%s\n", repo->name);
-	printf("repo.path=%s\n", repo->path);
+	const struct cgit_snapshot_format *f;
+	char *result = xstrdup("");
+	char *tmp;
+	int len;
+
+	for (f = cgit_snapshot_formats; f->suffix; f++) {
+		if (f->bit & bitmap) {
+			tmp = result;
+			result = xstrdup(fmt("%s%s ", tmp, f->suffix));
+			free(tmp);
+		}
+	}
+	len = strlen(result);
+	if (len)
+		result[len - 1] = '\0';
+	return result;
+}
+
+char *get_first_line(char *txt)
+{
+	char *t = xstrdup(txt);
+	char *p = strchr(t, '\n');
+	if (p)
+		*p = '\0';
+	return t;
+}
+
+void print_repo(FILE *f, struct cgit_repo *repo)
+{
+	fprintf(f, "repo.url=%s\n", repo->url);
+	fprintf(f, "repo.name=%s\n", repo->name);
+	fprintf(f, "repo.path=%s\n", repo->path);
 	if (repo->owner)
-		printf("repo.owner=%s\n", repo->owner);
-	if (repo->desc)
-		printf("repo.desc=%s\n", repo->desc);
+		fprintf(f, "repo.owner=%s\n", repo->owner);
+	if (repo->desc) {
+		char *tmp = get_first_line(repo->desc);
+		fprintf(f, "repo.desc=%s\n", tmp);
+		free(tmp);
+	}
 	if (repo->readme)
-		printf("repo.readme=%s\n", repo->readme);
-	printf("\n");
+		fprintf(f, "repo.readme=%s\n", repo->readme);
+	if (repo->defbranch)
+		fprintf(f, "repo.defbranch=%s\n", repo->defbranch);
+	if (repo->module_link)
+		fprintf(f, "repo.module-link=%s\n", repo->module_link);
+	if (repo->section)
+		fprintf(f, "repo.section=%s\n", repo->section);
+	if (repo->clone_url)
+		fprintf(f, "repo.clone-url=%s\n", repo->clone_url);
+	fprintf(f, "repo.enable-log-filecount=%d\n",
+	        repo->enable_log_filecount);
+	fprintf(f, "repo.enable-log-linecount=%d\n",
+	        repo->enable_log_linecount);
+	if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter)
+		fprintf(f, "repo.about-filter=%s\n", repo->about_filter->cmd);
+	if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter)
+		fprintf(f, "repo.commit-filter=%s\n", repo->commit_filter->cmd);
+	if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter)
+		fprintf(f, "repo.source-filter=%s\n", repo->source_filter->cmd);
+	if (repo->snapshots != ctx.cfg.snapshots) {
+		char *tmp = build_snapshot_setting(repo->snapshots);
+		fprintf(f, "repo.snapshots=%s\n", tmp);
+		free(tmp);
+	}
+	if (repo->max_stats != ctx.cfg.max_stats)
+		fprintf(f, "repo.max-stats=%s\n",
+		        cgit_find_stats_periodname(repo->max_stats));
+	fprintf(f, "\n");
 }
 
-void print_repolist(struct cgit_repolist *list)
+void print_repolist(FILE *f, struct cgit_repolist *list, int start)
 {
 	int i;
 
-	for(i = 0; i < list->count; i++)
-		print_repo(&list->repos[i]);
+	for(i = start; i < list->count; i++)
+		print_repo(f, &list->repos[i]);
+}
+
+/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc'
+ * and return 0 on success.
+ */
+static int generate_cached_repolist(const char *path, const char *cached_rc)
+{
+	char *locked_rc;
+	int idx;
+	FILE *f;
+
+	locked_rc = xstrdup(fmt("%s.lock", cached_rc));
+	f = fopen(locked_rc, "wx");
+	if (!f) {
+		/* Inform about the error unless the lockfile already existed,
+		 * since that only means we've got concurrent requests.
+		 */
+		if (errno != EEXIST)
+			fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n",
+				locked_rc, strerror(errno), errno);
+		return errno;
+	}
+	idx = cgit_repolist.count;
+	scan_tree(path, repo_config);
+	print_repolist(f, &cgit_repolist, idx);
+	if (rename(locked_rc, cached_rc))
+		fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n",
+			locked_rc, cached_rc, strerror(errno), errno);
+	fclose(f);
+	return 0;
 }
 
+static void process_cached_repolist(const char *path)
+{
+	struct stat st;
+	char *cached_rc;
+	time_t age;
+
+	cached_rc = xstrdup(fmt("%s/rc-%8x", ctx.cfg.cache_root,
+		hash_str(path)));
+
+	if (stat(cached_rc, &st)) {
+		/* Nothing is cached, we need to scan without forking. And
+		 * if we fail to generate a cached repolist, we need to
+		 * invoke scan_tree manually.
+		 */
+		if (generate_cached_repolist(path, cached_rc))
+			scan_tree(path, repo_config);
+		return;
+	}
+
+	parse_configfile(cached_rc, config_cb);
+
+	/* If the cached configfile hasn't expired, lets exit now */
+	age = time(NULL) - st.st_mtime;
+	if (age <= (ctx.cfg.cache_scanrc_ttl * 60))
+		return;
+
+	/* The cached repolist has been parsed, but it was old. So lets
+	 * rescan the specified path and generate a new cached repolist
+	 * in a child-process to avoid latency for the current request.
+	 */
+	if (fork())
+		return;
+
+	exit(generate_cached_repolist(path, cached_rc));
+}
 
 static void cgit_parse_args(int argc, const char **argv)
 {
@@ -379,6 +598,9 @@ static void cgit_parse_args(int argc, const char **argv)
 		if (!strcmp(argv[i], "--nocache")) {
 			ctx.cfg.nocache = 1;
 		}
+		if (!strcmp(argv[i], "--nohttp")) {
+			ctx.env.no_http = "1";
+		}
 		if (!strncmp(argv[i], "--query=", 8)) {
 			ctx.qry.raw = xstrdup(argv[i]+8);
 		}
@@ -399,15 +621,26 @@ static void cgit_parse_args(int argc, const char **argv)
 		if (!strncmp(argv[i], "--ofs=", 6)) {
 			ctx.qry.ofs = atoi(argv[i]+6);
 		}
-		if (!strncmp(argv[i], "--scan-tree=", 12)) {
+		if (!strncmp(argv[i], "--scan-tree=", 12) ||
+		    !strncmp(argv[i], "--scan-path=", 12)) {
+			/* HACK: the global snapshot bitmask defines the
+			 * set of allowed snapshot formats, but the config
+			 * file hasn't been parsed yet so the mask is
+			 * currently 0. By setting all bits high before
+			 * scanning we make sure that any in-repo cgitrc
+			 * snapshot setting is respected by scan_tree().
+			 * BTW: we assume that there'll never be more than
+			 * 255 different snapshot formats supported by cgit...
+			 */
+			ctx.cfg.snapshots = 0xFF;
 			scan++;
-			scan_tree(argv[i] + 12);
+			scan_tree(argv[i] + 12, repo_config);
 		}
 	}
 	if (scan) {
 		qsort(cgit_repolist.repos, cgit_repolist.count,
 			sizeof(struct cgit_repo), cmp_repos);
-		print_repolist(&cgit_repolist);
+		print_repolist(stdout, &cgit_repolist, 0);
 		exit(0);
 	}
 }
@@ -431,7 +664,6 @@ static int calc_ttl()
 
 int main(int argc, const char **argv)
 {
-	const char *cgit_config_env = getenv("CGIT_CONFIG");
 	const char *path;
 	char *qry;
 	int err, ttl;
@@ -441,13 +673,8 @@ int main(int argc, const char **argv)
 	cgit_repolist.count = 0;
 	cgit_repolist.repos = NULL;
 
-	if (getenv("SCRIPT_NAME"))
-		ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME"));
-	if (getenv("QUERY_STRING"))
-		ctx.qry.raw = xstrdup(getenv("QUERY_STRING"));
 	cgit_parse_args(argc, argv);
-	parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG,
-			 config_cb);
+	parse_configfile(ctx.env.cgit_config, config_cb);
 	ctx.repo = NULL;
 	http_parse_querystring(ctx.qry.raw, querystring_cb);
 
@@ -462,7 +689,7 @@ int main(int argc, const char **argv)
 	 * urls without the need for rewriterules in the webserver (as
 	 * long as PATH_INFO is included in the cache lookup key).
 	 */
-	path = getenv("PATH_INFO");
+	path = ctx.env.path_info;
 	if (!ctx.qry.url && path) {
 		if (path[0] == '/')
 			path++;
@@ -472,12 +699,14 @@ int main(int argc, const char **argv)
 			ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
 			free(qry);
 		} else
-			ctx.qry.raw = ctx.qry.url;
+			ctx.qry.raw = xstrdup(ctx.qry.url);
 		cgit_parse_url(ctx.qry.url);
 	}
 
 	ttl = calc_ttl();
 	ctx.page.expires += ttl*60;
+	if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
+		ctx.cfg.nocache = 1;
 	if (ctx.cfg.nocache)
 		ctx.cfg.cache_size = 0;
 	err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
diff --git a/cgit.css b/cgit.css
index adfc8ae..c47ebc9 100644
--- a/cgit.css
+++ b/cgit.css
@@ -155,7 +155,7 @@ table.list td.logsubject {
 table.list td.logmsg {
 	font-family: monospace;
 	white-space: pre;
-	padding: 1em 0em 2em 0em;
+	padding: 1em 0.5em 2em 0.5em;
 }
 
 table.list td a {
@@ -237,16 +237,16 @@ table.blob {
 }
 
 table.blob td.lines {
-	margin: 0; padding: 0;
+	margin: 0; padding: 0 0 0 0.5em;
 	vertical-align: top;
 	color: black;
 }
 
 table.blob td.linenumbers {
-	margin: 0; padding: 0;
+	margin: 0; padding: 0 0.5em 0 0.5em;
 	vertical-align: top;
+	text-align: right;
 	border-right: 1px solid gray;
-	background-color: #eee;
 }
 
 table.blob pre {
@@ -429,7 +429,7 @@ table.diff td div.del {
 	text-align: right;
 }
 
-table.list td.repogroup {
+table.list td.reposection {
 	font-style: italic;
 	color: #888;
 }
diff --git a/cgit.h b/cgit.h
index 5f7af51..6c6c460 100644
--- a/cgit.h
+++ b/cgit.h
@@ -15,6 +15,7 @@
 #include <revision.h>
 #include <log-tree.h>
 #include <archive.h>
+#include <string-list.h>
 #include <xdiff-interface.h>
 #include <xdiff/xdiff.h>
 #include <utf8.h>
@@ -48,6 +49,15 @@ typedef void (*configfn)(const char *name, const char *value);
 typedef void (*filepair_fn)(struct diff_filepair *pair);
 typedef void (*linediff_fn)(char *line, int len);
 
+struct cgit_filter {
+	char *cmd;
+	char **argv;
+	int old_stdout;
+	int pipe_fh[2];
+	int pid;
+	int exitstatus;
+};
+
 struct cgit_repo {
 	char *url;
 	char *name;
@@ -55,17 +65,23 @@ struct cgit_repo {
 	char *desc;
 	char *owner;
 	char *defbranch;
-	char *group;
 	char *module_link;
 	char *readme;
+	char *section;
 	char *clone_url;
 	int snapshots;
 	int enable_log_filecount;
 	int enable_log_linecount;
 	int max_stats;
 	time_t mtime;
+	struct cgit_filter *about_filter;
+	struct cgit_filter *commit_filter;
+	struct cgit_filter *source_filter;
 };
 
+typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name,
+	      const char *value);
+
 struct cgit_repolist {
 	int length;
 	int count;
@@ -136,28 +152,33 @@ struct cgit_config {
 	char *css;
 	char *favicon;
 	char *footer;
+	char *head_include;
 	char *header;
 	char *index_header;
 	char *index_info;
 	char *logo;
 	char *logo_link;
 	char *module_link;
-	char *repo_group;
 	char *robots;
 	char *root_title;
 	char *root_desc;
 	char *root_readme;
 	char *script_name;
+	char *section;
 	char *virtual_root;
 	int cache_size;
 	int cache_dynamic_ttl;
 	int cache_max_create_time;
 	int cache_repo_ttl;
 	int cache_root_ttl;
+	int cache_scanrc_ttl;
 	int cache_static_ttl;
+	int embedded;
+	int enable_filter_overrides;
 	int enable_index_links;
 	int enable_log_filecount;
 	int enable_log_linecount;
+	int enable_tree_linenumbers;
 	int local_time;
 	int max_repo_count;
 	int max_commit_count;
@@ -166,11 +187,17 @@ struct cgit_config {
 	int max_repodesc_len;
 	int max_stats;
 	int nocache;
+	int noplainemail;
+	int noheader;
 	int renamelimit;
 	int snapshots;
 	int summary_branches;
 	int summary_log;
 	int summary_tags;
+	struct string_list mimetypes;
+	struct cgit_filter *about_filter;
+	struct cgit_filter *commit_filter;
+	struct cgit_filter *source_filter;
 };
 
 struct cgit_page {
@@ -180,10 +207,27 @@ struct cgit_page {
 	char *mimetype;
 	char *charset;
 	char *filename;
+	char *etag;
 	char *title;
+	int status;
+	char *statusmsg;
+};
+
+struct cgit_environment {
+	char *cgit_config;
+	char *http_host;
+	char *https;
+	char *no_http;
+	char *path_info;
+	char *query_string;
+	char *request_method;
+	char *script_name;
+	char *server_name;
+	char *server_port;
 };
 
 struct cgit_context {
+	struct cgit_environment env;
 	struct cgit_query qry;
 	struct cgit_config cfg;
 	struct cgit_repo *repo;
@@ -242,5 +286,9 @@ extern const char *cgit_repobasename(const char *reponame);
 
 extern int cgit_parse_snapshots_mask(const char *str);
 
+extern int cgit_open_filter(struct cgit_filter *filter);
+extern int cgit_close_filter(struct cgit_filter *filter);
+
+extern int readfile(const char *path, char **buf, size_t *size);
 
 #endif /* CGIT_H */
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index fd299ae..4dc383d 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -1,203 +1,269 @@
-CGITRC
-======
+CGITRC(5)
+========
 
 
 NAME
 ----
-	cgitrc - runtime configuration for cgit
+cgitrc - runtime configuration for cgit
 
 
-DESCRIPTION
------------
+SYNOPSIS
+--------
 Cgitrc contains all runtime settings for cgit, including the list of git
 repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank
 lines, and lines starting with '#', are ignored.
 
 
+LOCATION
+--------
+The default location of cgitrc, defined at compile time, is /etc/cgitrc. At
+runtime, cgit will consult the environment variable CGIT_CONFIG and, if
+defined, use its value instead.
+
+
 GLOBAL SETTINGS
 ---------------
-agefile
+about-filter::
+	Specifies a command which will be invoked to format the content of
+	about pages (both top-level and for each repository). The command will
+	get the content of the about-file on its STDIN, and the STDOUT from the
+	command will be included verbatim on the about page. Default value:
+	none.
+
+agefile::
 	Specifies a path, relative to each repository path, which can be used
 	to specify the date and time of the youngest commit in the repository.
 	The first line in the file is used as input to the "parse_date"
 	function in libgit. Recommended timestamp-format is "yyyy-mm-dd
 	hh:mm:ss". Default value: "info/web/last-modified".
 
-cache-root
+cache-root::
 	Path used to store the cgit cache entries. Default value:
 	"/var/cache/cgit".
 
-cache-dynamic-ttl
+cache-dynamic-ttl::
 	Number which specifies the time-to-live, in minutes, for the cached
 	version of repository pages accessed without a fixed SHA1. Default
 	value: "5".
 
-cache-repo-ttl
+cache-repo-ttl::
 	Number which specifies the time-to-live, in minutes, for the cached
 	version of the repository summary page. Default value: "5".
 
-cache-root-ttl
+cache-root-ttl::
 	Number which specifies the time-to-live, in minutes, for the cached
 	version of the repository index page. Default value: "5".
 
-cache-size
+cache-scanrc-ttl::
+	Number which specifies the time-to-live, in minutes, for the result
+	of scanning a path for git repositories. Default value: "15".
+
+cache-size::
 	The maximum number of entries in the cgit cache. Default value: "0"
 	(i.e. caching is disabled).
 
-cache-static-ttl
+cache-static-ttl::
 	Number which specifies the time-to-live, in minutes, for the cached
 	version of repository pages accessed with a fixed SHA1. Default value:
 	"5".
 
-clone-prefix
+clone-prefix::
 	Space-separated list of common prefixes which, when combined with a
 	repository url, generates valid clone urls for the repository. This
 	setting is only used if `repo.clone-url` is unspecified. Default value:
 	none.
 
-css
+commit-filter::
+	Specifies a command which will be invoked to format commit messages.
+	The command will get the message on its STDIN, and the STDOUT from the
+	command will be included verbatim as the commit message, i.e. this can
+	be used to implement bugtracker integration. Default value: none.
+
+css::
 	Url which specifies the css document to include in all cgit pages.
 	Default value: "/cgit.css".
 
-enable-index-links
+embedded::
+	Flag which, when set to "1", will make cgit generate a html fragment
+	suitable for embedding in other html pages. Default value: none. See
+	also: "noheader".
+	
+enable-filter-overrides::
+	Flag which, when set to "1", allows all filter settings to be
+	overridden in repository-specific cgitrc files. Default value: none.
+
+enable-index-links::
 	Flag which, when set to "1", will make cgit generate extra links for
 	each repo in the repository index (specifically, to the "summary",
 	"commit" and "tree" pages). Default value: "0".
 
-enable-log-filecount
+enable-log-filecount::
 	Flag which, when set to "1", will make cgit print the number of
 	modified files for each commit on the repository log page. Default
 	value: "0".
 
-enable-log-linecount
+enable-log-linecount::
 	Flag which, when set to "1", will make cgit print the number of added
 	and removed lines for each commit on the repository log page. Default
 	value: "0".
 
-favicon
+enable-tree-linenumbers::
+	Flag which, when set to "1", will make cgit generate linenumber links
+	for plaintext blobs printed in the tree view. Default value: "1".
+
+favicon::
 	Url used as link to a shortcut icon for cgit. If specified, it is
 	suggested to use the value "/favicon.ico" since certain browsers will
 	ignore other values. Default value: none.
 
-footer
+footer::
 	The content of the file specified with this option will be included
 	verbatim at the bottom of all pages (i.e. it replaces the standard
 	"generated by..." message. Default value: none.
 
-header
+head-include::
+	The content of the file specified with this option will be included
+	verbatim in the html HEAD section on all pages. Default value: none.
+
+header::
 	The content of the file specified with this option will be included
 	verbatim at the top of all pages. Default value: none.
 
-include
+include::
 	Name of a configfile to include before the rest of the current config-
 	file is parsed. Default value: none.
 
-index-header
+index-header::
 	The content of the file specified with this option will be included
 	verbatim above the repository index. This setting is deprecated, and
 	will not be supported by cgit-1.0 (use root-readme instead). Default
 	value: none.
 
-index-info
+index-info::
 	The content of the file specified with this option will be included
 	verbatim below the heading on the repository index page. This setting
 	is deprecated, and will not be supported by cgit-1.0 (use root-desc
 	instead). Default value: none.
 
-local-time
+local-time::
 	Flag which, if set to "1", makes cgit print commit and tag times in the
 	servers timezone. Default value: "0".
 
-logo
+logo::
 	Url which specifies the source of an image which will be used as a logo
-	on all cgit pages.
+	on all cgit pages. Default value: "/cgit.png".
 
-logo-link
+logo-link::
 	Url loaded when clicking on the cgit logo image. If unspecified the
 	calculated url of the repository index page will be used. Default
 	value: none.
 
-max-commit-count
+max-commit-count::
 	Specifies the number of entries to list per page in "log" view. Default
 	value: "50".
 
-max-message-length
+max-message-length::
 	Specifies the maximum number of commit message characters to display in
 	"log" view. Default value: "80".
 
-max-repo-count
+max-repo-count::
 	Specifies the number of entries to list per page on the	repository
 	index page. Default value: "50".
 
-max-repodesc-length
+max-repodesc-length::
 	Specifies the maximum number of repo description characters to display
 	on the repository index page. Default value: "80".
 
-max-stats
+max-stats::
 	Set the default maximum statistics period. Valid values are "week",
 	"month", "quarter" and "year". If unspecified, statistics are
 	disabled. Default value: none. See also: "repo.max-stats".
 
-module-link
+mimetype.<ext>::
+	Set the mimetype for the specified filename extension. This is used
+	by the `plain` command when returning blob content.
+
+module-link::
 	Text which will be used as the formatstring for a hyperlink when a
 	submodule is printed in a directory listing. The arguments for the
 	formatstring are the path and SHA1 of the submodule commit. Default
 	value: "./?repo=%s&page=commit&id=%s"
 
-nocache
+nocache::
 	If set to the value "1" caching will be disabled. This settings is
 	deprecated, and will not be honored starting with cgit-1.0. Default
 	value: "0".
 
-renamelimit
+noplainemail::
+	If set to "1" showing full author email adresses will be disabled.
+	Default value: "0".
+
+noheader::
+	Flag which, when set to "1", will make cgit omit the standard header
+	on all pages. Default value: none. See also: "embedded".
+
+renamelimit::
 	Maximum number of files to consider when detecting renames. The value
 	 "-1" uses the compiletime value in git (for further info, look at
 	  `man git-diff`). Default value: "-1".
 
-repo.group
-	A value for the current repository group, which all repositories
-	specified after this setting will inherit. Default value: none.
+repo.group::
+	Legacy alias for "section". This option is deprecated and will not be
+	supported in cgit-1.0.
 
-robots
+robots::
 	Text used as content for the "robots" meta-tag. Default value:
 	"index, nofollow".
 
-root-desc
+root-desc::
 	Text printed below the heading on the repository index page. Default
 	value: "a fast webinterface for the git dscm".
 
-root-readme:
+root-readme::
 	The content of the file specified with this option will be included
 	verbatim below the "about" link on the repository index page. Default
 	value: none.
 
-root-title
+root-title::
 	Text printed as heading on the repository index page. Default value:
 	"Git Repository Browser".
 
-snapshots
-	Text which specifies the default (and allowed) set of snapshot formats
-	supported by cgit. The value is a space-separated list of zero or more
-	of the following values:
-		"tar"		uncompressed tar-file
-		"tar.gz"	gzip-compressed tar-file
-		"tar.bz2"	bzip-compressed tar-file
-		"zip"		zip-file
-	Default value: none.
+scan-path::
+	A path which will be scanned for repositories. If caching is enabled,
+	the result will be cached as a cgitrc include-file in the cache
+	directory. Default value: none. See also: cache-scanrc-ttl.
+
+section::
+	The name of the current repository section - all repositories defined
+	after this option will inherit the current section name. Default value:
+	none.
 
-summary-branches
+snapshots::
+	Text which specifies the default set of snapshot formats generated by
+	cgit. The value is a space-separated list of zero or more of the
+	values "tar", "tar.gz", "tar.bz2" and "zip". Default value: none.
+
+source-filter::
+	Specifies a command which will be invoked to format plaintext blobs
+	in the tree view. The command will get the blob content on its STDIN
+	and the name of the blob as its only command line argument. The STDOUT
+	from the command will be included verbatim as the blob contents, i.e.
+	this can be used to implement e.g. syntax highlighting. Default value:
+	none.
+
+summary-branches::
 	Specifies the number of branches to display in the repository "summary"
 	view. Default value: "10".
 
-summary-log
+summary-log::
 	Specifies the number of log entries to display in the repository
 	"summary" view. Default value: "10".
 
-summary-tags
+summary-tags::
 	Specifies the number of tags to display in the repository "summary"
 	view. Default value: "10".
 
-virtual-root
+virtual-root::
 	Url which, if specified, will be used as root for all cgit links. It
 	will also cause cgit to generate 'virtual urls', i.e. urls like
 	'/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default
@@ -207,58 +273,88 @@ virtual-root
 
 REPOSITORY SETTINGS
 -------------------
-repo.clone-url
+repo.about-filter::
+	Override the default about-filter. Default value: none. See also:
+	"enable-filter-overrides".
+
+repo.clone-url::
 	A list of space-separated urls which can be used to clone this repo.
 	Default value: none.
 
-repo.defbranch
+repo.commit-filter::
+	Override the default commit-filter. Default value: none. See also:
+	"enable-filter-overrides".
+
+repo.defbranch::
 	The name of the default branch for this repository. If no such branch
 	exists in the repository, the first branch name (when sorted) is used
 	as default instead. Default value: "master".
 
-repo.desc
+repo.desc::
 	The value to show as repository description. Default value: none.
 
-repo.enable-log-filecount
+repo.enable-log-filecount::
 	A flag which can be used to disable the global setting
 	`enable-log-filecount'. Default value: none.
 
-repo.enable-log-linecount
+repo.enable-log-linecount::
 	A flag which can be used to disable the global setting
 	`enable-log-linecount'. Default value: none.
 
-repo.max-stats
+repo.max-stats::
 	Override the default maximum statistics period. Valid values are equal
 	to the values specified for the global "max-stats" setting. Default
 	value: none.
 
-repo.name
+repo.name::
 	The value to show as repository name. Default value: <repo.url>.
 
-repo.owner
+repo.owner::
 	A value used to identify the owner of the repository. Default value:
 	none.
 
-repo.path
+repo.path::
 	An absolute path to the repository directory. For non-bare repositories
 	this is the .git-directory. Default value: none.
 
-repo.readme
+repo.readme::
 	A path (relative to <repo.path>) which specifies a file to include
 	verbatim as the "About" page for this repo. Default value: none.
 
-repo.snapshots
+repo.snapshots::
 	A mask of allowed snapshot-formats for this repo, restricted by the
 	"snapshots" global setting. Default value: <snapshots>.
 
-repo.url
+repo.section::
+	Override the current section name for this repository. Default value:
+	none.
+
+repo.source-filter::
+	Override the default source-filter. Default value: none. See also:
+	"enable-filter-overrides".
+
+repo.url::
 	The relative url used to access the repository. This must be the first
 	setting specified for each repo. Default value: none.
 
 
+REPOSITORY-SPECIFIC CGITRC FILE
+-------------------------------
+When the option "scan-path" is used to auto-discover git repositories, cgit
+will try to parse the file "cgitrc" within any found repository. Such a
+repo-specific config file may contain any of the repo-specific options
+described above, except "repo.url" and "repo.path". Additionally, the "filter"
+options are only acknowledged in repo-specific config files when
+"enable-filter-overrides" is set to "1".
+
+Note: the "repo." prefix is dropped from the option names in repo-specific
+config files, e.g. "repo.desc" becomes "desc".
+
+
 EXAMPLE CGITRC FILE
 -------------------
 
+....
 # Enable caching of up to 1000 output entriess
 cache-size=1000
 
@@ -311,6 +407,19 @@ snapshots=tar.gz tar.bz2 zip
 
 
 ##
+## List of common mimetypes
+##
+
+mimetype.git=image/git
+mimetype.html=text/html
+mimetype.jpg=image/jpeg
+mimetype.jpeg=image/jpeg
+mimetype.pdf=application/pdf
+mimetype.png=image/png
+mimetype.svg=image/svg+xml
+
+
+##
 ## List of repositories.
 ## PS: Any repositories listed when repo.group is unset will not be
 ##     displayed under a group heading
@@ -368,6 +477,7 @@ repo.enable-log-linecount=0
 
 # Restrict the max statistics period for this repo
 repo.max-stats=month
+....
 
 
 BUGS
diff --git a/cmd.c b/cmd.c
index cf97da7..766f903 100644
--- a/cmd.c
+++ b/cmd.c
@@ -39,7 +39,7 @@ static void atom_fn(struct cgit_context *ctx)
 static void about_fn(struct cgit_context *ctx)
 {
 	if (ctx->repo)
-		cgit_print_repo_readme();
+		cgit_print_repo_readme(ctx->qry.path);
 	else
 		cgit_print_site_readme();
 }
diff --git a/filters/commit-links.sh b/filters/commit-links.sh
new file mode 100755
index 0000000..165a533
--- /dev/null
+++ b/filters/commit-links.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+# This script can be used to generate links in commit messages - the first
+# sed expression generates links to commits referenced by their SHA1, while
+# the second expression generates links to a fictional bugtracker.
+#
+# To use this script, refer to this file with either the commit-filter or the
+# repo.commit-filter options in cgitrc.
+
+sed -re '
+s|\b([0-9a-fA-F]{8,40})\b|<a href="./?id=\1">\1</a>|g
+s| #([0-9]+)\b|<a href="http://bugs.example.com/?bug=\1">#\1</a>|g
+'
diff --git a/filters/syntax-highlighting.sh b/filters/syntax-highlighting.sh
new file mode 100755
index 0000000..999ad0c
--- /dev/null
+++ b/filters/syntax-highlighting.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+# This script can be used to implement syntax highlighting in the cgit
+# tree-view by refering to this file with the source-filter or repo.source-
+# filter options in cgitrc.
+#
+# Note: the highlight command (http://www.andre-simon.de/) uses css for syntax
+# highlighting, so you'll probably want something like the following included
+# in your css file (generated by highlight 2.4.8 and adapted for cgit):
+#
+# table.blob .num  { color:#2928ff; }
+# table.blob .esc  { color:#ff00ff; }
+# table.blob .str  { color:#ff0000; }
+# table.blob .dstr { color:#818100; }
+# table.blob .slc  { color:#838183; font-style:italic; }
+# table.blob .com  { color:#838183; font-style:italic; }
+# table.blob .dir  { color:#008200; }
+# table.blob .sym  { color:#000000; }
+# table.blob .kwa  { color:#000000; font-weight:bold; }
+# table.blob .kwb  { color:#830000; }
+# table.blob .kwc  { color:#000000; font-weight:bold; }
+# table.blob .kwd  { color:#010181; }
+
+case "$1" in
+	*.c)
+		highlight -f -I -X -S c
+		;;
+	*.h)
+		highlight -f -I -X -S c
+		;;
+	*.sh)
+		highlight -f -I -X -S sh
+		;;
+	*.css)
+		highlight -f -I -X -S css
+		;;
+	*)
+		highlight -f -I -X -S txt
+		;;
+esac
diff --git a/git b/git
-Subproject 5c415311f743ccb11a50f350ff1c385778f049d
+Subproject 7fb6bcff2dece2ff9fbc5ebfe526d9b2a7e764c
diff --git a/scan-tree.c b/scan-tree.c
index 47f3988..dbca797 100644
--- a/scan-tree.c
+++ b/scan-tree.c
@@ -1,4 +1,5 @@
 #include "cgit.h"
+#include "configfile.h"
 #include "html.h"
 
 #define MAX_PATH 4096
@@ -35,25 +36,20 @@ static int is_git_dir(const char *path)
 	return 1;
 }
 
-char *readfile(const char *path)
-{
-	FILE *f;
-	static char buf[MAX_PATH];
+struct cgit_repo *repo;
+repo_config_fn config_fn;
 
-	if (!(f = fopen(path, "r")))
-		return NULL;
-	buf[0] = 0;
-	fgets(buf, MAX_PATH, f);
-	fclose(f);
-	return buf;
+static void repo_config(const char *name, const char *value)
+{
+	config_fn(repo, name, value);
 }
 
-static void add_repo(const char *base, const char *path)
+static void add_repo(const char *base, const char *path, repo_config_fn fn)
 {
-	struct cgit_repo *repo;
 	struct stat st;
 	struct passwd *pwd;
 	char *p;
+	size_t size;
 
 	if (stat(path, &st)) {
 		fprintf(stderr, "Error accessing %s: %s (%d)\n",
@@ -76,18 +72,27 @@ static void add_repo(const char *base, const char *path)
 	repo = cgit_add_repo(xstrdup(p));
 	repo->name = repo->url;
 	repo->path = xstrdup(path);
+	p = (pwd && pwd->pw_gecos) ? strchr(pwd->pw_gecos, ',') : NULL;
+	if (p)
+		*p = '\0';
 	repo->owner = (pwd ? xstrdup(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name) : "");
 
 	p = fmt("%s/description", path);
 	if (!stat(p, &st))
-		repo->desc = xstrdup(readfile(p));
+		readfile(p, &repo->desc, &size);
 
 	p = fmt("%s/README.html", path);
 	if (!stat(p, &st))
 		repo->readme = "README.html";
+
+	p = fmt("%s/cgitrc", path);
+	if (!stat(p, &st)) {
+		config_fn = fn;
+		parse_configfile(xstrdup(p), &repo_config);
+	}
 }
 
-static void scan_path(const char *base, const char *path)
+static void scan_path(const char *base, const char *path, repo_config_fn fn)
 {
 	DIR *dir;
 	struct dirent *ent;
@@ -95,7 +100,11 @@ static void scan_path(const char *base, const char *path)
 	struct stat st;
 
 	if (is_git_dir(path)) {
-		add_repo(base, path);
+		add_repo(base, path, fn);
+		return;
+	}
+	if (is_git_dir(fmt("%s/.git", path))) {
+		add_repo(base, fmt("%s/.git", path), fn);
 		return;
 	}
 	dir = opendir(path);
@@ -125,13 +134,13 @@ static void scan_path(const char *base, const char *path)
 			continue;
 		}
 		if (S_ISDIR(st.st_mode))
-			scan_path(base, buf);
+			scan_path(base, buf, fn);
 		free(buf);
 	}
 	closedir(dir);
 }
 
-void scan_tree(const char *path)
+void scan_tree(const char *path, repo_config_fn fn)
 {
-	scan_path(path, path);
+	scan_path(path, path, fn);
 }
diff --git a/scan-tree.h b/scan-tree.h
index b103b16..11539f4 100644
--- a/scan-tree.h
+++ b/scan-tree.h
@@ -1,3 +1,3 @@
 
 
-extern void scan_tree(const char *path);
+extern void scan_tree(const char *path, repo_config_fn fn);
diff --git a/shared.c b/shared.c
index cce0af4..d7b2d5a 100644
--- a/shared.c
+++ b/shared.c
@@ -48,12 +48,13 @@ struct cgit_repo *cgit_add_repo(const char *url)
 	}
 
 	ret = &cgit_repolist.repos[cgit_repolist.count-1];
+	memset(ret, 0, sizeof(struct cgit_repo));
 	ret->url = trim_end(url, '/');
 	ret->name = ret->url;
 	ret->path = NULL;
 	ret->desc = "[no description]";
 	ret->owner = NULL;
-	ret->group = ctx.cfg.repo_group;
+	ret->section = ctx.cfg.section;
 	ret->defbranch = "master";
 	ret->snapshots = ctx.cfg.snapshots;
 	ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
@@ -62,6 +63,9 @@ struct cgit_repo *cgit_add_repo(const char *url)
 	ret->module_link = ctx.cfg.module_link;
 	ret->readme = NULL;
 	ret->mtime = -1;
+	ret->about_filter = ctx.cfg.about_filter;
+	ret->commit_filter = ctx.cfg.commit_filter;
+	ret->source_filter = ctx.cfg.source_filter;
 	return ret;
 }
 
@@ -355,3 +359,59 @@ int cgit_parse_snapshots_mask(const char *str)
 	}
 	return rv;
 }
+
+int cgit_open_filter(struct cgit_filter *filter)
+{
+
+	filter->old_stdout = chk_positive(dup(STDOUT_FILENO),
+		"Unable to duplicate STDOUT");
+	chk_zero(pipe(filter->pipe_fh), "Unable to create pipe to subprocess");
+	filter->pid = chk_non_negative(fork(), "Unable to create subprocess");
+	if (filter->pid == 0) {
+		close(filter->pipe_fh[1]);
+		chk_non_negative(dup2(filter->pipe_fh[0], STDIN_FILENO),
+			"Unable to use pipe as STDIN");
+		execvp(filter->cmd, filter->argv);
+		die("Unable to exec subprocess %s: %s (%d)", filter->cmd,
+			strerror(errno), errno);
+	}
+	close(filter->pipe_fh[0]);
+	chk_non_negative(dup2(filter->pipe_fh[1], STDOUT_FILENO),
+		"Unable to use pipe as STDOUT");
+	close(filter->pipe_fh[1]);
+	return 0;
+}
+
+int cgit_close_filter(struct cgit_filter *filter)
+{
+	chk_non_negative(dup2(filter->old_stdout, STDOUT_FILENO),
+		"Unable to restore STDOUT");
+	close(filter->old_stdout);
+	if (filter->pid < 0)
+		return 0;
+	waitpid(filter->pid, &filter->exitstatus, 0);
+	if (WIFEXITED(filter->exitstatus) && !WEXITSTATUS(filter->exitstatus))
+		return 0;
+	die("Subprocess %s exited abnormally", filter->cmd);
+}
+
+/* Read the content of the specified file into a newly allocated buffer,
+ * zeroterminate the buffer and return 0 on success, errno otherwise.
+ */
+int readfile(const char *path, char **buf, size_t *size)
+{
+	int fd;
+	struct stat st;
+
+	fd = open(path, O_RDONLY);
+	if (fd == -1)
+		return errno;
+	if (fstat(fd, &st))
+		return errno;
+	if (!S_ISREG(st.st_mode))
+		return EISDIR;
+	*buf = xmalloc(st.st_size + 1);
+	*size = read_in_full(fd, *buf, st.st_size);
+	(*buf)[*size] = '\0';
+	return (*size == st.st_size ? 0 : errno);
+}
diff --git a/ui-atom.c b/ui-atom.c
index a6ea3ee..808b2d0 100644
--- a/ui-atom.c
+++ b/ui-atom.c
@@ -32,7 +32,7 @@ void add_entry(struct commit *commit, char *host)
 		html_txt(info->author);
 		html("</name>\n");
 	}
-	if (info->author_email) {
+	if (info->author_email && !ctx.cfg.noplainemail) {
 		mail = xstrdup(info->author_email);
 		t = strchr(mail, '<');
 		if (t)
@@ -52,7 +52,8 @@ void add_entry(struct commit *commit, char *host)
 	cgit_print_date(info->author_date, FMT_ATOMDATE, ctx.cfg.local_time);
 	html("</published>\n");
 	if (host) {
-		html("<link rel='alternate' type='text/html' href='http://");
+		html("<link rel='alternate' type='text/html' href='");
+		html(cgit_httpscheme());
 		html_attr(host);
 		html_attr(cgit_pageurl(ctx.repo->url, "commit", NULL));
 		if (ctx.cfg.virtual_root)
@@ -113,7 +114,8 @@ void cgit_print_atom(char *tip, char *path, int max_count)
 	html_txt(ctx.repo->desc);
 	html("</subtitle>\n");
 	if (host) {
-		html("<link rel='alternate' type='text/html' href='http://");
+		html("<link rel='alternate' type='text/html' href='");
+		html(cgit_httpscheme());
 		html_attr(host);
 		html_attr(cgit_repourl(ctx.repo->url));
 		html("'/>\n");
diff --git a/ui-blob.c b/ui-blob.c
index 3cda03d..2ccd31d 100644
--- a/ui-blob.c
+++ b/ui-blob.c
@@ -27,7 +27,7 @@ void cgit_print_blob(const char *hex, char *path, const char *head)
 
 	unsigned char sha1[20];
 	enum object_type type;
-	unsigned char *buf;
+	char *buf;
 	unsigned long size;
 	struct commit *commit;
 	const char *paths[] = {path, NULL};
@@ -67,6 +67,12 @@ void cgit_print_blob(const char *hex, char *path, const char *head)
 
 	buf[size] = '\0';
 	ctx.page.mimetype = ctx.qry.mimetype;
+	if (!ctx.page.mimetype) {
+		if (buffer_is_binary(buf, size))
+			ctx.page.mimetype = "application/octet-stream";
+		else
+			ctx.page.mimetype = "text/plain";
+	}
 	ctx.page.filename = path;
 	cgit_print_http_headers(&ctx);
 	write(htmlfd, buf, size);
diff --git a/ui-commit.c b/ui-commit.c
index 41ce70e..f5b0ae5 100644
--- a/ui-commit.c
+++ b/ui-commit.c
@@ -35,20 +35,24 @@ void cgit_print_commit(char *hex)
 	}
 	info = cgit_parse_commit(commit);
 
-	load_ref_decorations();
+	load_ref_decorations(DECORATE_FULL_REFS);
 
 	html("<table summary='commit info' class='commit-info'>\n");
 	html("<tr><th>author</th><td>");
 	html_txt(info->author);
-	html(" ");
-	html_txt(info->author_email);
+	if (!ctx.cfg.noplainemail) {
+		html(" ");
+		html_txt(info->author_email);
+	}
 	html("</td><td class='right'>");
 	cgit_print_date(info->author_date, FMT_LONGDATE, ctx.cfg.local_time);
 	html("</td></tr>\n");
 	html("<tr><th>committer</th><td>");
 	html_txt(info->committer);
-	html(" ");
-	html_txt(info->committer_email);
+	if (!ctx.cfg.noplainemail) {
+		html(" ");
+		html_txt(info->committer_email);
+	}
 	html("</td><td class='right'>");
 	cgit_print_date(info->committer_date, FMT_LONGDATE, ctx.cfg.local_time);
 	html("</td></tr>\n");
@@ -89,11 +93,19 @@ void cgit_print_commit(char *hex)
 	}
 	html("</table>\n");
 	html("<div class='commit-subject'>");
+	if (ctx.repo->commit_filter)
+		cgit_open_filter(ctx.repo->commit_filter);
 	html_txt(info->subject);
+	if (ctx.repo->commit_filter)
+		cgit_close_filter(ctx.repo->commit_filter);
 	show_commit_decorations(commit);
 	html("</div>");
 	html("<div class='commit-msg'>");
+	if (ctx.repo->commit_filter)
+		cgit_open_filter(ctx.repo->commit_filter);
 	html_txt(info->msg);
+	if (ctx.repo->commit_filter)
+		cgit_close_filter(ctx.repo->commit_filter);
 	html("</div>");
 	if (parents < 3) {
 		if (parents)
diff --git a/ui-log.c b/ui-log.c
index ba2ab03..f3132c9 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -53,6 +53,10 @@ void show_commit_decorations(struct commit *commit)
 			strncpy(buf, deco->name + 15, sizeof(buf) - 1);
 			cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf);
 		}
+		else if (!prefixcmp(deco->name, "refs/tags/")) {
+			strncpy(buf, deco->name + 10, sizeof(buf) - 1);
+			cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf);
+		}
 		else if (!prefixcmp(deco->name, "refs/remotes/")) {
 			strncpy(buf, deco->name + 13, sizeof(buf) - 1);
 			cgit_log_link(buf, NULL, "remote-deco", NULL,
@@ -157,7 +161,7 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
 	rev.verbose_header = 1;
 	rev.show_root_diff = 0;
 	setup_revisions(argc, argv, &rev, NULL);
-	load_ref_decorations();
+	load_ref_decorations(DECORATE_FULL_REFS);
 	rev.show_decorations = 1;
 	rev.grep_filter.regflags |= REG_ICASE;
 	compile_grep_patterns(&rev.grep_filter);
diff --git a/ui-patch.c b/ui-patch.c
index 5d665d3..2a8f7a5 100644
--- a/ui-patch.c
+++ b/ui-patch.c
@@ -108,7 +108,11 @@ void cgit_print_patch(char *hex)
 	ctx.page.filename = patchname;
 	cgit_print_http_headers(&ctx);
 	htmlf("From %s Mon Sep 17 00:00:00 2001\n", sha1_to_hex(sha1));
-	htmlf("From: %s %s\n", info->author, info->author_email);
+	htmlf("From: %s", info->author);
+	if (!ctx.cfg.noplainemail) {
+		htmlf(" %s", info->author_email);
+	}
+	html("\n");
 	html("Date: ");
 	cgit_print_date(info->author_date, "%a, %d %b %Y %H:%M:%S %z%n", ctx.cfg.local_time);
 	htmlf("Subject: %s\n\n", info->subject);
diff --git a/ui-plain.c b/ui-plain.c
index e08b15b..a4ce077 100644
--- a/ui-plain.c
+++ b/ui-plain.c
@@ -17,8 +17,9 @@ int match;
 static void print_object(const unsigned char *sha1, const char *path)
 {
 	enum object_type type;
-	char *buf;
+	char *buf, *ext;
 	unsigned long size;
+	struct string_list_item *mime;
 
 	type = sha1_object_info(sha1, &size);
 	if (type == OBJ_BAD) {
@@ -31,9 +32,22 @@ static void print_object(const unsigned char *sha1, const char *path)
 		html_status(404, "Not found", 0);
 		return;
 	}
-	ctx.page.mimetype = "text/plain";
+	ctx.page.mimetype = NULL;
+	ext = strrchr(path, '.');
+	if (ext && *(++ext)) {
+		mime = string_list_lookup(ext, &ctx.cfg.mimetypes);
+		if (mime)
+			ctx.page.mimetype = (char *)mime->util;
+	}
+	if (!ctx.page.mimetype) {
+		if (buffer_is_binary(buf, size))
+			ctx.page.mimetype = "application/octet-stream";
+		else
+			ctx.page.mimetype = "text/plain";
+	}
 	ctx.page.filename = fmt("%s", path);
 	ctx.page.size = size;
+	ctx.page.etag = sha1_to_hex(sha1);
 	cgit_print_http_headers(&ctx);
 	html_raw(buf, size);
 	match = 1;
diff --git a/ui-refs.c b/ui-refs.c
index 25da00a..d3b4f6e 100644
--- a/ui-refs.c
+++ b/ui-refs.c
@@ -46,8 +46,19 @@ static int cmp_tag_age(const void *a, const void *b)
 {
 	struct refinfo *r1 = *(struct refinfo **)a;
 	struct refinfo *r2 = *(struct refinfo **)b;
+	int r1date, r2date;
 
-	return cmp_age(r1->tag->tagger_date, r2->tag->tagger_date);
+	if (r1->object->type != OBJ_COMMIT)
+		r1date = r1->tag->tagger_date;
+	else
+		r1date = r1->commit->committer_date;
+
+	if (r2->object->type != OBJ_COMMIT)
+		r2date = r2->tag->tagger_date;
+	else
+		r2date = r2->commit->committer_date;
+
+	return cmp_age(r1date, r2date);
 }
 
 static int print_branch(struct refinfo *ref)
@@ -145,6 +156,12 @@ static int print_tag(struct refinfo *ref)
 			print_tag_downloads(ctx.repo, name);
 		else
 			cgit_object_link(ref->object);
+		html("</td><td>");
+		if (ref->object->type == OBJ_COMMIT)
+			html(ref->commit->author);
+		html("</td><td colspan='2'>");
+		if (ref->object->type == OBJ_COMMIT)
+			cgit_print_age(ref->commit->commit->date, -1, NULL);
 		html("</td></tr>\n");
 	}
 	return 0;
diff --git a/ui-repolist.c b/ui-repolist.c
index 3aedde5..3ef2e99 100644
--- a/ui-repolist.c
+++ b/ui-repolist.c
@@ -18,19 +18,20 @@
 
 time_t read_agefile(char *path)
 {
-	FILE *f;
-	static char buf[64], buf2[64];
+	time_t result;
+	size_t size;
+	char *buf;
+	static char buf2[64];
 
-	if (!(f = fopen(path, "r")))
+	if (readfile(path, &buf, &size))
 		return -1;
-	buf[0] = 0;
-	if (fgets(buf, sizeof(buf), f) == NULL)
-		return -1;
-	fclose(f);
+
 	if (parse_date(buf, buf2, sizeof(buf2)))
-		return strtoul(buf2, NULL, 10);
+		result = strtoul(buf2, NULL, 10);
 	else
-		return 0;
+		result = 0;
+	free(buf);
+	return result;
 }
 
 static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime)
@@ -135,6 +136,18 @@ static int cmp(const char *s1, const char *s2)
 	return 0;
 }
 
+static int sort_section(const void *a, const void *b)
+{
+	const struct cgit_repo *r1 = a;
+	const struct cgit_repo *r2 = b;
+	int result;
+
+	result = cmp(r1->section, r2->section);
+	if (!result)
+		result = cmp(r1->name, r2->name);
+	return result;
+}
+
 static int sort_name(const void *a, const void *b)
 {
 	const struct cgit_repo *r1 = a;
@@ -177,6 +190,7 @@ struct sortcolumn {
 };
 
 struct sortcolumn sortcolumn[] = {
+	{"section", sort_section},
 	{"name", sort_name},
 	{"desc", sort_desc},
 	{"owner", sort_owner},
@@ -202,7 +216,8 @@ int sort_repolist(char *field)
 void cgit_print_repolist()
 {
 	int i, columns = 4, hits = 0, header = 0;
-	char *last_group = NULL;
+	char *last_section = NULL;
+	char *section;
 	int sorted = 0;
 
 	if (ctx.cfg.enable_index_links)
@@ -218,6 +233,8 @@ void cgit_print_repolist()
 
 	if(ctx.qry.sort)
 		sorted = sort_repolist(ctx.qry.sort);
+	else
+		sort_repolist("section");
 
 	html("<table summary='repository list' class='list nowrap'>");
 	for (i=0; i<cgit_repolist.count; i++) {
@@ -231,19 +248,22 @@ void cgit_print_repolist()
 			continue;
 		if (!header++)
 			print_header(columns);
+		section = ctx.repo->section;
+		if (section && !strcmp(section, ""))
+			section = NULL;
 		if (!sorted &&
-		    ((last_group == NULL && ctx.repo->group != NULL) ||
-		    (last_group != NULL && ctx.repo->group == NULL) ||
-		    (last_group != NULL && ctx.repo->group != NULL &&
-		     strcmp(ctx.repo->group, last_group)))) {
-			htmlf("<tr class='nohover'><td colspan='%d' class='repogroup'>",
+		    ((last_section == NULL && section != NULL) ||
+		    (last_section != NULL && section == NULL) ||
+		    (last_section != NULL && section != NULL &&
+		     strcmp(section, last_section)))) {
+			htmlf("<tr class='nohover'><td colspan='%d' class='reposection'>",
 			      columns);
-			html_txt(ctx.repo->group);
+			html_txt(section);
 			html("</td></tr>");
-			last_group = ctx.repo->group;
+			last_section = section;
 		}
 		htmlf("<tr><td class='%s'>",
-		      !sorted && ctx.repo->group ? "sublevel-repo" : "toplevel-repo");
+		      !sorted && section ? "sublevel-repo" : "toplevel-repo");
 		cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
 		html("</td><td>");
 		html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL);
@@ -274,6 +294,11 @@ void cgit_print_repolist()
 
 void cgit_print_site_readme()
 {
-	if (ctx.cfg.root_readme)
-		html_include(ctx.cfg.root_readme);
+	if (!ctx.cfg.root_readme)
+		return;
+	if (ctx.cfg.about_filter)
+		cgit_open_filter(ctx.cfg.about_filter);
+	html_include(ctx.cfg.root_readme);
+	if (ctx.cfg.about_filter)
+		cgit_close_filter(ctx.cfg.about_filter);
 }
diff --git a/ui-shared.c b/ui-shared.c
index 40060ba..07d5dd4 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -34,24 +34,23 @@ void cgit_print_error(char *msg)
 	html("</div>\n");
 }
 
-char *cgit_hosturl()
+char *cgit_httpscheme()
 {
-	char *host, *port;
+	if (ctx.env.https && !strcmp(ctx.env.https, "on"))
+		return "https://";
+	else
+		return "http://";
+}
 
-	host = getenv("HTTP_HOST");
-	if (host) {
-		host = xstrdup(host);
-	} else {
-		host = getenv("SERVER_NAME");
-		if (!host)
-			return NULL;
-		port = getenv("SERVER_PORT");
-		if (port && atoi(port) != 80)
-			host = xstrdup(fmt("%s:%d", host, atoi(port)));
-		else
-			host = xstrdup(host);
-	}
-	return host;
+char *cgit_hosturl()
+{
+	if (ctx.env.http_host)
+		return ctx.env.http_host;
+	if (!ctx.env.server_name)
+		return NULL;
+	if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80)
+		return ctx.env.server_name;
+	return xstrdup(fmt("%s:%s", ctx.env.server_name, ctx.env.server_port));
 }
 
 char *cgit_rooturl()
@@ -456,6 +455,11 @@ void cgit_print_age(time_t t, time_t max_relative, char *format)
 
 void cgit_print_http_headers(struct cgit_context *ctx)
 {
+	if (ctx->env.no_http && !strcmp(ctx->env.no_http, "1"))
+		return;
+
+	if (ctx->page.status)
+		htmlf("Status: %d %s\n", ctx->page.status, ctx->page.statusmsg);
 	if (ctx->page.mimetype && ctx->page.charset)
 		htmlf("Content-Type: %s; charset=%s\n", ctx->page.mimetype,
 		      ctx->page.charset);
@@ -468,11 +472,21 @@ void cgit_print_http_headers(struct cgit_context *ctx)
 		      ctx->page.filename);
 	htmlf("Last-Modified: %s\n", http_date(ctx->page.modified));
 	htmlf("Expires: %s\n", http_date(ctx->page.expires));
+	if (ctx->page.etag)
+		htmlf("ETag: \"%s\"\n", ctx->page.etag);
 	html("\n");
+	if (ctx->env.request_method && !strcmp(ctx->env.request_method, "HEAD"))
+		exit(0);
 }
 
 void cgit_print_docstart(struct cgit_context *ctx)
 {
+	if (ctx->cfg.embedded) {
+		if (ctx->cfg.header)
+			html_include(ctx->cfg.header);
+		return;
+	}
+
 	char *host = cgit_hosturl();
 	html(cgit_doctype);
 	html("<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n");
@@ -492,12 +506,15 @@ void cgit_print_docstart(struct cgit_context *ctx)
 		html("'/>\n");
 	}
 	if (host && ctx->repo) {
-		html("<link rel='alternate' title='Atom feed' href='http://");
+		html("<link rel='alternate' title='Atom feed' href='");
+		html(cgit_httpscheme());
 		html_attr(cgit_hosturl());
 		html_attr(cgit_fileurl(ctx->repo->url, "atom", ctx->qry.path,
 				       fmt("h=%s", ctx->qry.head)));
-		html("' type='application/atom+xml'/>");
+		html("' type='application/atom+xml'/>\n");
 	}
+	if (ctx->cfg.head_include)
+		html_include(ctx->cfg.head_include);
 	html("</head>\n");
 	html("<body>\n");
 	if (ctx->cfg.header)
@@ -506,7 +523,13 @@ void cgit_print_docstart(struct cgit_context *ctx)
 
 void cgit_print_docend()
 {
-	html("</div>");
+	html("</div> <!-- class=content -->\n");
+	if (ctx.cfg.embedded) {
+		html("</div> <!-- id=cgit -->\n");
+		if (ctx.cfg.footer)
+			html_include(ctx.cfg.footer);
+		return;
+	}
 	if (ctx.cfg.footer)
 		html_include(ctx.cfg.footer);
 	else {
@@ -515,6 +538,7 @@ void cgit_print_docend()
 		cgit_print_date(time(NULL), FMT_LONGDATE, ctx.cfg.local_time);
 		html("</div>\n");
 	}
+	html("</div> <!-- id=cgit -->\n");
 	html("</body>\n</html>\n");
 }
 
@@ -602,13 +626,8 @@ char *hc(struct cgit_cmd *cmd, const char *page)
 	return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active");
 }
 
-void cgit_print_pageheader(struct cgit_context *ctx)
+static void print_header(struct cgit_context *ctx)
 {
-	struct cgit_cmd *cmd = cgit_get_cmd(ctx);
-
-	if (!cmd && ctx->repo)
-		fallback_cmd = "summary";
-
 	html("<table id='header'>\n");
 	html("<tr>\n");
 
@@ -652,6 +671,18 @@ void cgit_print_pageheader(struct cgit_context *ctx)
 			html_include(ctx->cfg.index_info);
 	}
 	html("</td></tr></table>\n");
+}
+
+void cgit_print_pageheader(struct cgit_context *ctx)
+{
+	struct cgit_cmd *cmd = cgit_get_cmd(ctx);
+
+	if (!cmd && ctx->repo)
+		fallback_cmd = "summary";
+
+	html("<div id='cgit'>");
+	if (!ctx->cfg.noheader)
+		print_header(ctx);
 
 	html("<table class='tabs'><tr><td>\n");
 	if (ctx->repo) {
diff --git a/ui-shared.h b/ui-shared.h
index 5a3821f..bff4826 100644
--- a/ui-shared.h
+++ b/ui-shared.h
@@ -1,6 +1,7 @@
 #ifndef UI_SHARED_H
 #define UI_SHARED_H
 
+extern char *cgit_httpscheme();
 extern char *cgit_hosturl();
 extern char *cgit_repourl(const char *reponame);
 extern char *cgit_fileurl(const char *reponame, const char *pagename,
diff --git a/ui-snapshot.c b/ui-snapshot.c
index 5372f5d..4136b3e 100644
--- a/ui-snapshot.c
+++ b/ui-snapshot.c
@@ -12,37 +12,16 @@
 
 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;
+	struct cgit_filter f;
 
-	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");
-
+	f.cmd = xstrdup(filter);
+	f.argv = malloc(2 * sizeof(char *));
+	f.argv[0] = f.cmd;
+	f.argv[1] = NULL;
+	cgit_open_filter(&f);
 	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");
-
+	cgit_close_filter(&f);
 	return rv;
 }
 
diff --git a/ui-stats.c b/ui-stats.c
index 9fc06d3..bdaf9cc 100644
--- a/ui-stats.c
+++ b/ui-stats.c
@@ -154,6 +154,14 @@ int cgit_find_stats_period(const char *expr, struct cgit_period **period)
 	return 0;
 }
 
+const char *cgit_find_stats_periodname(int idx)
+{
+	if (idx > 0 && idx < 4)
+		return periods[idx - 1].name;
+	else
+		return "";
+}
+
 static void add_commit(struct string_list *authors, struct commit *commit,
 	struct cgit_period *period)
 {
diff --git a/ui-stats.h b/ui-stats.h
index 4f13dba..f0761ba 100644
--- a/ui-stats.h
+++ b/ui-stats.h
@@ -21,6 +21,7 @@ struct cgit_period {
 };
 
 extern int cgit_find_stats_period(const char *expr, struct cgit_period **period);
+extern const char *cgit_find_stats_periodname(int idx);
 
 extern void cgit_show_stats(struct cgit_context *ctx);
 
diff --git a/ui-summary.c b/ui-summary.c
index ede4a62..a2c018e 100644
--- a/ui-summary.c
+++ b/ui-summary.c
@@ -66,11 +66,27 @@ void cgit_print_summary()
 	html("</table>");
 }
 
-void cgit_print_repo_readme()
+void cgit_print_repo_readme(char *path)
 {
-	if (ctx.repo->readme) {
-		html("<div id='summary'>");
-		html_include(ctx.repo->readme);
-		html("</div>");
-	}
+	char *slash, *tmp;
+
+	if (!ctx.repo->readme)
+		return;
+
+	if (path) {
+		slash = strrchr(ctx.repo->readme, '/');
+		if (!slash)
+			return;
+		tmp = xmalloc(slash - ctx.repo->readme + 1 + strlen(path) + 1);
+		strncpy(tmp, ctx.repo->readme, slash - ctx.repo->readme + 1);
+		strcpy(tmp + (slash - ctx.repo->readme + 1), path);
+	} else
+		tmp = ctx.repo->readme;
+	html("<div id='summary'>");
+	if (ctx.repo->about_filter)
+		cgit_open_filter(ctx.repo->about_filter);
+	html_include(tmp);
+	if (ctx.repo->about_filter)
+		cgit_close_filter(ctx.repo->about_filter);
+	html("</div>");
 }
diff --git a/ui-summary.h b/ui-summary.h
index 3e13039..c01f560 100644
--- a/ui-summary.h
+++ b/ui-summary.h
@@ -2,6 +2,6 @@
 #define UI_SUMMARY_H
 
 extern void cgit_print_summary();
-extern void cgit_print_repo_readme();
+extern void cgit_print_repo_readme(char *path);
 
 #endif /* UI_SUMMARY_H */
diff --git a/ui-tag.c b/ui-tag.c
index 8c263ab..c2d72af 100644
--- a/ui-tag.c
+++ b/ui-tag.c
@@ -67,7 +67,7 @@ void cgit_print_tag(char *revname)
 		if (info->tagger) {
 			html("<tr><td>Tagged by</td><td>");
 			html_txt(info->tagger);
-			if (info->tagger_email) {
+			if (info->tagger_email && !ctx.cfg.noplainemail) {
 				html(" ");
 				html_txt(info->tagger_email);
 			}
diff --git a/ui-tree.c b/ui-tree.c
index 553dbaa..f53ab64 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -15,26 +15,43 @@ char *curr_rev;
 char *match_path;
 int header = 0;
 
-static void print_text_buffer(char *buf, unsigned long size)
+static void print_text_buffer(const char *name, char *buf, unsigned long size)
 {
 	unsigned long lineno, idx;
 	const char *numberfmt =
 		"<a class='no' id='n%1$d' name='n%1$d' href='#n%1$d'>%1$d</a>\n";
 
 	html("<table summary='blob content' class='blob'>\n");
-	html("<tr><td class='linenumbers'><pre>");
-	idx = 0;
-	lineno = 0;
-
-	if (size) {
-		htmlf(numberfmt, ++lineno);
-		while(idx < size - 1) { // skip absolute last newline
-			if (buf[idx] == '\n')
-				htmlf(numberfmt, ++lineno);
-			idx++;
+
+	if (ctx.cfg.enable_tree_linenumbers) {
+		html("<tr><td class='linenumbers'><pre>");
+		idx = 0;
+		lineno = 0;
+	
+		if (size) {
+			htmlf(numberfmt, ++lineno);
+			while(idx < size - 1) { // skip absolute last newline
+				if (buf[idx] == '\n')
+					htmlf(numberfmt, ++lineno);
+				idx++;
+			}
 		}
+		html("</pre></td>\n");
+	}
+	else {
+		html("<tr>\n");
+	}
+
+	if (ctx.repo->source_filter) {
+		html("<td class='lines'><pre><code>");
+		ctx.repo->source_filter->argv[1] = xstrdup(name);
+		cgit_open_filter(ctx.repo->source_filter);
+		write(STDOUT_FILENO, buf, size);
+		cgit_close_filter(ctx.repo->source_filter);
+		html("</code></pre></td></tr></table>\n");
+		return;
 	}
-	html("</pre></td>\n");
+
 	html("<td class='lines'><pre><code>");
 	html_txt(buf);
 	html("</code></pre></td></tr></table>\n");
@@ -65,7 +82,7 @@ static void print_binary_buffer(char *buf, unsigned long size)
 	html("</table>\n");
 }
 
-static void print_object(const unsigned char *sha1, char *path)
+static void print_object(const unsigned char *sha1, char *path, const char *basename)
 {
 	enum object_type type;
 	char *buf;
@@ -93,7 +110,7 @@ static void print_object(const unsigned char *sha1, char *path)
 	if (buffer_is_binary(buf, size))
 		print_binary_buffer(buf, size);
 	else
-		print_text_buffer(buf, size);
+		print_text_buffer(basename, buf, size);
 }
 
 
@@ -103,6 +120,7 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
 {
 	char *name;
 	char *fullpath;
+	char *class;
 	enum object_type type;
 	unsigned long size = 0;
 
@@ -135,7 +153,12 @@ static int ls_item(const unsigned char *sha1, const char *base, int baselen,
 		cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head,
 			       curr_rev, fullpath);
 	} else {
-		cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head,
+		class = strrchr(name, '.');
+		if (class != NULL) {
+			class = fmt("ls-blob %s", class + 1);
+		} else
+			class = "ls-blob";
+		cgit_tree_link(name, NULL, class, ctx.qry.head,
 			       curr_rev, fullpath);
 	}
 	htmlf("</td><td class='ls-size'>%li</td>", size);
@@ -213,7 +236,7 @@ static int walk_tree(const unsigned char *sha1, const char *base, int baselen,
 			ls_head();
 			return READ_TREE_RECURSIVE;
 		} else {
-			print_object(sha1, buffer);
+			print_object(sha1, buffer, pathname);
 			return 0;
 		}
 	}