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)
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
{
24 struct object_id old_oid
[1];
25 struct object_id new_oid
[1];
26 unsigned short old_mode
;
27 unsigned short new_mode
;
32 unsigned long old_size
;
33 unsigned long new_size
;
34 unsigned int binary
:1;
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
)
55 switch (info
->status
) {
56 case DIFF_STATUS_ADDED
:
59 case DIFF_STATUS_COPIED
:
62 case DIFF_STATUS_DELETED
:
65 case DIFF_STATUS_MODIFIED
:
68 case DIFF_STATUS_RENAMED
:
71 case DIFF_STATUS_TYPE_CHANGED
:
74 case DIFF_STATUS_UNKNOWN
:
77 case DIFF_STATUS_UNMERGED
:
81 die("bug: unhandled diff status %c", info
->status
);
85 html("<td class='mode'>");
86 if (is_null_oid(info
->new_oid
)) {
87 cgit_print_filemode(info
->old_mode
);
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
);
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
) {
104 info
->status
== DIFF_STATUS_COPIED
? "copied" : "renamed");
105 html_txt(info
->old_path
);
108 html("</td><td class='right'>");
110 htmlf("bin</td><td class='graph'>%ld -> %ld bytes",
111 info
->old_size
, info
->new_size
);
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)) {
131 else if (line
[0] == '-')
136 static int show_filepair(struct diff_filepair
*pair
)
138 /* Always show if we have no limiting prefix. */
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
))
147 /* Otherwise we don't want to show this filepair. */
151 static void inspect_filepair(struct diff_filepair
*pair
)
154 unsigned long old_size
= 0;
155 unsigned long new_size
= 0;
157 if (!show_filepair(pair
))
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
) {
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
,
196 html("<div class='diffstat-header'>");
197 cgit_diff_link("Diffstat", NULL
, NULL
, ctx
.qry
.head
, ctx
.qry
.oid
,
200 html(" (limited to '");
205 html("<table summary='diffstat' class='diffstat'>");
207 cgit_diff_tree(old_oid
, new_oid
, inspect_filepair
, prefix
,
209 for (i
= 0; i
<files
; i
++)
210 print_fileinfo(&items
[i
]);
212 html("<div class='diffstat-summary'>");
213 htmlf("%d files changed, %d insertions, %d deletions",
214 files
, total_adds
, total_rems
);
220 * print a single line returned from xdiff
222 static void print_line(char *line
, int len
)
225 char c
= line
[len
-1];
229 else if (line
[0] == '-')
231 else if (line
[0] == '@')
234 htmlf("<div class='%s'>", class);
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
;
247 subproject
= (S_ISGITLINK(mode1
) || S_ISGITLINK(mode2
));
248 html("<div class='head'>");
249 html("diff --git a/");
255 htmlf("<br/>new file mode %.6o", mode2
);
258 htmlf("<br/>deleted file mode %.6o", mode1
);
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
);
266 if (mode1
!= 0 && mode2
!= 0) {
267 htmlf(" %.6o", mode1
);
269 htmlf("..%.6o", mode2
);
271 if (is_null_oid(oid1
)) {
277 cgit_tree_link(path1
, NULL
, NULL
, ctx
.qry
.head
,
278 oid_to_hex(old_rev_oid
), path1
);
281 if (is_null_oid(oid2
)) {
287 cgit_tree_link(path2
, NULL
, NULL
, ctx
.qry
.head
,
288 oid_to_hex(new_rev_oid
), path2
);
295 static void filepair_cb(struct diff_filepair
*pair
)
297 unsigned long old_size
= 0;
298 unsigned long new_size
= 0;
300 linediff_fn print_line_fn
= print_line
;
302 if (!show_filepair(pair
))
305 current_filepair
= pair
;
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
);
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);
320 cgit_ssdiff_footer();
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");
329 html("<tr><td colspan='4'>Binary files differ</td></tr>");
331 html("Binary files differ");
334 cgit_ssdiff_footer();
337 void cgit_print_diff_ctrls(void)
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
);
346 html("<tr><td colspan='2'/></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
;
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
);
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
);
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>");
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
;
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
;
400 current_prefix
= NULL
;
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
);
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
));
416 new_tree_oid
= get_commit_tree_oid(commit
);
419 if (get_oid(old_rev
, old_rev_oid
)) {
420 cgit_print_error_page(404, "Not found",
421 "Bad object name: %s", old_rev
);
424 } else if (commit
->parents
&& commit
->parents
->item
) {
425 oidcpy(old_rev_oid
, &commit
->parents
->item
->object
.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
));
437 old_tree_oid
= get_commit_tree_oid(commit2
);
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();
453 diff_tree_oid(old_tree_oid
, new_tree_oid
, "",
456 diff_root_tree_oid(new_tree_oid
, "", &diffopt
);
458 diffcore_std(&diffopt
);
459 diff_flush(&diffopt
);
464 difftype
= ctx
.qry
.has_difftype
? ctx
.qry
.difftype
: ctx
.cfg
.difftype
;
465 use_ssdiff
= difftype
== DIFF_SSDIFF
;
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
)
488 html("<table summary='ssdiff' class='ssdiff'>");
490 html("<table summary='diff' class='diff'>");
493 cgit_diff_tree(old_rev_oid
, new_rev_oid
, filepair_cb
, prefix
,
500 cgit_print_layout_end();