config: make empty js= omit script tag
[cgit.git] / ui-diff.c
blob5ed5990c291522ac4f9342e1a3c9377c1ed72a82
1 /* ui-diff.c: show diff between two blobs
3 * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text)
7 */
9 #include "cgit.h"
10 #include "ui-diff.h"
11 #include "html.h"
12 #include "ui-shared.h"
13 #include "ui-ssdiff.h"
15 struct object_id old_rev_oid[1];
16 struct object_id new_rev_oid[1];
18 static int files, slots;
19 static int total_adds, total_rems, max_changes;
20 static int lines_added, lines_removed;
22 static struct fileinfo {
23 char status;
24 struct object_id old_oid[1];
25 struct object_id new_oid[1];
26 unsigned short old_mode;
27 unsigned short new_mode;
28 char *old_path;
29 char *new_path;
30 unsigned int added;
31 unsigned int removed;
32 unsigned long old_size;
33 unsigned long new_size;
34 unsigned int binary:1;
35 } *items;
37 static int use_ssdiff = 0;
38 static struct diff_filepair *current_filepair;
39 static const char *current_prefix;
41 struct diff_filespec *cgit_get_current_old_file(void)
43 return current_filepair->one;
46 struct diff_filespec *cgit_get_current_new_file(void)
48 return current_filepair->two;
51 static void print_fileinfo(struct fileinfo *info)
53 char *class;
55 switch (info->status) {
56 case DIFF_STATUS_ADDED:
57 class = "add";
58 break;
59 case DIFF_STATUS_COPIED:
60 class = "cpy";
61 break;
62 case DIFF_STATUS_DELETED:
63 class = "del";
64 break;
65 case DIFF_STATUS_MODIFIED:
66 class = "upd";
67 break;
68 case DIFF_STATUS_RENAMED:
69 class = "mov";
70 break;
71 case DIFF_STATUS_TYPE_CHANGED:
72 class = "typ";
73 break;
74 case DIFF_STATUS_UNKNOWN:
75 class = "unk";
76 break;
77 case DIFF_STATUS_UNMERGED:
78 class = "stg";
79 break;
80 default:
81 die("bug: unhandled diff status %c", info->status);
84 html("<tr>");
85 html("<td class='mode'>");
86 if (is_null_oid(info->new_oid)) {
87 cgit_print_filemode(info->old_mode);
88 } else {
89 cgit_print_filemode(info->new_mode);
92 if (info->old_mode != info->new_mode &&
93 !is_null_oid(info->old_oid) &&
94 !is_null_oid(info->new_oid)) {
95 html("<span class='modechange'>[");
96 cgit_print_filemode(info->old_mode);
97 html("]</span>");
99 htmlf("</td><td class='%s'>", class);
100 cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.oid,
101 ctx.qry.oid2, info->new_path);
102 if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED) {
103 htmlf(" (%s from ",
104 info->status == DIFF_STATUS_COPIED ? "copied" : "renamed");
105 html_txt(info->old_path);
106 html(")");
108 html("</td><td class='right'>");
109 if (info->binary) {
110 htmlf("bin</td><td class='graph'>%ld -> %ld bytes",
111 info->old_size, info->new_size);
112 return;
114 htmlf("%d", info->added + info->removed);
115 html("</td><td class='graph'>");
116 htmlf("<table summary='file diffstat' width='%d%%'><tr>", (max_changes > 100 ? 100 : max_changes));
117 htmlf("<td class='add' style='width: %.1f%%;'/>",
118 info->added * 100.0 / max_changes);
119 htmlf("<td class='rem' style='width: %.1f%%;'/>",
120 info->removed * 100.0 / max_changes);
121 htmlf("<td class='none' style='width: %.1f%%;'/>",
122 (max_changes - info->removed - info->added) * 100.0 / max_changes);
123 html("</tr></table></td></tr>\n");
126 static void count_diff_lines(char *line, int len)
128 if (line && (len > 0)) {
129 if (line[0] == '+')
130 lines_added++;
131 else if (line[0] == '-')
132 lines_removed++;
136 static int show_filepair(struct diff_filepair *pair)
138 /* Always show if we have no limiting prefix. */
139 if (!current_prefix)
140 return 1;
142 /* Show if either path in the pair begins with the prefix. */
143 if (starts_with(pair->one->path, current_prefix) ||
144 starts_with(pair->two->path, current_prefix))
145 return 1;
147 /* Otherwise we don't want to show this filepair. */
148 return 0;
151 static void inspect_filepair(struct diff_filepair *pair)
153 int binary = 0;
154 unsigned long old_size = 0;
155 unsigned long new_size = 0;
157 if (!show_filepair(pair))
158 return;
160 files++;
161 lines_added = 0;
162 lines_removed = 0;
163 cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size, &new_size,
164 &binary, 0, ctx.qry.ignorews, count_diff_lines);
165 if (files >= slots) {
166 if (slots == 0)
167 slots = 4;
168 else
169 slots = slots * 2;
170 items = xrealloc(items, slots * sizeof(struct fileinfo));
172 items[files-1].status = pair->status;
173 oidcpy(items[files-1].old_oid, &pair->one->oid);
174 oidcpy(items[files-1].new_oid, &pair->two->oid);
175 items[files-1].old_mode = pair->one->mode;
176 items[files-1].new_mode = pair->two->mode;
177 items[files-1].old_path = xstrdup(pair->one->path);
178 items[files-1].new_path = xstrdup(pair->two->path);
179 items[files-1].added = lines_added;
180 items[files-1].removed = lines_removed;
181 items[files-1].old_size = old_size;
182 items[files-1].new_size = new_size;
183 items[files-1].binary = binary;
184 if (lines_added + lines_removed > max_changes)
185 max_changes = lines_added + lines_removed;
186 total_adds += lines_added;
187 total_rems += lines_removed;
190 static void cgit_print_diffstat(const struct object_id *old_oid,
191 const struct object_id *new_oid,
192 const char *prefix)
194 int i;
196 html("<div class='diffstat-header'>");
197 cgit_diff_link("Diffstat", NULL, NULL, ctx.qry.head, ctx.qry.oid,
198 ctx.qry.oid2, NULL);
199 if (prefix) {
200 html(" (limited to '");
201 html_txt(prefix);
202 html("')");
204 html("</div>");
205 html("<table summary='diffstat' class='diffstat'>");
206 max_changes = 0;
207 cgit_diff_tree(old_oid, new_oid, inspect_filepair, prefix,
208 ctx.qry.ignorews);
209 for (i = 0; i<files; i++)
210 print_fileinfo(&items[i]);
211 html("</table>");
212 html("<div class='diffstat-summary'>");
213 htmlf("%d files changed, %d insertions, %d deletions",
214 files, total_adds, total_rems);
215 html("</div>");
220 * print a single line returned from xdiff
222 static void print_line(char *line, int len)
224 char *class = "ctx";
225 char c = line[len-1];
227 if (line[0] == '+')
228 class = "add";
229 else if (line[0] == '-')
230 class = "del";
231 else if (line[0] == '@')
232 class = "hunk";
234 htmlf("<div class='%s'>", class);
235 line[len-1] = '\0';
236 html_txt(line);
237 html("</div>");
238 line[len-1] = c;
241 static void header(const struct object_id *oid1, char *path1, int mode1,
242 const struct object_id *oid2, char *path2, int mode2)
244 char *abbrev1, *abbrev2;
245 int subproject;
247 subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2));
248 html("<div class='head'>");
249 html("diff --git a/");
250 html_txt(path1);
251 html(" b/");
252 html_txt(path2);
254 if (mode1 == 0)
255 htmlf("<br/>new file mode %.6o", mode2);
257 if (mode2 == 0)
258 htmlf("<br/>deleted file mode %.6o", mode1);
260 if (!subproject) {
261 abbrev1 = xstrdup(find_unique_abbrev(oid1, DEFAULT_ABBREV));
262 abbrev2 = xstrdup(find_unique_abbrev(oid2, DEFAULT_ABBREV));
263 htmlf("<br/>index %s..%s", abbrev1, abbrev2);
264 free(abbrev1);
265 free(abbrev2);
266 if (mode1 != 0 && mode2 != 0) {
267 htmlf(" %.6o", mode1);
268 if (mode2 != mode1)
269 htmlf("..%.6o", mode2);
271 if (is_null_oid(oid1)) {
272 path1 = "dev/null";
273 html("<br/>--- /");
274 } else
275 html("<br/>--- a/");
276 if (mode1 != 0)
277 cgit_tree_link(path1, NULL, NULL, ctx.qry.head,
278 oid_to_hex(old_rev_oid), path1);
279 else
280 html_txt(path1);
281 if (is_null_oid(oid2)) {
282 path2 = "dev/null";
283 html("<br/>+++ /");
284 } else
285 html("<br/>+++ b/");
286 if (mode2 != 0)
287 cgit_tree_link(path2, NULL, NULL, ctx.qry.head,
288 oid_to_hex(new_rev_oid), path2);
289 else
290 html_txt(path2);
292 html("</div>");
295 static void filepair_cb(struct diff_filepair *pair)
297 unsigned long old_size = 0;
298 unsigned long new_size = 0;
299 int binary = 0;
300 linediff_fn print_line_fn = print_line;
302 if (!show_filepair(pair))
303 return;
305 current_filepair = pair;
306 if (use_ssdiff) {
307 cgit_ssdiff_header_begin();
308 print_line_fn = cgit_ssdiff_line_cb;
310 header(&pair->one->oid, pair->one->path, pair->one->mode,
311 &pair->two->oid, pair->two->path, pair->two->mode);
312 if (use_ssdiff)
313 cgit_ssdiff_header_end();
314 if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) {
315 if (S_ISGITLINK(pair->one->mode))
316 print_line_fn(fmt("-Subproject %s", oid_to_hex(&pair->one->oid)), 52);
317 if (S_ISGITLINK(pair->two->mode))
318 print_line_fn(fmt("+Subproject %s", oid_to_hex(&pair->two->oid)), 52);
319 if (use_ssdiff)
320 cgit_ssdiff_footer();
321 return;
323 if (cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size,
324 &new_size, &binary, ctx.qry.context,
325 ctx.qry.ignorews, print_line_fn))
326 cgit_print_error("Error running diff");
327 if (binary) {
328 if (use_ssdiff)
329 html("<tr><td colspan='4'>Binary files differ</td></tr>");
330 else
331 html("Binary files differ");
333 if (use_ssdiff)
334 cgit_ssdiff_footer();
337 void cgit_print_diff_ctrls(void)
339 int i, curr;
341 html("<div class='cgit-panel'>");
342 html("<b>diff options</b>");
343 html("<form method='get'>");
344 cgit_add_hidden_formfields(1, 0, ctx.qry.page);
345 html("<table>");
346 html("<tr><td colspan='2'/></tr>");
347 html("<tr>");
348 html("<td class='label'>context:</td>");
349 html("<td class='ctrl'>");
350 html("<select name='context' onchange='this.form.submit();'>");
351 curr = ctx.qry.context;
352 if (!curr)
353 curr = 3;
354 for (i = 1; i <= 10; i++)
355 html_intoption(i, fmt("%d", i), curr);
356 for (i = 15; i <= 40; i += 5)
357 html_intoption(i, fmt("%d", i), curr);
358 html("</select>");
359 html("</td>");
360 html("</tr><tr>");
361 html("<td class='label'>space:</td>");
362 html("<td class='ctrl'>");
363 html("<select name='ignorews' onchange='this.form.submit();'>");
364 html_intoption(0, "include", ctx.qry.ignorews);
365 html_intoption(1, "ignore", ctx.qry.ignorews);
366 html("</select>");
367 html("</td>");
368 html("</tr><tr>");
369 html("<td class='label'>mode:</td>");
370 html("<td class='ctrl'>");
371 html("<select name='dt' onchange='this.form.submit();'>");
372 curr = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
373 html_intoption(0, "unified", curr);
374 html_intoption(1, "ssdiff", curr);
375 html_intoption(2, "stat only", curr);
376 html("</select></td></tr>");
377 html("<tr><td/><td class='ctrl'>");
378 html("<noscript><input type='submit' value='reload'/></noscript>");
379 html("</td></tr></table>");
380 html("</form>");
381 html("</div>");
384 void cgit_print_diff(const char *new_rev, const char *old_rev,
385 const char *prefix, int show_ctrls, int raw)
387 struct commit *commit, *commit2;
388 const struct object_id *old_tree_oid, *new_tree_oid;
389 diff_type difftype;
392 * If "follow" is set then the diff machinery needs to examine the
393 * entire commit to detect renames so we must limit the paths in our
394 * own callbacks and not pass the prefix to the diff machinery.
396 if (ctx.qry.follow && ctx.cfg.enable_follow_links) {
397 current_prefix = prefix;
398 prefix = "";
399 } else {
400 current_prefix = NULL;
403 if (!new_rev)
404 new_rev = ctx.qry.head;
405 if (get_oid(new_rev, new_rev_oid)) {
406 cgit_print_error_page(404, "Not found",
407 "Bad object name: %s", new_rev);
408 return;
410 commit = lookup_commit_reference(the_repository, new_rev_oid);
411 if (!commit || parse_commit(commit)) {
412 cgit_print_error_page(404, "Not found",
413 "Bad commit: %s", oid_to_hex(new_rev_oid));
414 return;
416 new_tree_oid = get_commit_tree_oid(commit);
418 if (old_rev) {
419 if (get_oid(old_rev, old_rev_oid)) {
420 cgit_print_error_page(404, "Not found",
421 "Bad object name: %s", old_rev);
422 return;
424 } else if (commit->parents && commit->parents->item) {
425 oidcpy(old_rev_oid, &commit->parents->item->object.oid);
426 } else {
427 oidclr(old_rev_oid);
430 if (!is_null_oid(old_rev_oid)) {
431 commit2 = lookup_commit_reference(the_repository, old_rev_oid);
432 if (!commit2 || parse_commit(commit2)) {
433 cgit_print_error_page(404, "Not found",
434 "Bad commit: %s", oid_to_hex(old_rev_oid));
435 return;
437 old_tree_oid = get_commit_tree_oid(commit2);
438 } else {
439 old_tree_oid = NULL;
442 if (raw) {
443 struct diff_options diffopt;
445 diff_setup(&diffopt);
446 diffopt.output_format = DIFF_FORMAT_PATCH;
447 diffopt.flags.recursive = 1;
448 diff_setup_done(&diffopt);
450 ctx.page.mimetype = "text/plain";
451 cgit_print_http_headers();
452 if (old_tree_oid) {
453 diff_tree_oid(old_tree_oid, new_tree_oid, "",
454 &diffopt);
455 } else {
456 diff_root_tree_oid(new_tree_oid, "", &diffopt);
458 diffcore_std(&diffopt);
459 diff_flush(&diffopt);
461 return;
464 difftype = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
465 use_ssdiff = difftype == DIFF_SSDIFF;
467 if (show_ctrls) {
468 cgit_print_layout_start();
469 cgit_print_diff_ctrls();
473 * Clicking on a link to a file in the diff stat should show a diff
474 * of the file, showing the diff stat limited to a single file is
475 * pretty useless. All links from this point on will be to
476 * individual files, so we simply reset the difftype in the query
477 * here to avoid propagating DIFF_STATONLY to the individual files.
479 if (difftype == DIFF_STATONLY)
480 ctx.qry.difftype = ctx.cfg.difftype;
482 cgit_print_diffstat(old_rev_oid, new_rev_oid, prefix);
484 if (difftype == DIFF_STATONLY)
485 return;
487 if (use_ssdiff) {
488 html("<table summary='ssdiff' class='ssdiff'>");
489 } else {
490 html("<table summary='diff' class='diff'>");
491 html("<tr><td>");
493 cgit_diff_tree(old_rev_oid, new_rev_oid, filepair_cb, prefix,
494 ctx.qry.ignorews);
495 if (!use_ssdiff)
496 html("</td></tr>");
497 html("</table>");
499 if (show_ctrls)
500 cgit_print_layout_end();