aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui-log.c
blob: 6bff94807886c39e5e15ec58e66f67a81b50187b (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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
65'>365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540
/* ui-log.c: functions for log output
 *
 * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
 *
 * Licensed under GNU General Public License v2
 *   (see COPYING for full license text)
 */

#include "cgit.h"
#include "ui-log.h"
#include "html.h"
#include "ui-shared.h"
#include "argv-array.h"

static int files, add_lines, rem_lines, lines_counted;

/*
 * The list of available column colors in the commit graph.
 */
static const char *column_colors_html[] = {
	"<span class='column1'>",
	"<span class='column2'>",
	"<span class='column3'>",
	"<span class='column4'>",
	"<span class='column5'>",
	"<span class='column6'>",
	"</span>",
};

#define COLUMN_COLORS_HTML_MAX (ARRAY_SIZE(column_colors_html) - 1)

static void count_lines(char *line, int size)
{
	if (size <= 0)
		return;

	if (line[0] == '+')
		add_lines++;

	else if (line[0] == '-')
		rem_lines++;
}

static void inspect_files(struct diff_filepair *pair)
{
	unsigned long old_size = 0;
	unsigned long new_size = 0;
	int binary = 0;

	files++;
	if (ctx.repo->enable_log_linecount)
		cgit_diff_files(pair->one->sha1, pair->two->sha1, &old_size,
				&new_size, &binary, 0, ctx.qry.ignorews,
				count_lines);
}

void show_commit_decorations(struct commit *commit)
{
	const struct name_decoration *deco;
	static char buf[1024];

	buf[sizeof(buf) - 1] = 0;
	deco = get_name_decoration(&commit->object);
	html("<span class='decoration'>");
	while (deco) {
		if (starts_with(deco->name, "refs/heads/")) {
			strncpy(buf, deco->name + 11, sizeof(buf) - 1);
			cgit_log_link(buf, NULL, "branch-deco", buf, NULL,
				      ctx.qry.vpath, 0, NULL, NULL,
				      ctx.qry.showmsg, 0);
		}
		else if (starts_with(deco->name, "tag: refs/tags/")) {
			strncpy(buf, deco->name + 15, sizeof(buf) - 1);
			cgit_tag_link(buf, NULL, "tag-deco", buf);
		}
		else if (starts_with(deco->name, "refs/tags/")) {
			strncpy(buf, deco->name + 10, sizeof(buf) - 1);
			cgit_tag_link(buf, NULL, "tag-deco", buf);
		}
		else if (starts_with(deco->name, "refs/remotes/")) {
			if (!ctx.repo->enable_remote_branches)
				goto next;
			strncpy(buf, deco->name + 13, sizeof(buf) - 1);
			cgit_log_link(buf, NULL, "remote-deco", NULL,
				      sha1_to_hex(commit->object.sha1),
				      ctx.qry.vpath, 0, NULL, NULL,
				      ctx.qry.showmsg, 0);
		}
		else {
			strncpy(buf, deco->name, sizeof(buf) - 1);
			cgit_commit_link(buf, NULL, "deco", ctx.qry.head,
					 sha1_to_hex(commit->object.sha1),
					 ctx.qry.vpath);
		}
next:
		deco = deco->next;
	}
	html("</span>");
}

static void handle_rename(struct diff_filepair *pair)
{
	/*
	 * After we have seen a rename, we generate links to the previous
	 * name of the file so that commit & diff views get fed the path
	 * 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;
	parse_commit(parent);

	files = 0;
	add_lines = 0;
	rem_lines = 0;

	DIFF_OPT_SET(&revs->diffopt, RECURSIVE);
	diff_tree_sha1(parent->tree->object.sha1,
		       commit->tree->object.sha1,
		       "", &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(commit->date, TM_WEEK * 2, FMT_SHORTDATE);
		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,
			 sha1_to_hex(commit->object.sha1), 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(commit->date, TM_WEEK * 2, FMT_SHORTDATE);
	}

	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.sha1,
					     &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)
		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>");
	} 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);
}