]> gitweb.ps.run Git - ps-cgit/blob - ui-diff.c
Simplify commit and tag parsing
[ps-cgit] / ui-diff.c
1 /* ui-diff.c: show diff between two blobs
2  *
3  * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
4  *
5  * Licensed under GNU General Public License v2
6  *   (see COPYING for full license text)
7  */
8
9 #include "cgit.h"
10 #include "ui-diff.h"
11 #include "html.h"
12 #include "ui-shared.h"
13 #include "ui-ssdiff.h"
14
15 unsigned char old_rev_sha1[20];
16 unsigned char new_rev_sha1[20];
17
18 static int files, slots;
19 static int total_adds, total_rems, max_changes;
20 static int lines_added, lines_removed;
21
22 static struct fileinfo {
23         char status;
24         unsigned char old_sha1[20];
25         unsigned char new_sha1[20];
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         int binary:1;
35 } *items;
36
37 static int use_ssdiff = 0;
38 static struct diff_filepair *current_filepair;
39
40 struct diff_filespec *cgit_get_current_old_file(void)
41 {
42         return current_filepair->one;
43 }
44
45 struct diff_filespec *cgit_get_current_new_file(void)
46 {
47         return current_filepair->two;
48 }
49
50 static void print_fileinfo(struct fileinfo *info)
51 {
52         char *class;
53
54         switch (info->status) {
55         case DIFF_STATUS_ADDED:
56                 class = "add";
57                 break;
58         case DIFF_STATUS_COPIED:
59                 class = "cpy";
60                 break;
61         case DIFF_STATUS_DELETED:
62                 class = "del";
63                 break;
64         case DIFF_STATUS_MODIFIED:
65                 class = "upd";
66                 break;
67         case DIFF_STATUS_RENAMED:
68                 class = "mov";
69                 break;
70         case DIFF_STATUS_TYPE_CHANGED:
71                 class = "typ";
72                 break;
73         case DIFF_STATUS_UNKNOWN:
74                 class = "unk";
75                 break;
76         case DIFF_STATUS_UNMERGED:
77                 class = "stg";
78                 break;
79         default:
80                 die("bug: unhandled diff status %c", info->status);
81         }
82
83         html("<tr>");
84         htmlf("<td class='mode'>");
85         if (is_null_sha1(info->new_sha1)) {
86                 cgit_print_filemode(info->old_mode);
87         } else {
88                 cgit_print_filemode(info->new_mode);
89         }
90
91         if (info->old_mode != info->new_mode &&
92             !is_null_sha1(info->old_sha1) &&
93             !is_null_sha1(info->new_sha1)) {
94                 html("<span class='modechange'>[");
95                 cgit_print_filemode(info->old_mode);
96                 html("]</span>");
97         }
98         htmlf("</td><td class='%s'>", class);
99         cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.sha1,
100                        ctx.qry.sha2, info->new_path);
101         if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED) {
102                 htmlf(" (%s from ",
103                       info->status == DIFF_STATUS_COPIED ? "copied" : "renamed");
104                 html_txt(info->old_path);
105                 html(")");
106         }
107         html("</td><td class='right'>");
108         if (info->binary) {
109                 htmlf("bin</td><td class='graph'>%ld -> %ld bytes",
110                       info->old_size, info->new_size);
111                 return;
112         }
113         htmlf("%d", info->added + info->removed);
114         html("</td><td class='graph'>");
115         htmlf("<table summary='file diffstat' width='%d%%'><tr>", (max_changes > 100 ? 100 : max_changes));
116         htmlf("<td class='add' style='width: %.1f%%;'/>",
117               info->added * 100.0 / max_changes);
118         htmlf("<td class='rem' style='width: %.1f%%;'/>",
119               info->removed * 100.0 / max_changes);
120         htmlf("<td class='none' style='width: %.1f%%;'/>",
121               (max_changes - info->removed - info->added) * 100.0 / max_changes);
122         html("</tr></table></td></tr>\n");
123 }
124
125 static void count_diff_lines(char *line, int len)
126 {
127         if (line && (len > 0)) {
128                 if (line[0] == '+')
129                         lines_added++;
130                 else if (line[0] == '-')
131                         lines_removed++;
132         }
133 }
134
135 static void inspect_filepair(struct diff_filepair *pair)
136 {
137         int binary = 0;
138         unsigned long old_size = 0;
139         unsigned long new_size = 0;
140         files++;
141         lines_added = 0;
142         lines_removed = 0;
143         cgit_diff_files(pair->one->sha1, pair->two->sha1, &old_size, &new_size,
144                         &binary, 0, ctx.qry.ignorews, count_diff_lines);
145         if (files >= slots) {
146                 if (slots == 0)
147                         slots = 4;
148                 else
149                         slots = slots * 2;
150                 items = xrealloc(items, slots * sizeof(struct fileinfo));
151         }
152         items[files-1].status = pair->status;
153         hashcpy(items[files-1].old_sha1, pair->one->sha1);
154         hashcpy(items[files-1].new_sha1, pair->two->sha1);
155         items[files-1].old_mode = pair->one->mode;
156         items[files-1].new_mode = pair->two->mode;
157         items[files-1].old_path = xstrdup(pair->one->path);
158         items[files-1].new_path = xstrdup(pair->two->path);
159         items[files-1].added = lines_added;
160         items[files-1].removed = lines_removed;
161         items[files-1].old_size = old_size;
162         items[files-1].new_size = new_size;
163         items[files-1].binary = binary;
164         if (lines_added + lines_removed > max_changes)
165                 max_changes = lines_added + lines_removed;
166         total_adds += lines_added;
167         total_rems += lines_removed;
168 }
169
170 static void cgit_print_diffstat(const unsigned char *old_sha1,
171                                 const unsigned char *new_sha1,
172                                 const char *prefix)
173 {
174         int i;
175
176         html("<div class='diffstat-header'>");
177         cgit_diff_link("Diffstat", NULL, NULL, ctx.qry.head, ctx.qry.sha1,
178                        ctx.qry.sha2, NULL);
179         if (prefix) {
180                 html(" (limited to '");
181                 html_txt(prefix);
182                 html("')");
183         }
184         html("</div>");
185         html("<table summary='diffstat' class='diffstat'>");
186         max_changes = 0;
187         cgit_diff_tree(old_sha1, new_sha1, inspect_filepair, prefix,
188                        ctx.qry.ignorews);
189         for (i = 0; i<files; i++)
190                 print_fileinfo(&items[i]);
191         html("</table>");
192         html("<div class='diffstat-summary'>");
193         htmlf("%d files changed, %d insertions, %d deletions",
194               files, total_adds, total_rems);
195         html("</div>");
196 }
197
198
199 /*
200  * print a single line returned from xdiff
201  */
202 static void print_line(char *line, int len)
203 {
204         char *class = "ctx";
205         char c = line[len-1];
206
207         if (line[0] == '+')
208                 class = "add";
209         else if (line[0] == '-')
210                 class = "del";
211         else if (line[0] == '@')
212                 class = "hunk";
213
214         htmlf("<div class='%s'>", class);
215         line[len-1] = '\0';
216         html_txt(line);
217         html("</div>");
218         line[len-1] = c;
219 }
220
221 static void header(unsigned char *sha1, char *path1, int mode1,
222                    unsigned char *sha2, char *path2, int mode2)
223 {
224         char *abbrev1, *abbrev2;
225         int subproject;
226
227         subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2));
228         html("<div class='head'>");
229         html("diff --git a/");
230         html_txt(path1);
231         html(" b/");
232         html_txt(path2);
233
234         if (mode1 == 0)
235                 htmlf("<br/>new file mode %.6o", mode2);
236
237         if (mode2 == 0)
238                 htmlf("<br/>deleted file mode %.6o", mode1);
239
240         if (!subproject) {
241                 abbrev1 = xstrdup(find_unique_abbrev(sha1, DEFAULT_ABBREV));
242                 abbrev2 = xstrdup(find_unique_abbrev(sha2, DEFAULT_ABBREV));
243                 htmlf("<br/>index %s..%s", abbrev1, abbrev2);
244                 free(abbrev1);
245                 free(abbrev2);
246                 if (mode1 != 0 && mode2 != 0) {
247                         htmlf(" %.6o", mode1);
248                         if (mode2 != mode1)
249                                 htmlf("..%.6o", mode2);
250                 }
251                 if (is_null_sha1(sha1)) {
252                         path1 = "dev/null";
253                         html("<br/>--- /");
254                 } else
255                         html("<br/>--- a/");
256                 if (mode1 != 0)
257                         cgit_tree_link(path1, NULL, NULL, ctx.qry.head,
258                                        sha1_to_hex(old_rev_sha1), path1);
259                 else
260                         html_txt(path1);
261                 if (is_null_sha1(sha2)) {
262                         path2 = "dev/null";
263                         html("<br/>+++ /");
264                 } else
265                         html("<br/>+++ b/");
266                 if (mode2 != 0)
267                         cgit_tree_link(path2, NULL, NULL, ctx.qry.head,
268                                        sha1_to_hex(new_rev_sha1), path2);
269                 else
270                         html_txt(path2);
271         }
272         html("</div>");
273 }
274
275 static void filepair_cb(struct diff_filepair *pair)
276 {
277         unsigned long old_size = 0;
278         unsigned long new_size = 0;
279         int binary = 0;
280         linediff_fn print_line_fn = print_line;
281
282         current_filepair = pair;
283         if (use_ssdiff) {
284                 cgit_ssdiff_header_begin();
285                 print_line_fn = cgit_ssdiff_line_cb;
286         }
287         header(pair->one->sha1, pair->one->path, pair->one->mode,
288                pair->two->sha1, pair->two->path, pair->two->mode);
289         if (use_ssdiff)
290                 cgit_ssdiff_header_end();
291         if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) {
292                 if (S_ISGITLINK(pair->one->mode))
293                         print_line_fn(fmt("-Subproject %s", sha1_to_hex(pair->one->sha1)), 52);
294                 if (S_ISGITLINK(pair->two->mode))
295                         print_line_fn(fmt("+Subproject %s", sha1_to_hex(pair->two->sha1)), 52);
296                 if (use_ssdiff)
297                         cgit_ssdiff_footer();
298                 return;
299         }
300         if (cgit_diff_files(pair->one->sha1, pair->two->sha1, &old_size,
301                             &new_size, &binary, ctx.qry.context,
302                             ctx.qry.ignorews, print_line_fn))
303                 cgit_print_error("Error running diff");
304         if (binary) {
305                 if (use_ssdiff)
306                         html("<tr><td colspan='4'>Binary files differ</td></tr>");
307                 else
308                         html("Binary files differ");
309         }
310         if (use_ssdiff)
311                 cgit_ssdiff_footer();
312 }
313
314 void cgit_print_diff_ctrls()
315 {
316         int i, curr;
317
318         html("<div class='cgit-panel'>");
319         html("<b>diff options</b>");
320         html("<form method='get' action='.'>");
321         cgit_add_hidden_formfields(1, 0, ctx.qry.page);
322         html("<table>");
323         html("<tr><td colspan='2'/></tr>");
324         html("<tr>");
325         html("<td class='label'>context:</td>");
326         html("<td class='ctrl'>");
327         html("<select name='context' onchange='this.form.submit();'>");
328         curr = ctx.qry.context;
329         if (!curr)
330                 curr = 3;
331         for (i = 1; i <= 10; i++)
332                 html_intoption(i, fmt("%d", i), curr);
333         for (i = 15; i <= 40; i += 5)
334                 html_intoption(i, fmt("%d", i), curr);
335         html("</select>");
336         html("</td>");
337         html("</tr><tr>");
338         html("<td class='label'>space:</td>");
339         html("<td class='ctrl'>");
340         html("<select name='ignorews' onchange='this.form.submit();'>");
341         html_intoption(0, "include", ctx.qry.ignorews);
342         html_intoption(1, "ignore", ctx.qry.ignorews);
343         html("</select>");
344         html("</td>");
345         html("</tr><tr>");
346         html("<td class='label'>mode:</td>");
347         html("<td class='ctrl'>");
348         html("<select name='dt' onchange='this.form.submit();'>");
349         curr = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
350         html_intoption(0, "unified", curr);
351         html_intoption(1, "ssdiff", curr);
352         html_intoption(2, "stat only", curr);
353         html("</select></td></tr>");
354         html("<tr><td/><td class='ctrl'>");
355         html("<noscript><input type='submit' value='reload'/></noscript>");
356         html("</td></tr></table>");
357         html("</form>");
358         html("</div>");
359 }
360
361 void cgit_print_diff(const char *new_rev, const char *old_rev,
362                      const char *prefix, int show_ctrls, int raw)
363 {
364         struct commit *commit, *commit2;
365         const unsigned char *old_tree_sha1, *new_tree_sha1;
366         diff_type difftype;
367
368         if (!new_rev)
369                 new_rev = ctx.qry.head;
370         if (get_sha1(new_rev, new_rev_sha1)) {
371                 cgit_print_error("Bad object name: %s", new_rev);
372                 return;
373         }
374         commit = lookup_commit_reference(new_rev_sha1);
375         if (!commit || parse_commit(commit)) {
376                 cgit_print_error("Bad commit: %s", sha1_to_hex(new_rev_sha1));
377                 return;
378         }
379         new_tree_sha1 = commit->tree->object.sha1;
380
381         if (old_rev) {
382                 if (get_sha1(old_rev, old_rev_sha1)) {
383                         cgit_print_error("Bad object name: %s", old_rev);
384                         return;
385                 }
386         } else if (commit->parents && commit->parents->item) {
387                 hashcpy(old_rev_sha1, commit->parents->item->object.sha1);
388         } else {
389                 hashclr(old_rev_sha1);
390         }
391
392         if (!is_null_sha1(old_rev_sha1)) {
393                 commit2 = lookup_commit_reference(old_rev_sha1);
394                 if (!commit2 || parse_commit(commit2)) {
395                         cgit_print_error("Bad commit: %s", sha1_to_hex(old_rev_sha1));
396                         return;
397                 }
398                 old_tree_sha1 = commit2->tree->object.sha1;
399         } else {
400                 old_tree_sha1 = NULL;
401         }
402
403         if (raw) {
404                 struct diff_options diffopt;
405
406                 diff_setup(&diffopt);
407                 diffopt.output_format = DIFF_FORMAT_PATCH;
408                 DIFF_OPT_SET(&diffopt, RECURSIVE);
409                 diff_setup_done(&diffopt);
410
411                 ctx.page.mimetype = "text/plain";
412                 cgit_print_http_headers();
413                 if (old_tree_sha1) {
414                         diff_tree_sha1(old_tree_sha1, new_tree_sha1, "",
415                                        &diffopt);
416                 } else {
417                         diff_root_tree_sha1(new_tree_sha1, "", &diffopt);
418                 }
419                 diffcore_std(&diffopt);
420                 diff_flush(&diffopt);
421
422                 return;
423         }
424
425         difftype = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
426         use_ssdiff = difftype == DIFF_SSDIFF;
427
428         if (show_ctrls)
429                 cgit_print_diff_ctrls();
430
431         /*
432          * Clicking on a link to a file in the diff stat should show a diff
433          * of the file, showing the diff stat limited to a single file is
434          * pretty useless.  All links from this point on will be to
435          * individual files, so we simply reset the difftype in the query
436          * here to avoid propagating DIFF_STATONLY to the individual files.
437          */
438         if (difftype == DIFF_STATONLY)
439                 ctx.qry.difftype = ctx.cfg.difftype;
440
441         cgit_print_diffstat(old_rev_sha1, new_rev_sha1, prefix);
442
443         if (difftype == DIFF_STATONLY)
444                 return;
445
446         if (use_ssdiff) {
447                 html("<table summary='ssdiff' class='ssdiff'>");
448         } else {
449                 html("<table summary='diff' class='diff'>");
450                 html("<tr><td>");
451         }
452         cgit_diff_tree(old_rev_sha1, new_rev_sha1, filepair_cb, prefix,
453                        ctx.qry.ignorews);
454         if (!use_ssdiff)
455                 html("</td></tr>");
456         html("</table>");
457 }