about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--cache.c7
-rw-r--r--cgit.c2
-rw-r--r--cgit.css28
-rw-r--r--filter.c22
-rw-r--r--html.c2
-rw-r--r--robots.txt1
-rwxr-xr-xtests/t0105-commit.sh6
-rwxr-xr-xtests/t0106-diff.sh4
-rw-r--r--ui-blame.c4
-rw-r--r--ui-commit.c7
-rw-r--r--ui-diff.c30
-rw-r--r--ui-log.c4
-rw-r--r--ui-repolist.c4
-rw-r--r--ui-shared.c44
-rw-r--r--ui-snapshot.c3
-rw-r--r--ui-tag.c4
-rw-r--r--ui-tree.c40
17 files changed, 148 insertions, 64 deletions
diff --git a/cache.c b/cache.c
index 55199e8..578b73b 100644
--- a/cache.c
+++ b/cache.c
@@ -265,6 +265,13 @@ static int process_slot(struct cache_slot *slot)
 {
 	int err;
 
+	/*
+	 * Make sure any buffered data is flushed before we redirect,
+	 * do sendfile(2) or write(2)
+	 */
+	if (fflush(stdout))
+		return errno;
+
 	err = open_slot(slot);
 	if (!err && slot->match) {
 		if (is_expired(slot)) {
diff --git a/cgit.c b/cgit.c
index 08d81a1..40825cb 100644
--- a/cgit.c
+++ b/cgit.c
@@ -674,7 +674,7 @@ static inline void authenticate_post(void)
 		len = MAX_AUTHENTICATION_POST_BYTES;
 	if ((len = read(STDIN_FILENO, buffer, len)) < 0)
 		die_errno("Could not read POST from stdin");
-	if (write(STDOUT_FILENO, buffer, len) < 0)
+	if (fwrite(buffer, 1, len, stdout) < len)
 		die_errno("Could not write POST to stdout");
 	cgit_close_filter(ctx.cfg.auth_filter);
 	exit(0);
diff --git a/cgit.css b/cgit.css
index dfa144d..f3dbb7a 100644
--- a/cgit.css
+++ b/cgit.css
@@ -75,7 +75,7 @@ div#cgit table.tabs td {
 }
 
 div#cgit table.tabs td a {
-	padding: 2px 0.75em;
+	padding: 2px 0.25em;
 	color: #777;
 	font-size: 110%;
 }
@@ -437,11 +437,6 @@ div#cgit div.commit-subject {
 	padding: 0em;
 }
 
-div#cgit div.commit-msg {
-	white-space: pre;
-	font-family: monospace;
-}
-
 div#cgit div.notes-header {
 	font-weight: bold;
 	padding-top: 1.5em;
@@ -538,26 +533,20 @@ div#cgit table.diff {
 	width: 100%;
 }
 
-div#cgit table.diff td {
-	font-family: monospace;
-	white-space: pre;
-}
-
-div#cgit table.diff td div.head {
+div#cgit table.diff td span.head {
 	font-weight: bold;
-	margin-top: 1em;
 	color: black;
 }
 
-div#cgit table.diff td div.hunk {
+div#cgit table.diff td span.hunk {
 	color: #009;
 }
 
-div#cgit table.diff td div.add {
+div#cgit table.diff td span.add {
 	color: green;
 }
 
-div#cgit table.diff td div.del {
+div#cgit table.diff td span.del {
 	color: red;
 }
 
@@ -581,7 +570,6 @@ div#cgit table.list td.reposection {
 
 div#cgit a.button {
 	font-size: 80%;
-	padding: 0em 0.5em;
 }
 
 div#cgit a.primary {
@@ -671,7 +659,6 @@ div#cgit div.footer a:hover {
 
 div#cgit a.branch-deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #88ff88;
 	border: solid 1px #007700;
@@ -679,7 +666,6 @@ div#cgit a.branch-deco {
 
 div#cgit a.tag-deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #ffff88;
 	border: solid 1px #777700;
@@ -687,7 +673,6 @@ div#cgit a.tag-deco {
 
 div#cgit a.tag-annotated-deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #ffcc88;
 	border: solid 1px #777700;
@@ -695,7 +680,6 @@ div#cgit a.tag-annotated-deco {
 
 div#cgit a.remote-deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #ccccff;
 	border: solid 1px #000077;
@@ -703,7 +687,6 @@ div#cgit a.remote-deco {
 
 div#cgit a.deco {
 	color: #000;
-	margin: 0px 0.5em;
 	padding: 0px 0.25em;
 	background-color: #ff8888;
 	border: solid 1px #770000;
@@ -714,7 +697,6 @@ div#cgit div.commit-subject a.tag-deco,
 div#cgit div.commit-subject a.tag-annotated-deco,
 div#cgit div.commit-subject a.remote-deco,
 div#cgit div.commit-subject a.deco {
-	margin-left: 1em;
 	font-size: 75%;
 }
 
diff --git a/filter.c b/filter.c
index 70f5b74..fba26aa 100644
--- a/filter.c
+++ b/filter.c
@@ -48,6 +48,7 @@ static int open_exec_filter(struct cgit_filter *base, va_list ap)
 	for (i = 0; i < filter->base.argument_count; i++)
 		filter->argv[i + 1] = va_arg(ap, char *);
 
+	chk_zero(fflush(stdout), "unable to flush STDOUT");
 	filter->old_stdout = chk_positive(dup(STDOUT_FILENO),
 		"Unable to duplicate STDOUT");
 	chk_zero(pipe(pipe_fh), "Unable to create pipe to subprocess");
@@ -71,6 +72,7 @@ static int close_exec_filter(struct cgit_filter *base)
 	struct cgit_exec_filter *filter = (struct cgit_exec_filter *)base;
 	int i, exit_status = 0;
 
+	chk_zero(fflush(stdout), "unable to flush STDOUT");
 	chk_non_negative(dup2(filter->old_stdout, STDOUT_FILENO),
 		"Unable to restore STDOUT");
 	close(filter->old_stdout);
@@ -143,17 +145,32 @@ void cgit_init_filters(void)
 #endif
 
 #ifndef NO_LUA
-static ssize_t (*libc_write)(int fd, const void *buf, size_t count);
+static size_t (*libc_fwrite)(const void *buf, size_t size, size_t n, FILE *);
+static ssize_t (*libc_write)(int fd, const void *buf, size_t size);
 static ssize_t (*filter_write)(struct cgit_filter *base, const void *buf, size_t count) = NULL;
 static struct cgit_filter *current_write_filter = NULL;
 
 void cgit_init_filters(void)
 {
+	/*
+	 * we need to wrap both functions since the Lua filter may
+	 * have code which calls write(2) directly, bypassing fwrite(3)
+	 */
+	libc_fwrite = dlsym(RTLD_NEXT, "fwrite");
+	if (!libc_fwrite)
+		die("Could not locate libc's write function");
 	libc_write = dlsym(RTLD_NEXT, "write");
 	if (!libc_write)
 		die("Could not locate libc's write function");
 }
 
+size_t fwrite(const void *buf, size_t size, size_t n, FILE *f)
+{
+	if (f != stdout || !filter_write)
+		return libc_fwrite(buf, size, n, f);
+	return filter_write(current_write_filter, buf, size * n);
+}
+
 ssize_t write(int fd, const void *buf, size_t count)
 {
 	if (fd != STDOUT_FILENO || !filter_write)
@@ -305,6 +322,9 @@ static int open_lua_filter(struct cgit_filter *base, va_list ap)
 	struct lua_filter *filter = (struct lua_filter *)base;
 	int i;
 
+	if (fflush(stdout))
+		return 1;
+
 	if (init_lua_filter(filter))
 		return 1;
 
diff --git a/html.c b/html.c
index 7f81965..cefcf5e 100644
--- a/html.c
+++ b/html.c
@@ -80,7 +80,7 @@ char *fmtalloc(const char *format, ...)
 
 void html_raw(const char *data, size_t size)
 {
-	if (write(STDOUT_FILENO, data, size) != size)
+	if (fwrite(data, 1, size, stdout) != size)
 		die_errno("write error on html output");
 }
 
diff --git a/robots.txt b/robots.txt
index 4ce948f..1b33266 100644
--- a/robots.txt
+++ b/robots.txt
@@ -1,3 +1,4 @@
 User-agent: *
 Disallow: /*/snapshot/*
+Disallow: /*/blame/*
 Allow: /
diff --git a/tests/t0105-commit.sh b/tests/t0105-commit.sh
index 1a12ee3..cfed1e7 100755
--- a/tests/t0105-commit.sh
+++ b/tests/t0105-commit.sh
@@ -11,7 +11,7 @@ test_expect_success 'find commit subject' '
 	grep "<div class=.commit-subject.>commit 5<" tmp
 '
 
-test_expect_success 'find commit msg' 'grep "<div class=.commit-msg.></div>" tmp'
+test_expect_success 'find commit msg' 'grep "<pre class=.commit-msg.></pre>" tmp'
 test_expect_success 'find diffstat' 'grep "<table summary=.diffstat. class=.diffstat.>" tmp'
 
 test_expect_success 'find diff summary' '
@@ -29,8 +29,8 @@ test_expect_success 'root commit contains diffstat' '
 '
 
 test_expect_success 'root commit contains diff' '
-	grep ">diff --git a/file-1 b/file-1<" tmp &&
-	grep "<div class=.add.>+1</div>" tmp
+	grep ">diff --git a/file-1 b/file-1" tmp &&
+	grep "<span class=.add.>+1</span>" tmp
 '
 
 test_done
diff --git a/tests/t0106-diff.sh b/tests/t0106-diff.sh
index 82b645e..62a0a74 100755
--- a/tests/t0106-diff.sh
+++ b/tests/t0106-diff.sh
@@ -9,11 +9,11 @@ test_expect_success 'find blob link' 'grep "<a href=./foo/tree/file-5?id=" tmp'
 test_expect_success 'find added file' 'grep "new file mode 100644" tmp'
 
 test_expect_success 'find hunk header' '
-	grep "<div class=.hunk.>@@ -0,0 +1 @@</div>" tmp
+	grep "<span class=.hunk.>@@ -0,0 +1 @@</span>" tmp
 '
 
 test_expect_success 'find added line' '
-	grep "<div class=.add.>+5</div>" tmp
+	grep "<span class=.add.>+5</span>" tmp
 '
 
 test_done
diff --git a/ui-blame.c b/ui-blame.c
index 03136f7..4adec2b 100644
--- a/ui-blame.c
+++ b/ui-blame.c
@@ -152,6 +152,10 @@ static void print_object(const struct object_id *oid, const char *path,
 	cgit_tree_link("tree", NULL, NULL, ctx.qry.head, rev, path);
 	html(")\n");
 
+	if (buffer_is_binary(buf, size)) {
+		html("<div class='error'>blob is binary.</div>");
+		goto cleanup;
+	}
 	if (ctx.cfg.max_blob_size && size / 1024 > ctx.cfg.max_blob_size) {
 		htmlf("<div class='error'>blob size (%ldKB)"
 		      " exceeds display size limit (%dKB).</div>",
diff --git a/ui-commit.c b/ui-commit.c
index 948118c..b49259e 100644
--- a/ui-commit.c
+++ b/ui-commit.c
@@ -39,10 +39,11 @@ void cgit_print_commit(char *hex, const char *prefix)
 	}
 	info = cgit_parse_commit(commit);
 
-	format_display_notes(&oid, &notes, PAGE_ENCODING, 0);
+	format_display_notes(&oid, &notes, PAGE_ENCODING, 1);
 
 	load_ref_decorations(NULL, DECORATE_FULL_REFS);
 
+	ctx.page.title = fmtalloc("%s - %s", info->subject, ctx.page.title);
 	cgit_print_layout_start();
 	cgit_print_diff_ctrls();
 	html("<table summary='commit info' class='commit-info'>\n");
@@ -120,11 +121,11 @@ void cgit_print_commit(char *hex, const char *prefix)
 	cgit_close_filter(ctx.repo->commit_filter);
 	show_commit_decorations(commit);
 	html("</div>");
-	html("<div class='commit-msg'>");
+	html("<pre class='commit-msg'>");
 	cgit_open_filter(ctx.repo->commit_filter);
 	html_txt(info->msg);
 	cgit_close_filter(ctx.repo->commit_filter);
-	html("</div>");
+	html("</pre>");
 	if (notes.len != 0) {
 		html("<div class='notes-header'>Notes</div>");
 		html("<div class='notes'>");
diff --git a/ui-diff.c b/ui-diff.c
index 5ed5990..2a64ae8 100644
--- a/ui-diff.c
+++ b/ui-diff.c
@@ -231,11 +231,11 @@ static void print_line(char *line, int len)
 	else if (line[0] == '@')
 		class = "hunk";
 
-	htmlf("<div class='%s'>", class);
+	htmlf("<span class='%s'>", class);
 	line[len-1] = '\0';
 	html_txt(line);
-	html("</div>");
 	line[len-1] = c;
+	html("</span>\n");
 }
 
 static void header(const struct object_id *oid1, char *path1, int mode1,
@@ -245,22 +245,23 @@ static void header(const struct object_id *oid1, char *path1, int mode1,
 	int subproject;
 
 	subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2));
-	html("<div class='head'>");
+	html("<span class='head'>");
 	html("diff --git a/");
 	html_txt(path1);
 	html(" b/");
 	html_txt(path2);
+	html("\n");
 
 	if (mode1 == 0)
-		htmlf("<br/>new file mode %.6o", mode2);
+		htmlf("new file mode %.6o\n", mode2);
 
 	if (mode2 == 0)
-		htmlf("<br/>deleted file mode %.6o", mode1);
+		htmlf("deleted file mode %.6o\n", mode1);
 
 	if (!subproject) {
 		abbrev1 = xstrdup(find_unique_abbrev(oid1, DEFAULT_ABBREV));
 		abbrev2 = xstrdup(find_unique_abbrev(oid2, DEFAULT_ABBREV));
-		htmlf("<br/>index %s..%s", abbrev1, abbrev2);
+		htmlf("index %s..%s", abbrev1, abbrev2);
 		free(abbrev1);
 		free(abbrev2);
 		if (mode1 != 0 && mode2 != 0) {
@@ -268,28 +269,31 @@ static void header(const struct object_id *oid1, char *path1, int mode1,
 			if (mode2 != mode1)
 				htmlf("..%.6o", mode2);
 		}
+		html("\n");
 		if (is_null_oid(oid1)) {
 			path1 = "dev/null";
-			html("<br/>--- /");
+			html("--- /");
 		} else
-			html("<br/>--- a/");
+			html("--- a/");
 		if (mode1 != 0)
 			cgit_tree_link(path1, NULL, NULL, ctx.qry.head,
 				       oid_to_hex(old_rev_oid), path1);
 		else
 			html_txt(path1);
+		html("\n");
 		if (is_null_oid(oid2)) {
 			path2 = "dev/null";
-			html("<br/>+++ /");
+			html("+++ /");
 		} else
-			html("<br/>+++ b/");
+			html("+++ b/");
 		if (mode2 != 0)
 			cgit_tree_link(path2, NULL, NULL, ctx.qry.head,
 				       oid_to_hex(new_rev_oid), path2);
 		else
 			html_txt(path2);
+		html("\n");
 	}
-	html("</div>");
+	html("</span>");
 }
 
 static void filepair_cb(struct diff_filepair *pair)
@@ -488,12 +492,12 @@ void cgit_print_diff(const char *new_rev, const char *old_rev,
 		html("<table summary='ssdiff' class='ssdiff'>");
 	} else {
 		html("<table summary='diff' class='diff'>");
-		html("<tr><td>");
+		html("<tr><td><pre>");
 	}
 	cgit_diff_tree(old_rev_oid, new_rev_oid, filepair_cb, prefix,
 		       ctx.qry.ignorews);
 	if (!use_ssdiff)
-		html("</td></tr>");
+		html("</pre></td></tr>");
 	html("</table>");
 
 	if (show_ctrls)
diff --git a/ui-log.c b/ui-log.c
index 20774bf..b443ca7 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -75,11 +75,13 @@ void show_commit_decorations(struct commit *commit)
 			 * don't display anything. */
 			break;
 		case DECORATION_REF_LOCAL:
+			html(" ");
 			cgit_log_link(buf, NULL, "branch-deco", buf, NULL,
 				ctx.qry.vpath, 0, NULL, NULL,
 				ctx.qry.showmsg, 0);
 			break;
 		case DECORATION_REF_TAG:
+			html(" ");
 			if (!read_ref(deco->name, &oid_tag) && !peel_iterated_oid(&oid_tag, &peeled))
 				is_annotated = !oideq(&oid_tag, &peeled);
 			cgit_tag_link(buf, NULL, is_annotated ? "tag-annotated-deco" : "tag-deco", buf);
@@ -87,12 +89,14 @@ void show_commit_decorations(struct commit *commit)
 		case DECORATION_REF_REMOTE:
 			if (!ctx.repo->enable_remote_branches)
 				break;
+			html(" ");
 			cgit_log_link(buf, NULL, "remote-deco", NULL,
 				oid_to_hex(&commit->object.oid),
 				ctx.qry.vpath, 0, NULL, NULL,
 				ctx.qry.showmsg, 0);
 			break;
 		default:
+			html(" ");
 			cgit_commit_link(buf, NULL, "deco", ctx.qry.head,
 					oid_to_hex(&commit->object.oid),
 					ctx.qry.vpath);
diff --git a/ui-repolist.c b/ui-repolist.c
index 529a203..97b11c5 100644
--- a/ui-repolist.c
+++ b/ui-repolist.c
@@ -321,7 +321,7 @@ void cgit_print_repolist(void)
 		}
 		htmlf("<tr><td class='%s'>",
 		      !sorted && section ? "sublevel-repo" : "toplevel-repo");
-		cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
+		cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
 		html("</td><td>");
 		repourl = cgit_repourl(ctx.repo->url);
 		html_link_open(repourl, NULL, NULL);
@@ -353,8 +353,10 @@ void cgit_print_repolist(void)
 		if (ctx.cfg.enable_index_links) {
 			html("<td>");
 			cgit_summary_link("summary", NULL, "button", NULL);
+			html(" ");
 			cgit_log_link("log", NULL, "button", NULL, NULL, NULL,
 				      0, NULL, NULL, ctx.qry.showmsg, 0);
+			html(" ");
 			cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL);
 			html("</td>");
 		}
diff --git a/ui-shared.c b/ui-shared.c
index acd8ab5..225a363 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -894,6 +894,15 @@ void cgit_add_clone_urls(void (*fn)(const char *))
 		add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url);
 }
 
+static int print_this_commit_option(void)
+{
+	struct object_id oid;
+	if (!ctx.qry.head || get_oid(ctx.qry.head, &oid))
+		return 1;
+	html_option(oid_to_hex(&oid), "this commit", ctx.qry.head);
+	return 0;
+}
+
 static int print_branch_option(const char *refname, const struct object_id *oid,
 			       int flags, void *cb_data)
 {
@@ -995,15 +1004,18 @@ static void print_header(void)
 	if (ctx.repo) {
 		cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1);
 		html(" : ");
-		cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
+		cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
 		if (ctx.env.authenticated) {
 			html("</td><td class='form'>");
 			html("<form method='get'>\n");
 			cgit_add_hidden_formfields(0, 1, ctx.qry.page);
 			html("<select name='h' onchange='this.form.submit();'>\n");
+			print_this_commit_option();
+			html("<optgroup label='branches'>");
 			for_each_branch_ref(print_branch_option, ctx.qry.head);
 			if (ctx.repo->enable_remote_branches)
 				for_each_remote_ref(print_branch_option, ctx.qry.head);
+			html("</optgroup>");
 			html("</select> ");
 			html("<input type='submit' value='switch'/>");
 			html("</form>");
@@ -1016,7 +1028,13 @@ static void print_header(void)
 	if (ctx.repo) {
 		html_txt(ctx.repo->desc);
 		html("</td><td class='sub right'>");
-		html_txt(ctx.repo->owner);
+		if (ctx.repo->owner_filter) {
+			cgit_open_filter(ctx.repo->owner_filter);
+			html_txt(ctx.repo->owner);
+			cgit_close_filter(ctx.repo->owner_filter);
+		} else {
+			html_txt(ctx.repo->owner);
+		}
 	} else {
 		if (ctx.cfg.root_desc)
 			html_txt(ctx.cfg.root_desc);
@@ -1032,32 +1050,41 @@ void cgit_print_pageheader(void)
 
 	html("<table class='tabs'><tr><td>\n");
 	if (ctx.env.authenticated && ctx.repo) {
-		if (ctx.repo->readme.nr)
+		if (ctx.repo->readme.nr) {
 			reporevlink("about", "about", NULL,
 				    hc("about"), ctx.qry.head, NULL,
 				    NULL);
+			html(" ");
+		}
 		cgit_summary_link("summary", NULL, hc("summary"),
 				  ctx.qry.head);
+		html(" ");
 		cgit_refs_link("refs", NULL, hc("refs"), ctx.qry.head,
 			       ctx.qry.oid, NULL);
+		html(" ");
 		cgit_log_link("log", NULL, hc("log"), ctx.qry.head,
 			      NULL, ctx.qry.vpath, 0, NULL, NULL,
 			      ctx.qry.showmsg, ctx.qry.follow);
+		html(" ");
 		if (ctx.qry.page && !strcmp(ctx.qry.page, "blame"))
 			cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head,
 				        ctx.qry.oid, ctx.qry.vpath);
 		else
 			cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
-				       ctx.qry.oid, ctx.qry.vpath);
+				        ctx.qry.oid, ctx.qry.vpath);
+		html(" ");
 		cgit_commit_link("commit", NULL, hc("commit"),
 				 ctx.qry.head, ctx.qry.oid, ctx.qry.vpath);
+		html(" ");
 		cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head,
 			       ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath);
-		if (ctx.repo->max_stats)
+		if (ctx.repo->max_stats) {
+			html(" ");
 			cgit_stats_link("stats", NULL, hc("stats"),
 					ctx.qry.head, ctx.qry.vpath);
+		}
 		if (ctx.repo->homepage) {
-			html("<a href='");
+			html(" <a href='");
 			html_attr(ctx.repo->homepage);
 			html("'>homepage</a>");
 		}
@@ -1201,9 +1228,12 @@ void cgit_set_title_from_path(const char *path)
 	if (!path)
 		return;
 
-	for (last_slash = path + strlen(path); (slash = memrchr(path, '/', last_slash - path)) != NULL; last_slash = slash) {
+	last_slash = path + strlen(path);
+	for (slash = last_slash; slash > path; --slash) {
+		if (*slash != '/') continue;
 		strbuf_add(&sb, slash + 1, last_slash - slash - 1);
 		strbuf_addstr(&sb, " \xc2\xab ");
+		last_slash = slash;
 	}
 	strbuf_add(&sb, path, last_slash - path);
 	strbuf_addf(&sb, " - %s", ctx.page.title);
diff --git a/ui-snapshot.c b/ui-snapshot.c
index 18361a6..2801393 100644
--- a/ui-snapshot.c
+++ b/ui-snapshot.c
@@ -37,6 +37,9 @@ static int write_archive_type(const char *format, const char *hex, const char *p
 	/* strvec guarantees a trailing NULL entry. */
 	memcpy(nargv, argv.v, sizeof(char *) * (argv.nr + 1));
 
+	if (fflush(stdout))
+		return errno;
+
 	result = write_archive(argv.nr, nargv, NULL, the_repository, NULL, 0);
 	strvec_clear(&argv);
 	free(nargv);
diff --git a/ui-tag.c b/ui-tag.c
index 424bbcc..0595242 100644
--- a/ui-tag.c
+++ b/ui-tag.c
@@ -25,9 +25,9 @@ static void print_tag_content(char *buf)
 	html_txt(buf);
 	html("</div>");
 	if (p) {
-		html("<div class='commit-msg'>");
+		html("<pre class='commit-msg'>");
 		html_txt(++p);
-		html("</div>");
+		html("</pre>");
 	}
 }
 
diff --git a/ui-tree.c b/ui-tree.c
index b61f6f5..21e0b88 100644
--- a/ui-tree.c
+++ b/ui-tree.c
@@ -89,6 +89,7 @@ static void print_object(const struct object_id *oid, const char *path, const ch
 	enum object_type type;
 	char *buf;
 	unsigned long size;
+	int is_binary;
 
 	type = oid_object_info(the_repository, oid, &size);
 	if (type == OBJ_BAD) {
@@ -103,6 +104,7 @@ static void print_object(const struct object_id *oid, const char *path, const ch
 			"Error reading object %s", oid_to_hex(oid));
 		return;
 	}
+	is_binary = buffer_is_binary(buf, size);
 
 	cgit_set_title_from_path(path);
 
@@ -110,7 +112,7 @@ static void print_object(const struct object_id *oid, const char *path, const ch
 	htmlf("blob: %s (", oid_to_hex(oid));
 	cgit_plain_link("plain", NULL, NULL, ctx.qry.head,
 		        rev, path);
-	if (ctx.repo->enable_blame) {
+	if (ctx.repo->enable_blame && !is_binary) {
 		html(") (");
 		cgit_blame_link("blame", NULL, NULL, ctx.qry.head,
 			        rev, path);
@@ -123,7 +125,7 @@ static void print_object(const struct object_id *oid, const char *path, const ch
 		return;
 	}
 
-	if (buffer_is_binary(buf, size))
+	if (is_binary)
 		print_binary_buffer(buf, size);
 	else
 		print_text_buffer(basename, buf, size);
@@ -202,9 +204,11 @@ static int ls_item(const struct object_id *oid, struct strbuf *base,
 	struct walk_tree_context *walk_tree_ctx = cbdata;
 	char *name;
 	struct strbuf fullpath = STRBUF_INIT;
+	struct strbuf linkpath = STRBUF_INIT;
 	struct strbuf class = STRBUF_INIT;
 	enum object_type type;
 	unsigned long size = 0;
+	char *buf;
 
 	name = xstrdup(pathname);
 	strbuf_addf(&fullpath, "%s%s%s", ctx.qry.path ? ctx.qry.path : "",
@@ -216,8 +220,7 @@ static int ls_item(const struct object_id *oid, struct strbuf *base,
 			htmlf("<tr><td colspan='3'>Bad object: %s %s</td></tr>",
 			      name,
 			      oid_to_hex(oid));
-			free(name);
-			return 0;
+			goto cleanup;
 		}
 	}
 
@@ -237,22 +240,45 @@ static int ls_item(const struct object_id *oid, struct strbuf *base,
 		cgit_tree_link(name, NULL, class.buf, ctx.qry.head,
 			       walk_tree_ctx->curr_rev, fullpath.buf);
 	}
+	if (S_ISLNK(mode)) {
+		html(" -> ");
+		buf = read_object_file(oid, &type, &size);
+		if (!buf) {
+			htmlf("Error reading object: %s", oid_to_hex(oid));
+			goto cleanup;
+		}
+		strbuf_addbuf(&linkpath, &fullpath);
+		strbuf_addf(&linkpath, "/../%s", buf);
+		strbuf_normalize_path(&linkpath);
+		cgit_tree_link(buf, NULL, class.buf, ctx.qry.head,
+			walk_tree_ctx->curr_rev, linkpath.buf);
+		free(buf);
+		strbuf_release(&linkpath);
+	}
 	htmlf("</td><td class='ls-size'>%li</td>", size);
 
 	html("<td>");
 	cgit_log_link("log", NULL, "button", ctx.qry.head,
 		      walk_tree_ctx->curr_rev, fullpath.buf, 0, NULL, NULL,
 		      ctx.qry.showmsg, 0);
-	if (ctx.repo->max_stats)
+	if (ctx.repo->max_stats) {
+		html(" ");
 		cgit_stats_link("stats", NULL, "button", ctx.qry.head,
 				fullpath.buf);
-	if (!S_ISGITLINK(mode))
+	}
+	if (!S_ISGITLINK(mode)) {
+		html(" ");
 		cgit_plain_link("plain", NULL, "button", ctx.qry.head,
 				walk_tree_ctx->curr_rev, fullpath.buf);
-	if (!S_ISDIR(mode) && ctx.repo->enable_blame)
+	}
+	if (!S_ISDIR(mode) && ctx.repo->enable_blame) {
+		html(" ");
 		cgit_blame_link("blame", NULL, "button", ctx.qry.head,
 				walk_tree_ctx->curr_rev, fullpath.buf);
+	}
 	html("</td></tr>\n");
+
+cleanup:
 	free(name);
 	strbuf_release(&fullpath);
 	strbuf_release(&class);