Commit Diff


commit - 8087c3c56bccad29ed74f1200794a00d923b62f8
commit + 87f9ebf5e2e3e64f8f5da0f572efe6ef1ce51ace
blob - 9f6342de645d8a808670982c188a766e1a3e7974
blob + 231a31485f7e1ba1ef6d22ad62715a94b5e57b10
--- gotweb/files/htdocs/gotweb/gotweb.css
+++ gotweb/files/htdocs/gotweb/gotweb.css
@@ -231,11 +231,12 @@ body {
 	width: 100%;
 }
 #tags_age {
-	float: left;
-	width: 7.5em;
 	padding-left: 10px;
 	padding-top: 5px;
 	padding-bottom: 5px;
+	float: left;
+	width: 7.5em;
+	overflow: auto;
 }
 #tag {
 	float: left;
@@ -246,6 +247,7 @@ body {
 }
 #tag_name {
 	float: left;
+	padding-left: 10px;
 	padding-right: 10px;
 	padding-top: 5px;
 	padding-bottom: 5px;
@@ -256,11 +258,12 @@ body {
 	width: 100%;
 }
 #heads_age {
-	float: left;
-	width: 7.5em;
 	padding-left: 10px;
 	padding-top: 5px;
 	padding-bottom: 5px;
+	float: left;
+	width: 7.5em;
+	overflow: auto;
 }
 #head {
 	float: left;
@@ -446,11 +449,6 @@ body {
 
 /* logs.tmpl */
 
-#logs_wrapper {
-	clear: left;
-	float: left;
-	width: 100%;
-}
 #logs_title_wrapper {
 	clear: left;
 	float: left;
@@ -486,11 +484,6 @@ body {
 
 /* tree.tmpl */
 
-#log_tree_wrapper {
-	clear: left;
-	float: left;
-	width: 100%;
-}
 #log_tree_title_wrapper {
 	clear: left;
 	float: left;
@@ -525,15 +518,11 @@ body {
 	clear: left;
 	float: left;
 	padding: 20px;
+	font-family: monospace;
 }
 
 /* commit.tmpl */
 
-#log_commit_wrapper {
-	clear: left;
-	float: left;
-	width: 100%;
-}
 #log_commit_title_wrapper {
 	clear: left;
 	float: left;
@@ -568,8 +557,48 @@ body {
 	clear: left;
 	float: left;
 	padding: 20px;
+	font-family: monospace;
 }
 
+/* diff.tmpl */
+
+#log_diff_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#log_diff_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#log_diff_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#log_diff_row_wrapper {
+	clear: left;
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#log_commit_diff {
+	clear: left;
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+}
+#log_diff {
+	clear: left;
+	float: left;
+	padding: 20px;
+	font-family: monospace;
+}
+
 /* summary.tmpl */
 
 #summary_wrapper {
blob - 5da34cc020cd786d0b2691068406cc3c797d4360
blob + 56e19969650c089c3bfb09e8a3e8c17f8743800b
--- gotweb/gotweb.c
+++ gotweb/gotweb.c
@@ -114,8 +114,15 @@ enum logs {
 	LOGCOMMIT,
 	LOGFULL,
 	LOGTREE,
+	LOGDIFF,
+	LOGBLAME,
 };
 
+enum tags {
+	TAGBRIEF,
+	TAGFULL,
+};
+
 struct buf {
 	/* buffer handle, buffer size, and data length */
 	u_char	*cb_buf;
@@ -153,7 +160,7 @@ static char			*gw_get_repo_age(struct trans *,
 				    char *, char *, int);
 static char			*gw_get_repo_log(struct trans *, const char *,
 				    char *, int, int);
-static char			*gw_get_repo_tags(struct trans *);
+static char			*gw_get_repo_tags(struct trans *, int, int);
 static char			*gw_get_repo_heads(struct trans *);
 static char			*gw_get_clone_url(struct trans *, char *);
 static char			*gw_get_got_link(struct trans *);
@@ -168,6 +175,9 @@ static void			 gw_display_index(struct trans *,
 static int			 gw_template(size_t, void *);
 
 static const struct got_error*	 apply_unveil(const char *, const char *);
+static const struct got_error*	 cmp_tags(void *, int *,
+				    struct got_reference *,
+				    struct got_reference *);
 static const struct got_error*	 gw_load_got_paths(struct trans *);
 static const struct got_error*	 gw_load_got_path(struct trans *,
 				    struct gw_dir *);
@@ -257,6 +267,52 @@ apply_unveil(const char *repo_path, const char *repo_f
 		return got_error_from_errno("unveil");
 
 	return NULL;
+}
+
+static const struct got_error *
+cmp_tags(void *arg, int *cmp, struct got_reference *ref1,
+    struct got_reference *ref2)
+{
+	const struct got_error *err = NULL;
+	struct got_repository *repo = arg;
+	struct got_object_id *id1, *id2 = NULL;
+	struct got_tag_object *tag1 = NULL, *tag2 = NULL;
+	time_t time1, time2;
+
+	*cmp = 0;
+
+	err = got_ref_resolve(&id1, repo, ref1);
+	if (err)
+		return err;
+	err = got_object_open_as_tag(&tag1, repo, id1);
+	if (err)
+		goto done;
+
+	err = got_ref_resolve(&id2, repo, ref2);
+	if (err)
+		goto done;
+	err = got_object_open_as_tag(&tag2, repo, id2);
+	if (err)
+		goto done;
+
+	time1 = got_object_tag_get_tagger_time(tag1);
+	time2 = got_object_tag_get_tagger_time(tag2);
+
+	/* Put latest tags first. */
+	if (time1 < time2)
+		*cmp = 1;
+	else if (time1 > time2)
+		*cmp = -1;
+	else
+		err = got_ref_cmp_by_name(NULL, cmp, ref2, ref1);
+done:
+	free(id1);
+	free(id2);
+	if (tag1)
+		got_object_tag_close(tag1);
+	if (tag2)
+		got_object_tag_close(tag2);
+	return err;
 }
 
 static const struct got_error *
@@ -309,7 +365,21 @@ static const struct got_error *
 gw_commitdiff(struct trans *gw_trans)
 {
 	const struct got_error *error = NULL;
+	char *log, *log_html;
+
+	error = apply_unveil(gw_trans->gw_dir->path, NULL);
+	if (error)
+		return error;
 
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit, 1, LOGDIFF);
+
+	if (log != NULL && strcmp(log, "") != 0) {
+		if ((asprintf(&log_html, log_diff, log)) == -1)
+			return got_error_from_errno("asprintf");
+		khttp_puts(gw_trans->gw_req, log_html);
+		free(log_html);
+		free(log);
+	}
 	return error;
 }
 
@@ -423,7 +493,7 @@ gw_log(struct trans *gw_trans)
 	if (error)
 		return error;
 
-	log = gw_get_repo_log(gw_trans, NULL, NULL,
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit,
 	    gw_trans->gw_conf->got_max_commits_display, LOGFULL);
 
 	if (log != NULL && strcmp(log, "") != 0) {
@@ -454,7 +524,7 @@ gw_logbriefs(struct trans *gw_trans)
 	if (error)
 		return error;
 
-	log = gw_get_repo_log(gw_trans, NULL, NULL,
+	log = gw_get_repo_log(gw_trans, NULL, gw_trans->commit,
 	    gw_trans->gw_conf->got_max_commits_display, LOGBRIEF);
 
 	if (log != NULL && strcmp(log, "") != 0) {
@@ -540,7 +610,7 @@ gw_summary(struct trans *gw_trans)
 	khttp_puts(gw_trans->gw_req, div_end);
 
 	log = gw_get_repo_log(gw_trans, NULL, NULL, D_MAXSLCOMMDISP, 0);
-	tags = gw_get_repo_tags(gw_trans);
+	tags = gw_get_repo_tags(gw_trans, D_MAXSLCOMMDISP, TAGBRIEF);
 	heads = gw_get_repo_heads(gw_trans);
 
 	if (log != NULL && strcmp(log, "") != 0) {
@@ -1026,11 +1096,16 @@ gw_get_repo_age(struct trans *gw_trans, char *dir, cha
 	struct got_reflist_head refs;
 	struct got_reflist_entry *re;
 	struct got_reference *head_ref;
+	int is_head = 0;
 	time_t committer_time = 0, cmp_time = 0;
+	const char *refname;
 	char *repo_age = NULL;
 
 	if (repo_ref == NULL)
 		return NULL;
+
+	if (strncmp(repo_ref, "refs/heads/", 11) == 0)
+		is_head = 1;
 
 	SIMPLEQ_INIT(&refs);
 	if (gw_trans->gw_conf->got_show_repo_age == false) {
@@ -1038,18 +1113,25 @@ gw_get_repo_age(struct trans *gw_trans, char *dir, cha
 			return NULL;
 		return repo_age;
 	}
+
 	error = got_repo_open(&repo, dir, NULL);
 	if (error != NULL)
 		goto err;
 
-	error = got_ref_list(&refs, repo, repo_ref, got_ref_cmp_by_name,
-	    NULL);
+	if (is_head)
+		error = got_ref_list(&refs, repo, "refs/heads",
+		    got_ref_cmp_by_name, NULL);
+	else
+		error = got_ref_list(&refs, repo, repo_ref,
+		    got_ref_cmp_by_name, NULL);
 	if (error != NULL)
 		goto err;
 
-	const char *refname;
 	SIMPLEQ_FOREACH(re, &refs, entry) {
-		refname = got_ref_get_name(re->ref);
+		if (is_head)
+			refname = strdup(repo_ref);
+		else
+			refname = got_ref_get_name(re->ref);
 		error = got_ref_open(&head_ref, repo, refname, 0);
 		if (error != NULL)
 			goto err;
@@ -1059,7 +1141,6 @@ gw_get_repo_age(struct trans *gw_trans, char *dir, cha
 		if (error != NULL)
 			goto err;
 
-		/*  here is what breaks tags, so adjust */
 		error = got_object_open_as_commit(&commit, repo, id);
 		if (error != NULL)
 			goto err;
@@ -1184,18 +1265,20 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 	struct got_object_id *id1 = NULL, *id2 = NULL;
 	struct got_object_qid *parent_id;
 	struct got_commit_graph *graph = NULL;
-	char *logs = NULL, *id_str1 = NULL, *id_str2 = NULL,
-	     *path = NULL, *refs_str_disp = NULL,
-	     *refs_str = NULL, *in_repo_path = NULL, *commit_row = NULL,
-	     *commit_commit = NULL, *commit_commit_disp = NULL,
-	     *commit_age_diff = NULL, *commit_age_diff_disp = NULL,
-	     *commit_age_long = NULL, *commit_age_long_disp = NULL,
-	     *commit_author = NULL, *commit_author_disp = NULL,
-	     *commit_committer = NULL, *commit_committer_disp = NULL,
-	     *commit_log = NULL, *commit_log_disp = NULL,
-	     *commit_parent = NULL, *commit_diff_disp = NULL, *commit_log0,
-	     *newline, *logbriefs_navs_html = NULL,
-	     *log_tree_html = NULL, *log_commit_html = NULL;
+	char *logs = NULL, *id_str1 = NULL, *id_str2 = NULL, *path = NULL,
+	     *in_repo_path = NULL, *refs_str = NULL, *refs_str_disp = NULL,
+	     *treeid = NULL, *commit_row = NULL, *commit_commit = NULL,
+	     *commit_commit_disp = NULL, *commit_age_diff = NULL,
+	     *commit_age_diff_disp = NULL, *commit_age_long = NULL,
+	     *commit_age_long_disp = NULL, *commit_author = NULL,
+	     *commit_author_disp = NULL, *commit_committer = NULL,
+	     *commit_committer_disp = NULL, *commit_log = NULL,
+	     *commit_log_disp = NULL, *commit_parent = NULL,
+	     *commit_diff_disp = NULL, *logbriefs_navs_html = NULL,
+	     *log_tree_html = NULL, *log_commit_html = NULL,
+	     *log_diff_html = NULL, *commit_tree = NULL,
+	     *commit_tree_disp = NULL;
+	char *commit_log0, *newline;
 	regex_t regex;
 	int have_match;
 	size_t newsize;
@@ -1396,12 +1479,17 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 			}
 		}
 
-		/* commit id */
 		error = got_object_id_str(&id_str1, id1);
 		if (error)
 			goto done;
 
-		if (gw_trans->action == GW_COMMIT) {
+		error = got_object_id_str(&treeid,
+		    got_object_commit_get_tree_id(commit));
+		if (error)
+			goto done;
+
+		if (gw_trans->action == GW_COMMIT ||
+		    gw_trans->action == GW_COMMITDIFF) {
 			parent_id =
 			    SIMPLEQ_FIRST(
 			    got_object_commit_get_parent_ids(commit));
@@ -1411,6 +1499,7 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 				error = got_object_id_str(&id_str2, id2);
 				if (error)
 					goto done;
+				free(id2);
 			} else
 				id_str2 = strdup("/dev/null");
 		}
@@ -1423,6 +1512,17 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 			goto done;
 		}
 
+		if ((asprintf(&commit_tree, "%s", treeid)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if ((asprintf(&commit_tree_disp, commit_tree_html,
+		    treeid)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
 		if ((asprintf(&commit_diff_disp, commit_diff_html, id_str2,
 			id_str1)) == -1) {
 			error = got_error_from_errno("asprintf");
@@ -1496,12 +1596,13 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 		error = got_object_commit_get_logmsg(&commit_log0, commit);
 		if (error)
 			goto done;
+
+		commit_log = commit_log0;
+		while (*commit_log == '\n')
+			commit_log++;
 
 		switch(log_type) {
 		case (LOGBRIEF):
-			commit_log = commit_log0;
-			while (*commit_log == '\n')
-				commit_log++;
 			newline = strchr(commit_log, '\n');
 			if (newline)
 				*newline = '\0';
@@ -1525,13 +1626,6 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 			logbriefs_navs_html = NULL;
 			break;
 		case (LOGFULL):
-			commit_log = commit_log0;
-			while (*commit_log == '\n')
-				commit_log++;
-			newline = strchr(commit_log, '\n');
-			if (newline)
-				*newline = '\n';
-
 			if ((asprintf(&logbriefs_navs_html, logbriefs_navs,
 			    gw_trans->repo_name, id_str1, gw_trans->repo_name,
 			    id_str1, gw_trans->repo_name, id_str1,
@@ -1552,12 +1646,10 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 			logbriefs_navs_html = NULL;
 			break;
 		case (LOGTREE):
-			commit_log = gw_html_escape(commit_log0);
-
 			log_tree_html = strdup("log tree here");
 
-			if ((asprintf(&commit_row, log_tree_row, commit_log,
-			    log_tree_html)) == -1) {
+			if ((asprintf(&commit_row, log_tree_row,
+			    gw_html_escape(commit_log), log_tree_html)) == -1) {
 				error = got_error_from_errno("asprintf");
 				goto done;
 			}
@@ -1565,13 +1657,6 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 			free(log_tree_html);
 			break;
 		case (LOGCOMMIT):
-			commit_log = commit_log0;
-			while (*commit_log == '\n')
-				commit_log++;
-			newline = strchr(commit_log, '\n');
-			if (newline)
-				*newline = '\n';
-
 			if ((asprintf(&commit_log_disp, commit_log_html,
 			    gw_html_escape(commit_log))) == -1) {
 				error = got_error_from_errno("asprintf");
@@ -1582,9 +1667,9 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 
 			if ((asprintf(&commit_row, log_commit_row,
 			    commit_diff_disp, commit_commit_disp,
-			    commit_author_disp, commit_committer_disp,
-			    commit_age_long_disp, commit_log_disp,
-			    log_commit_html)) == -1) {
+			    commit_tree_disp, commit_author_disp,
+			    commit_committer_disp, commit_age_long_disp,
+			    commit_log_disp, log_commit_html)) == -1) {
 				error = got_error_from_errno("asprintf");
 				goto done;
 			}
@@ -1592,6 +1677,27 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 			free(log_commit_html);
 
 			break;
+		case (LOGDIFF):
+			if ((asprintf(&commit_log_disp, commit_log_html,
+			    gw_html_escape(commit_log))) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			log_diff_html = strdup("diff here");
+
+			if ((asprintf(&commit_row, log_diff_row,
+			    commit_diff_disp, commit_commit_disp,
+			    commit_tree_disp, commit_author_disp,
+			    commit_committer_disp, commit_age_long_disp,
+			    commit_log_disp, log_diff_html)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+			free(commit_log_disp);
+			free(log_diff_html);
+
+			break;
 		default:
 			return NULL;
 		}
@@ -1601,6 +1707,7 @@ gw_get_repo_log(struct trans *gw_trans, const char *se
 
 		free(commit_parent);
 		free(commit_diff_disp);
+		free(commit_tree_disp);
 		free(commit_age_diff);
 		free(commit_age_diff_disp);
 		free(commit_age_long);
@@ -1649,22 +1756,260 @@ done:
 }
 
 static char *
-gw_get_repo_tags(struct trans *gw_trans)
+gw_get_repo_tags(struct trans *gw_trans, int limit, int tag_type)
 {
-	char *tags = NULL;
+	const struct got_error *error = NULL;
+	struct got_repository *repo = NULL;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
+	char *tags = NULL, *tag_row = NULL, *tags_navs_disp = NULL,
+	     *age = NULL;
+	char *newline;
+	struct buf *diffbuf = NULL;
+	size_t newsize;
 
-	asprintf(&tags, tags_row, "30 min ago", "1.0.0", "tag 1.0.0",
-	    tags_navs);
-	return tags;
+	error = buf_alloc(&diffbuf, BUFFER_SIZE);
+	if (error != NULL)
+		goto done;
+
+	error = got_repo_open(&repo, gw_trans->repo_path, NULL);
+	if (error != NULL)
+		goto done;
+
+	SIMPLEQ_INIT(&refs);
+
+	error = got_ref_list(&refs, repo, "refs/tags", cmp_tags, repo);
+	if (error)
+		goto done;
+
+	SIMPLEQ_FOREACH(re, &refs, entry) {
+		const char *refname;
+		char *refstr, *tag_log0, *tag_log, *id_str;
+		time_t tagger_time;
+		struct got_object_id *id;
+		struct got_tag_object *tag;
+
+		refname = got_ref_get_name(re->ref);
+		if (strncmp(refname, "refs/tags/", 10) != 0)
+			continue;
+		refname += 10;
+		refstr = got_ref_to_str(re->ref);
+		if (refstr == NULL) {
+			error = got_error_from_errno("got_ref_to_str");
+			goto done;
+		}
+
+		error = got_ref_resolve(&id, repo, re->ref);
+		if (error)
+			goto done;
+		error = got_object_open_as_tag(&tag, repo, id);
+		free(id);
+		if (error)
+			goto done;
+
+		tagger_time = got_object_tag_get_tagger_time(tag);
+
+		error = got_object_id_str(&id_str,
+		    got_object_tag_get_object_id(tag));
+		if (error)
+			goto done;
+
+		tag_log0 = strdup(got_object_tag_get_message(tag));
+
+		got_object_tag_close(tag);
+		if (tag_log0 == NULL) {
+			error = got_error_from_errno("strdup");
+			goto done;
+		}
+
+		tag_log = tag_log0;
+		while (*tag_log == '\n')
+			tag_log++;
+
+		switch (tag_type) {
+		case TAGBRIEF:
+			newline = strchr(tag_log, '\n');
+			if (newline)
+				*newline = '\0';
+
+			if ((asprintf(&age, "%s", gw_get_time_str(tagger_time,
+			    TM_DIFF))) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			if ((asprintf(&tags_navs_disp, tags_navs,
+			    gw_trans->repo_name, id_str, gw_trans->repo_name,
+			    id_str, gw_trans->repo_name, id_str,
+			    gw_trans->repo_name, id_str)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			if ((asprintf(&tag_row, tags_row, age, refname, tag_log,
+			    tags_navs_disp)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			free(tags_navs_disp);
+			break;
+		case TAGFULL:
+			break;
+		default:
+			break;
+		}
+		/* /1* printf("%stag %s %s\n", GOT_COMMIT_SEP_STR, refname, refstr); *1/ */
+		/* free(refstr); */
+
+		/* error = got_ref_resolve(&id, repo, re->ref); */
+		/* if (error) */
+		/* 	break; */
+		/* error = got_object_open_as_tag(&tag, repo, id); */
+		/* free(id); */
+		/* if (error) */
+		/* 	break; */
+		/* /1* printf("from: %s\n", got_object_tag_get_tagger(tag)); *1/ */
+		/* tagger_time = got_object_tag_get_tagger_time(tag); */
+		/* /1* datestr = get_datestr(&tagger_time, datebuf); *1/ */
+		/* /1* if (datestr) *1/ */
+		/* /1* 	printf("date: %s UTC\n", datestr); *1/ */
+		/* error = got_object_id_str(&id_str, */
+		/*     got_object_tag_get_object_id(tag)); */
+		/* if (error) */
+		/* 	break; */
+		/* switch (got_object_tag_get_object_type(tag)) { */
+		/* case GOT_OBJ_TYPE_BLOB: */
+		/* 	/1* printf("object: %s %s\n", GOT_OBJ_LABEL_BLOB, id_str); *1/ */
+		/* 	break; */
+		/* case GOT_OBJ_TYPE_TREE: */
+		/* 	/1* printf("object: %s %s\n", GOT_OBJ_LABEL_TREE, id_str); *1/ */
+		/* 	break; */
+		/* case GOT_OBJ_TYPE_COMMIT: */
+		/* 	/1* printf("object: %s %s\n", GOT_OBJ_LABEL_COMMIT, id_str); *1/ */
+		/* 	break; */
+		/* case GOT_OBJ_TYPE_TAG: */
+		/* 	/1* printf("object: %s %s\n", GOT_OBJ_LABEL_TAG, id_str); *1/ */
+		/* 	break; */
+		/* default: */
+		/* 	break; */
+		/* } */
+		/* free(id_str); */
+		/* tagmsg0 = strdup(got_object_tag_get_message(tag)); */
+		/* got_object_tag_close(tag); */
+		/* if (tagmsg0 == NULL) { */
+		/* 	error = got_error_from_errno("strdup"); */
+		/* 	break; */
+		/* } */
+
+		/* tagmsg = tagmsg0; */
+		/* do { */
+		/* 	line = strsep(&tagmsg, "\n"); */
+		/* 	if (line) */
+		/* 		printf(" %s\n", line); */
+		/* } while (line); */
+		/* free(tagmsg0); */
+		/* error = buf_append(&newsize, diffbuf, tags_row, */
+		/*     strlen(tags_row)); */
+		error = buf_append(&newsize, diffbuf, tag_row, strlen(tag_row));
+
+		free(tag_log0);
+		free(tag_row);
+		free(age);
+		free(id_str);
+		id_str = NULL;
+
+		if (error || (limit && --limit == 0))
+			break;
+	}
+	tags = strdup(diffbuf->cb_buf);
+done:
+	buf_free(diffbuf);
+	got_ref_list_free(&refs);
+	if (repo)
+		got_repo_close(repo);
+	if (error)
+		return NULL;
+	else
+		return tags;
 }
 
 static char *
 gw_get_repo_heads(struct trans *gw_trans)
 {
-	char *heads = NULL;
+	const struct got_error *error = NULL;
+	struct got_repository *repo = NULL;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
+	char *heads, *head_row = NULL, *head_navs_disp = NULL, *age = NULL;
+	struct buf *diffbuf = NULL;
+	size_t newsize;
 
-	asprintf(&heads, heads_row, "30 min ago", "master", heads_navs);
-	return heads;
+	error = buf_alloc(&diffbuf, BUFFER_SIZE);
+	if (error != NULL)
+		goto done;
+
+	error = got_repo_open(&repo, gw_trans->repo_path, NULL);
+	if (error != NULL)
+		goto done;
+
+	SIMPLEQ_INIT(&refs);
+	error = got_ref_list(&refs, repo, "refs/heads", got_ref_cmp_by_name,
+	    NULL);
+	if (error)
+		goto done;
+
+	SIMPLEQ_FOREACH(re, &refs, entry) {
+		char *refname;
+
+		refname = strdup(got_ref_get_name(re->ref));
+		if (refname == NULL) {
+			error = got_error_from_errno("got_ref_to_str");
+			goto done;
+		}
+
+		if (strncmp(refname, "refs/heads/", 11) != 0) {
+			free(refname);
+			continue;
+		}
+
+		age = gw_get_repo_age(gw_trans, gw_trans->gw_dir->path, refname,
+		    TM_DIFF);
+
+		if ((asprintf(&head_navs_disp, heads_navs, gw_trans->repo_name,
+		    refname, gw_trans->repo_name, refname,
+		    gw_trans->repo_name, refname, gw_trans->repo_name,
+		    refname)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		if (strncmp(refname, "refs/heads/", 11) == 0)
+			refname += 11;
+
+		if ((asprintf(&head_row, heads_row, age, refname,
+		    head_navs_disp)) == -1) {
+			error = got_error_from_errno("asprintf");
+			goto done;
+		}
+
+		error = buf_append(&newsize, diffbuf, head_row,
+		    strlen(head_row));
+
+		free(head_navs_disp);
+		free(head_row);
+	}
+
+	heads = strdup(diffbuf->cb_buf);
+done:
+	buf_free(diffbuf);
+	got_ref_list_free(&refs);
+	if (repo)
+		got_repo_close(repo);
+	if (error)
+		return NULL;
+	else
+		return heads;
 }
 
 static char *
blob - 9f8f89567c941bf2dd7ec45f6aaa139d6f190bc3
blob + a205f7481950727bdf8a85c169eb6c4d45882c3d
--- gotweb/gotweb_ui.h
+++ gotweb/gotweb_ui.h
@@ -50,13 +50,13 @@ char *site_owner =
 	"<div id='site_owner_wrapper'><div id='site_owner'>%s</div></div>";
 
 char *search =
-	"<div id='search'>" \
+	"<!--/* <div id='search'>" \
 	"<form method='POST'>" \
 	"<input type='search' id='got-search' name='got-search' size='15'" \
 	    " maxlength='50' />" \
 	"<button>Search</button>" \
 	"</form>" \
-	"</div>";
+	"</div> */-->";
 
 char *np_wrapper_start =
 	"<div id='np_wrapper'>" \
@@ -104,13 +104,13 @@ char *logbriefs_row =
 char *logbriefs_navs =
 	"<a href='?path=%s&action=commit&commit=%s'>commit</a> | " \
 	"<a href='?path=%s&action=commitdiff&commit=%s'>diff</a> | " \
-	"<a href='?path=%s&action=tree&commit=%s'>tree</a> | " \
-	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a>";
+	"<a href='?path=%s&action=tree&commit=%s'>tree</a><!--/* | " \
+	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a> */-->";
 
 char *tags_row =
 	"<div id='tags_wrapper'>" \
 	"<div id='tags_age'>%s</div>" \
-	"<div id='tag'>%s</div>" \
+	"<div id='tag'>tag %s</div>" \
 	"<div id='tag_name'>%s</div>" \
 	"</div>" \
 	"<div id='navs_wrapper'>" \
@@ -137,6 +137,7 @@ char *heads_row =
 	"<div id='dotted_line'></div>";
 
 char *heads_navs =
+	"<a href='?path=%s&action=summary&headref=%s'>summary</a> | " \
 	"<a href='?path=%s&action=logbriefs&headref=%s'>log briefs</a> | " \
 	"<a href='?path=%s&action=log&headref=%s'>log</a> | " \
 	"<a href='?path=%s&action=tree&headref=%s'>commit</a>";
@@ -165,11 +166,12 @@ char *commit_log_html =
 	"<div id='commit_log_title'>Log:</div>" \
 	"<div id='commit_log'>%s</div>";
 
+char *commit_tree_html =
+	"<div id='commit_log_title'>Tree:</div>" \
+	"<div id='commit_log'>%s</div>";
+
 /* log.tmpl */
 
-char *logs_wrapper =
-	"<div id='logs_wrapper'>";
-
 char *logs =
 	"<div id='logs_title_wrapper'>" \
 	"<div id='logs_title'>Commits</div></div>" \
@@ -188,14 +190,11 @@ char *logs_row =
 char *logs_navs =
 	"<a href='?path=%s&action=commit&commit=%s'>commit</a> | " \
 	"<a href='?path=%s&action=commitdiff&commit=%s'>diff</a> | " \
-	"<a href='?path=%s&action=tree&commit=%s'>tree</a> | " \
-	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a>";
+	"<a href='?path=%s&action=tree&commit=%s'>tree</a><!--/* | " \
+	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a> */-->";
 
 /* tree.tmpl */
 
-char *log_tree_wrapper =
-	"<div id='log_tree_wrapper'>";
-
 char *log_tree =
 	"<div id='log_tree_title_wrapper'>" \
 	"<div id='log_tree_title'>Tree</div></div>" \
@@ -212,14 +211,11 @@ char *log_tree_row =
 char *log_tree_navs =
 	"<a href='?path=%s&action=commit&commit=%s'>commit</a> | " \
 	"<a href='?path=%s&action=commitdiff&commit=%s'>diff</a> | " \
-	"<a href='?path=%s&action=tree&commit=%s'>tree</a> | " \
-	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a>";
+	"<a href='?path=%s&action=tree&commit=%s'>tree</a><!--/* | " \
+	"<a href='?path=%s&action=snapshot&commit=%s'>snapshot</a> */-->";
 
 /* commit.tmpl */
 
-char *log_commit_wrapper =
-	"<div id='log_commit_wrapper'>";
-
 char *log_commit =
 	"<div id='log_commit_title_wrapper'>" \
 	"<div id='log_commit_title'>Commit</div></div>" \
@@ -227,12 +223,27 @@ char *log_commit =
 
 char *log_commit_row =
 	"<div id='log_commit_row_wrapper'>" \
-	"<div id='log_commit_commit'>%s%s%s%s%s%s</div>" \
+	"<div id='log_commit_commit'>%s%s%s%s%s%s%s</div>" \
 	"</div>" \
 	"<div id='dotted_line'></div>" \
 	"<div id='log_commit'>%s</div>" \
 	"</div>";
 
+/* diff.tmpl */
+
+char *log_diff =
+	"<div id='log_diff_title_wrapper'>" \
+	"<div id='log_diff_title'>Commit Diff</div></div>" \
+	"<div id='log_diff_content'>%s</div>";
+
+char *log_diff_row =
+	"<div id='log_diff_row_wrapper'>" \
+	"<div id='log_commit_diff'>%s%s%s%s%s%s%s</div>" \
+	"</div>" \
+	"<div id='dotted_line'></div>" \
+	"<div id='log_diff'>%s</div>" \
+	"</div>";
+
 /* index.tmpl */
 
 char *index_projects_header =