[PATCH 1/3] ui-blame: add blame UI

John Keeping john at keeping.me.uk
Wed Aug 12 15:03:34 CEST 2015


This is disabled by default and needs to be turned on in the config
file.

Because libgit.a does not include the blame implementation (which lives
in git/builtin/blame.c), this is implemented by executing a git-blame
subprocess and parsing its output.  Given how expensive the blame
operation itself is, the overhead of using a separate process should not
be noticeable.
---
 cgit.c       |   4 +-
 cgit.css     |   4 ++
 cgit.h       |   3 +
 cgit.mk      |   1 +
 cgitrc.5.txt |   4 ++
 cmd.c        |   7 +++
 ui-blame.c   | 202 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 ui-blame.h   |   6 ++
 8 files changed, 230 insertions(+), 1 deletion(-)
 create mode 100644 ui-blame.c
 create mode 100644 ui-blame.h

diff --git a/cgit.c b/cgit.c
index ae413c6..fe952d4 100644
--- a/cgit.c
+++ b/cgit.c
@@ -150,6 +150,8 @@ static void config_cb(const char *name, const char *value)
 		ctx.cfg.noheader = atoi(value);
 	else if (!strcmp(name, "snapshots"))
 		ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
+	else if (!strcmp(name, "enable-blame"))
+		ctx.cfg.enable_blame = atoi(value);
 	else if (!strcmp(name, "enable-filter-overrides"))
 		ctx.cfg.enable_filter_overrides = atoi(value);
 	else if (!strcmp(name, "enable-http-clone"))
@@ -707,7 +709,7 @@ static void process_request(void)
 	}
 
 	cmd = cgit_get_cmd();
-	if (!cmd) {
+	if (!cmd || (!ctx.cfg.enable_blame && !strcmp(cmd->name, "blame"))) {
 		ctx.page.title = "cgit error";
 		ctx.page.status = 404;
 		ctx.page.statusmsg = "Not found";
diff --git a/cgit.css b/cgit.css
index 82c755c..39b8277 100644
--- a/cgit.css
+++ b/cgit.css
@@ -280,6 +280,10 @@ div#cgit table.blob td.lines {
 	color: black;
 }
 
+div#cgit table.blob td.noprevious {
+	font-weight: bold;
+}
+
 div#cgit table.blob td.linenumbers {
 	margin: 0; padding: 0 0.5em 0 0.5em;
 	vertical-align: top;
diff --git a/cgit.h b/cgit.h
index 16f8092..4347046 100644
--- a/cgit.h
+++ b/cgit.h
@@ -16,12 +16,14 @@
 #include <revision.h>
 #include <log-tree.h>
 #include <archive.h>
+#include <run-command.h>
 #include <string-list.h>
 #include <xdiff-interface.h>
 #include <xdiff/xdiff.h>
 #include <utf8.h>
 #include <notes.h>
 #include <graph.h>
+#include <quote.h>
 
 
 /*
@@ -220,6 +222,7 @@ struct cgit_config {
 	int cache_snapshot_ttl;
 	int case_sensitive_sort;
 	int embedded;
+	int enable_blame;
 	int enable_filter_overrides;
 	int enable_http_clone;
 	int enable_index_links;
diff --git a/cgit.mk b/cgit.mk
index 1b50307..c427229 100644
--- a/cgit.mk
+++ b/cgit.mk
@@ -75,6 +75,7 @@ CGIT_OBJ_NAMES += parsing.o
 CGIT_OBJ_NAMES += scan-tree.o
 CGIT_OBJ_NAMES += shared.o
 CGIT_OBJ_NAMES += ui-atom.o
+CGIT_OBJ_NAMES += ui-blame.o
 CGIT_OBJ_NAMES += ui-blob.o
 CGIT_OBJ_NAMES += ui-clone.o
 CGIT_OBJ_NAMES += ui-commit.o
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index e21ece9..a5a54ec 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -146,6 +146,10 @@ enable-commit-graph::
 	history graph to the left of the commit messages in the repository
 	log page. Default value: "0".
 
+enable-blame::
+	Flag which, when set to "1", enables the blame UI.  Default value:
+	"0".
+
 enable-filter-overrides::
 	Flag which, when set to "1", allows all filter settings to be
 	overridden in repository-specific cgitrc files. Default value: none.
diff --git a/cmd.c b/cmd.c
index 188cd56..ca27e8d 100644
--- a/cmd.c
+++ b/cmd.c
@@ -11,6 +11,7 @@
 #include "cache.h"
 #include "ui-shared.h"
 #include "ui-atom.h"
+#include "ui-blame.h"
 #include "ui-blob.h"
 #include "ui-clone.h"
 #include "ui-commit.h"
@@ -44,6 +45,11 @@ static void about_fn(void)
 		cgit_print_site_readme();
 }
 
+static void blame_fn(void)
+{
+	cgit_print_blame();
+}
+
 static void blob_fn(void)
 {
 	cgit_print_blob(ctx.qry.sha1, ctx.qry.path, ctx.qry.head, 0);
@@ -145,6 +151,7 @@ struct cgit_cmd *cgit_get_cmd(void)
 		def_cmd(HEAD, 1, 0, 0, 1),
 		def_cmd(atom, 1, 0, 0, 0),
 		def_cmd(about, 0, 1, 0, 0),
+		def_cmd(blame, 1, 1, 0, 0),
 		def_cmd(blob, 1, 0, 0, 0),
 		def_cmd(commit, 1, 1, 1, 0),
 		def_cmd(diff, 1, 1, 1, 0),
diff --git a/ui-blame.c b/ui-blame.c
new file mode 100644
index 0000000..34c836a
--- /dev/null
+++ b/ui-blame.c
@@ -0,0 +1,202 @@
+/* ui-blame.c: functions for showing blame output
+ *
+ * Copyright (C) 2015 cgit Development Team <cgit at lists.zx2c4.com>
+ *
+ * Licensed under GNU General Public License v2
+ *   (see COPYING for full license text)
+ */
+
+#include "cgit.h"
+#include "ui-blame.h"
+#include "html.h"
+#include "ui-shared.h"
+
+static void start_output(void)
+{
+	html("<table class='blob'>");
+}
+
+static void end_output(void)
+{
+	html("</table>");
+}
+
+static int parse_init_line(const char *line, unsigned char sha1[20],
+		unsigned long *orig_line, unsigned long *final_line,
+		unsigned long *count)
+{
+	char *end;
+	if (get_sha1_hex(line, sha1) < 0)
+		return -1;
+
+	*orig_line = strtoul(line + 41, &end, 10);
+	/* We already have a count, so there isn't one on this line. */
+	if (*count)
+		return 0;
+
+	end++;
+	*final_line = strtoul(end, &end, 10);
+
+	if (!*end)
+		return -1;
+	end++;
+	*count = strtoul(end, NULL, 10);
+
+	return 0;
+}
+
+static const char *author_abbrev(const char *author)
+{
+	static char buf[4];
+	size_t idx = 0;
+	const char *c = strrchr(author, ' ');
+	buf[idx++] = *author;
+	if (c && c != author)
+		buf[idx++] = c[1];
+	buf[idx] = '\0';
+	return buf;
+}
+
+static int process_blame(FILE *out)
+{
+	/*
+	 * The format is:
+	 *	<sha1> <orig-line> <cur-line> [<count>]
+	 *	<header> <value>
+	 *	...
+	 *	<TAB> <content>
+	 *
+	 * STATE_INIT means we expect a SHA1, otherwise it's a header or
+	 * content depending on whether or not the line starts with <TAB>.
+	 */
+	enum {
+		STATE_INIT,
+		STATE_HEADER,
+	} state = STATE_INIT;
+	int output_started = 0;
+	int first = 0;
+	int previous = 0;
+	unsigned long line = 1;
+	unsigned long orig_line = 0;
+	unsigned long final_line = 0;
+	unsigned long count = 0;
+	unsigned char sha1[20];
+	struct strbuf buf = STRBUF_INIT;
+	struct strbuf author = STRBUF_INIT;
+	time_t author_time = 0;
+	int author_tz;
+
+	while (strbuf_getline(&buf, out, '\n') != EOF) {
+		const char *value;
+		if (ferror(out))
+			break;
+
+		switch (state) {
+		case STATE_INIT:
+			first = !count;
+			previous = 0;
+
+			if (parse_init_line(buf.buf, sha1, &orig_line, &final_line, &count) < 0)
+				goto err;
+
+			if (!output_started) {
+				start_output();
+				output_started = 1;
+			}
+			html("<tr>");
+			state++;
+			break;
+
+		case STATE_HEADER:
+			if (buf.buf[0] == '\t') {
+				if (first) {
+					char date_buf[64];
+					strftime(date_buf, sizeof(date_buf) - 1, FMT_LONGDATE, gmtime(&author_time));
+					htmlf("<td rowspan='%lu' class='lines%s' title='",
+							count, previous ? "" : " noprevious");
+					html_attr(fmt("%s, %s", author.buf, date_buf));
+					htmlf("'><pre>");
+					cgit_commit_link(fmt("%s", find_unique_abbrev(sha1, DEFAULT_ABBREV)),
+						NULL, NULL, NULL, sha1_to_hex(sha1), ctx.qry.path);
+					if (count > 1) {
+						html("<br>");
+						html_txt(author_abbrev(author.buf));
+					}
+					html("</pre></td>");
+					first = 0;
+				}
+				htmlf("<td class='linenumbers'><a name='l%lu' href='#l%lu'>%lu</a></td><td class='lines'><pre>",
+						line, line, line);
+				html_txt(buf.buf + 1);
+				html("</pre></td></tr>");
+
+				line++;
+				count--;
+				state = STATE_INIT;
+			} else if (skip_prefix(buf.buf, "author ", &value)) {
+				strbuf_reset(&author);
+				strbuf_addstr(&author, value);
+			} else if (skip_prefix(buf.buf, "author-time ", &value)) {
+				author_time = (time_t) strtoul(value, NULL, 10);
+			} else if (skip_prefix(buf.buf, "author-tz ", &value)) {
+				strtol_i(value, 10, &author_tz);
+			} else if (skip_prefix(buf.buf, "previous ", &value)) {
+				previous++;
+			}
+			break;
+		}
+	}
+
+	return output_started;
+err:
+	return -1;
+}
+
+void cgit_print_blame(void)
+{
+	FILE *out;
+	int output_started = 0;
+	struct child_process proc = CHILD_PROCESS_INIT;
+
+	if (!ctx.qry.path) {
+		cgit_print_error("Bad request");
+		return;
+	}
+
+	argv_array_push(&proc.args, "blame");
+	argv_array_push(&proc.args, "--line-porcelain");
+	if (ctx.qry.ignorews)
+		argv_array_push(&proc.args, "-w");
+	argv_array_push(&proc.args, ctx.qry.sha1 ? ctx.qry.sha1 : ctx.qry.head);
+	argv_array_push(&proc.args, "--");
+	argv_array_push(&proc.args, ctx.qry.path);
+
+	proc.out = -1;
+	proc.no_stdin = 1;
+	proc.no_stderr = 1;
+	proc.git_cmd = 1;
+
+	if (start_command(&proc) < 0)
+		goto err;
+
+	out = fdopen(proc.out, "r");
+	if (!out)
+		goto err;
+
+	output_started = process_blame(out);
+
+	if (finish_command(&proc) != 0) {
+		if (!output_started) {
+			cgit_print_error("Not found");
+			return;
+		}
+		/* What to do? - we started getting data then something went wrong. */
+	}
+
+	if (output_started)
+		end_output();
+
+	return;
+err:
+	cgit_print_error("Internal server error");
+}
diff --git a/ui-blame.h b/ui-blame.h
new file mode 100644
index 0000000..5b97e03
--- /dev/null
+++ b/ui-blame.h
@@ -0,0 +1,6 @@
+#ifndef UI_BLAME_H
+#define UI_BLAME_H
+
+extern void cgit_print_blame(void);
+
+#endif /* UI_BLAME_H */
-- 
2.5.0.466.g9af26fa



More information about the CGit mailing list