summaryrefslogtreecommitdiffstatshomepage
path: root/ui-diff.c
blob: 263a7baee0bacdc01001d12abe70e4e202cb4fdc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/* ui-diff.c: show diff between two blobs
 *
 * Copyright (C) 2006 Lars Hjemli
 *
 * Licensed under GNU General Public License v2
 *   (see COPYING for full license text)
 */

#include "cgit.h"


unsigned char old_rev_sha1[20];
unsigned char new_rev_sha1[20];

/*
 * print a single line returned from xdiff
 */
static void print_line(char *line, int len)
{
	char *class = "ctx";
	char c = line[len-1];

	if (line[0] == '+')
		class = "add";
	else if (line[0] == '-')
		class = "del";
	else if (line[0] == '@')
		class = "hunk";

	htmlf("<div class='%s'>", class);
	line[len-1] = '\0';
	html_txt(line);
	html("</div>");
	line[len-1] = c;
}

static void header(unsigned char *sha1, char *path1, int mode1,
		   unsigned char *sha2, char *path2, int mode2)
{
	char *abbrev1, *abbrev2;
	int subproject;

	subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2));
	html("<div class='head'>");
	html("diff --git a/");
	html_txt(path1);
	html(" b/");
	html_txt(path2);

	if (is_null_sha1(sha1))
		path1 = "dev/null";
	if (is_null_sha1(sha2))
		path2 = "dev/null";

	if (mode1 == 0)
		htmlf("<br/>new file mode %.6o", mode2);

	if (mode2 == 0)
		htmlf("<br/>deleted file mode %.6o", mode1);

	if (!subproject) {
		abbrev1 = xstrdup(find_unique_abbrev(sha1, DEFAULT_ABBREV));
		abbrev2 = xstrdup(find_unique_abbrev(sha2, DEFAULT_ABBREV));
		htmlf("<br/>index %s..%s", abbrev1, abbrev2);
		free(abbrev1);
		free(abbrev2);
		if (mode1 != 0 && mode2 != 0) {
			htmlf(" %.6o", mode1);
			if (mode2 != mode1)
				htmlf("..%.6o", mode2);
		}
		html("<br/>--- a/");
		if (mode1 != 0)
			cgit_tree_link(path1, NULL, NULL, ctx.qry.head,
				       sha1_to_hex(old_rev_sha1), path1);
		else
			html_txt(path1);
		html("<br/>+++ b/");
		if (mode2 != 0)
			cgit_tree_link(path2, NULL, NULL, ctx.qry.head,
				       sha1_to_hex(new_rev_sha1), path2);
		else
			html_txt(path2);
	}
	html("</div>");
}

static void filepair_cb(struct diff_filepair *pair)
{
	header(pair->one->sha1, pair->one->path, pair->one->mode,
	       pair->two->sha1, pair->two->path, pair->two->mode);
	if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) {
		if (S_ISGITLINK(pair->one->mode))
			print_line(fmt("-Subproject %s", sha1_to_hex(pair->one->sha1)), 52);
		if (S_ISGITLINK(pair->two->mode))
			print_line(fmt("+Subproject %s", sha1_to_hex(pair->two->sha1)), 52);
		return;
	}
	if (cgit_diff_files(pair->one->sha1, pair->two->sha1, print_line))
		cgit_print_error("Error running diff");
}

void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefix)
{
	enum object_type type;
	unsigned long size;
	struct commit *commit, *commit2;

	if (!new_rev)
		new_rev = ctx.qry.head;
	get_sha1(new_rev, new_rev_sha1);
	type = sha1_object_info(new_rev_sha1, &size);
	if (type == OBJ_BAD) {
		cgit_print_error(fmt("Bad object name: %s", new_rev));
		return;
	}
	if (type != OBJ_COMMIT) {
		cgit_print_error(fmt("Unhandled object type: %s",
				     typename(type)));
		return;
	}

	commit = lookup_commit_reference(new_rev_sha1);
	if (!commit || parse_commit(commit))
		cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(new_rev_sha1)));

	if (old_rev)
		get_sha1(old_rev, old_rev_sha1);
	else if (commit->parents && commit->parents->item)
		hashcpy(old_rev_sha1, commit->parents->item->object.sha1);
	else
		hashclr(old_rev_sha1);

	if (!is_null_sha1(old_rev_sha1)) {
		type = sha1_object_info(old_rev_sha1, &size);
		if (type == OBJ_BAD) {
			cgit_print_error(fmt("Bad object name: %s", sha1_to_hex(old_rev_sha1)));
			return;
		}
		commit2 = lookup_commit_reference(old_rev_sha1);
		if (!commit2 || parse_commit(commit2))
			cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(old_rev_sha1)));
	}
	html("<table summary='diff' class='diff'>");
	html("<tr><td>");
	cgit_diff_tree(old_rev_sha1, new_rev_sha1, filepair_cb, prefix);
	html("</td></tr>");
	html("</table>");
}
n class="cm"> * that is correct for the commit they are showing, avoiding the * need to walk the entire history leading back to every commit we * show in order detect renames. */ if (0 != strcmp(ctx.qry.vpath, pair->two->path)) { free(ctx.qry.vpath); ctx.qry.vpath = xstrdup(pair->two->path); } inspect_files(pair); } static int show_commit(struct commit *commit, struct rev_info *revs) { struct commit_list *parents = commit->parents; struct commit *parent; int found = 0, saved_fmt; unsigned saved_flags = revs->diffopt.flags; /* Always show if we're not in "follow" mode with a single file. */ if (!ctx.qry.follow) return 1; /* * In "follow" mode, we don't show merges. This is consistent with * "git log --follow -- <file>". */ if (parents && parents->next) return 0; /* * If this is the root commit, do what rev_info tells us. */ if (!parents) return revs->show_root_diff; /* When we get here we have precisely one parent. */ parent = parents->item; /* If we can't parse the commit, let print_commit() report an error. */ if (parse_commit(parent)) return 1; files = 0; add_lines = 0; rem_lines = 0; DIFF_OPT_SET(&revs->diffopt, RECURSIVE); diff_tree_sha1(parent->tree->object.oid.hash, commit->tree->object.oid.hash, "", &revs->diffopt); diffcore_std(&revs->diffopt); found = !diff_queue_is_empty(); saved_fmt = revs->diffopt.output_format; revs->diffopt.output_format = DIFF_FORMAT_CALLBACK; revs->diffopt.format_callback = cgit_diff_tree_cb; revs->diffopt.format_callback_data = handle_rename; diff_flush(&revs->diffopt); revs->diffopt.output_format = saved_fmt; revs->diffopt.flags = saved_flags; lines_counted = 1; return found; } static void print_commit(struct commit *commit, struct rev_info *revs) { struct commitinfo *info; int columns = revs->graph ? 4 : 3; struct strbuf graphbuf = STRBUF_INIT; struct strbuf msgbuf = STRBUF_INIT; if (ctx.repo->enable_log_filecount) columns++; if (ctx.repo->enable_log_linecount) columns++; if (revs->graph) { /* Advance graph until current commit */ while (!graph_next_line(revs->graph, &graphbuf)) { /* Print graph segment in otherwise empty table row */ html("<tr class='nohover'><td class='commitgraph'>"); html(graphbuf.buf); htmlf("</td><td colspan='%d' /></tr>\n", columns); strbuf_setlen(&graphbuf, 0); } /* Current commit's graph segment is now ready in graphbuf */ } info = cgit_parse_commit(commit); htmlf("<tr%s>", ctx.qry.showmsg ? " class='logheader'" : ""); if (revs->graph) { /* Print graph segment for current commit */ html("<td class='commitgraph'>"); html(graphbuf.buf); html("</td>"); strbuf_setlen(&graphbuf, 0); } else { html("<td>"); cgit_print_age(info->committer_date, info->committer_tz, TM_WEEK * 2); html("</td>"); } htmlf("<td%s>", ctx.qry.showmsg ? " class='logsubject'" : ""); if (ctx.qry.showmsg) { /* line-wrap long commit subjects instead of truncating them */ size_t subject_len = strlen(info->subject); if (subject_len > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) { /* symbol for signaling line-wrap (in PAGE_ENCODING) */ const char wrap_symbol[] = { ' ', 0xE2, 0x86, 0xB5, 0 }; int i = ctx.cfg.max_msg_len - strlen(wrap_symbol); /* Rewind i to preceding space character */ while (i > 0 && !isspace(info->subject[i])) --i; if (!i) /* Oops, zero spaces. Reset i */ i = ctx.cfg.max_msg_len - strlen(wrap_symbol); /* add remainder starting at i to msgbuf */ strbuf_add(&msgbuf, info->subject + i, subject_len - i); strbuf_trim(&msgbuf); strbuf_add(&msgbuf, "\n\n", 2); /* Place wrap_symbol at position i in info->subject */ strcpy(info->subject + i, wrap_symbol); } } cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head, oid_to_hex(&commit->object.oid), ctx.qry.vpath); show_commit_decorations(commit); html("</td><td>"); cgit_open_filter(ctx.repo->email_filter, info->author_email, "log"); html_txt(info->author); cgit_close_filter(ctx.repo->email_filter); if (revs->graph) { html("</td><td>"); cgit_print_age(info->committer_date, info->committer_tz, TM_WEEK * 2); } if (!lines_counted && (ctx.repo->enable_log_filecount || ctx.repo->enable_log_linecount)) { files = 0; add_lines = 0; rem_lines = 0; cgit_diff_commit(commit, inspect_files, ctx.qry.vpath); } if (ctx.repo->enable_log_filecount) htmlf("</td><td>%d", files); if (ctx.repo->enable_log_linecount) htmlf("</td><td>-%d/+%d", rem_lines, add_lines); html("</td></tr>\n"); if (revs->graph || ctx.qry.showmsg) { /* Print a second table row */ html("<tr class='nohover'>"); if (ctx.qry.showmsg) { /* Concatenate commit message + notes in msgbuf */ if (info->msg && *(info->msg)) { strbuf_addstr(&msgbuf, info->msg); strbuf_addch(&msgbuf, '\n'); } format_display_notes(commit->object.oid.hash, &msgbuf, PAGE_ENCODING, 0); strbuf_addch(&msgbuf, '\n'); strbuf_ltrim(&msgbuf); } if (revs->graph) { int lines = 0; /* Calculate graph padding */ if (ctx.qry.showmsg) { /* Count #lines in commit message + notes */ const char *p = msgbuf.buf; lines = 1; while ((p = strchr(p, '\n'))) { p++; lines++; } } /* Print graph padding */ html("<td class='commitgraph'>"); while (lines > 0 || !graph_is_commit_finished(revs->graph)) { if (graphbuf.len) html("\n"); strbuf_setlen(&graphbuf, 0); graph_next_line(revs->graph, &graphbuf); html(graphbuf.buf); lines--; } html("</td>\n"); } else html("<td/>"); /* Empty 'Age' column */ /* Print msgbuf into remainder of table row */ htmlf("<td colspan='%d'%s>\n", columns - (revs->graph ? 1 : 0), ctx.qry.showmsg ? " class='logmsg'" : ""); html_txt(msgbuf.buf); html("</td></tr>\n"); } strbuf_release(&msgbuf); strbuf_release(&graphbuf); cgit_free_commitinfo(info); } static const char *disambiguate_ref(const char *ref, int *must_free_result) { struct object_id oid; struct strbuf longref = STRBUF_INIT; strbuf_addf(&longref, "refs/heads/%s", ref); if (get_sha1(longref.buf, oid.hash) == 0) { *must_free_result = 1; return strbuf_detach(&longref, NULL); } *must_free_result = 0; strbuf_release(&longref); return ref; } static char *next_token(char **src) { char *result; if (!src || !*src) return NULL; while (isspace(**src)) (*src)++; if (!**src) return NULL; result = *src; while (**src) { if (isspace(**src)) { **src = '\0'; (*src)++; break; } (*src)++; } return result; } void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern, char *path, int pager, int commit_graph, int commit_sort) { struct rev_info rev; struct commit *commit; struct argv_array rev_argv = ARGV_ARRAY_INIT; int i, columns = commit_graph ? 4 : 3; int must_free_tip = 0; /* rev_argv.argv[0] will be ignored by setup_revisions */ argv_array_push(&rev_argv, "log_rev_setup"); if (!tip) tip = ctx.qry.head; tip = disambiguate_ref(tip, &must_free_tip); argv_array_push(&rev_argv, tip); if (grep && pattern && *pattern) { pattern = xstrdup(pattern); if (!strcmp(grep, "grep") || !strcmp(grep, "author") || !strcmp(grep, "committer")) { argv_array_pushf(&rev_argv, "--%s=%s", grep, pattern); } else if (!strcmp(grep, "range")) { char *arg; /* Split the pattern at whitespace and add each token * as a revision expression. Do not accept other * rev-list options. Also, replace the previously * pushed tip (it's no longer relevant). */ argv_array_pop(&rev_argv); while ((arg = next_token(&pattern))) { if (*arg == '-') { fprintf(stderr, "Bad range expr: %s\n", arg); break; } argv_array_push(&rev_argv, arg); } } } if (!path || !ctx.cfg.enable_follow_links) { /* * If we don't have a path, "follow" is a no-op so make sure * the variable is set to false to avoid needing to check * both this and whether we have a path everywhere. */ ctx.qry.follow = 0; } if (commit_graph && !ctx.qry.follow) { argv_array_push(&rev_argv, "--graph"); argv_array_push(&rev_argv, "--color"); graph_set_column_colors(column_colors_html, COLUMN_COLORS_HTML_MAX); } if (commit_sort == 1) argv_array_push(&rev_argv, "--date-order"); else if (commit_sort == 2) argv_array_push(&rev_argv, "--topo-order"); if (path && ctx.qry.follow) argv_array_push(&rev_argv, "--follow"); argv_array_push(&rev_argv, "--"); if (path) argv_array_push(&rev_argv, path); init_revisions(&rev, NULL); rev.abbrev = DEFAULT_ABBREV; rev.commit_format = CMIT_FMT_DEFAULT; rev.verbose_header = 1; rev.show_root_diff = 0; rev.ignore_missing = 1; rev.simplify_history = 1; setup_revisions(rev_argv.argc, rev_argv.argv, &rev, NULL); load_ref_decorations(DECORATE_FULL_REFS); rev.show_decorations = 1; rev.grep_filter.regflags |= REG_ICASE; rev.diffopt.detect_rename = 1; rev.diffopt.rename_limit = ctx.cfg.renamelimit; if (ctx.qry.ignorews) DIFF_XDL_SET(&rev.diffopt, IGNORE_WHITESPACE); compile_grep_patterns(&rev.grep_filter); prepare_revision_walk(&rev); if (pager) { cgit_print_layout_start(); html("<table class='list nowrap'>"); } html("<tr class='nohover'>"); if (commit_graph) html("<th></th>"); else html("<th class='left'>Age</th>"); html("<th class='left'>Commit message"); if (pager) { html(" ("); cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL, NULL, ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath, ctx.qry.ofs, ctx.qry.grep, ctx.qry.search, ctx.qry.showmsg ? 0 : 1, ctx.qry.follow); html(")"); } html("</th><th class='left'>Author</th>"); if (rev.graph) html("<th class='left'>Age</th>"); if (ctx.repo->enable_log_filecount) { html("<th class='left'>Files</th>"); columns++; } if (ctx.repo->enable_log_linecount) { html("<th class='left'>Lines</th>"); columns++; } html("</tr>\n"); if (ofs<0) ofs = 0; for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; /* nop */) { if (show_commit(commit, &rev)) i++; free_commit_buffer(commit); free_commit_list(commit->parents); commit->parents = NULL; } for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; /* nop */) { /* * In "follow" mode, we must count the files and lines the * first time we invoke diff on a given commit, and we need * to do that to see if the commit touches the path we care * about, so we do it in show_commit. Hence we must clear * lines_counted here. * * This has the side effect of avoiding running diff twice * when we are both following renames and showing file * and/or line counts. */ lines_counted = 0; if (show_commit(commit, &rev)) { i++; print_commit(commit, &rev); } free_commit_buffer(commit); free_commit_list(commit->parents); commit->parents = NULL; } if (pager) { html("</table><ul class='pager'>"); if (ofs > 0) { html("<li>"); cgit_log_link("[prev]", NULL, NULL, ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath, ofs - cnt, ctx.qry.grep, ctx.qry.search, ctx.qry.showmsg, ctx.qry.follow); html("</li>"); } if ((commit = get_revision(&rev)) != NULL) { html("<li>"); cgit_log_link("[next]", NULL, NULL, ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath, ofs + cnt, ctx.qry.grep, ctx.qry.search, ctx.qry.showmsg, ctx.qry.follow); html("</li>"); } html("</ul>"); cgit_print_layout_end(); } else if ((commit = get_revision(&rev)) != NULL) { htmlf("<tr class='nohover'><td colspan='%d'>", columns); cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, ctx.qry.vpath, 0, NULL, NULL, ctx.qry.showmsg, ctx.qry.follow); html("</td></tr>\n"); } /* If we allocated tip then it is safe to cast away const. */ if (must_free_tip) free((char*) tip); }