1 /*
2 * Copyright (C) the libgit2 contributors. All rights reserved.
3 *
4 * This file is part of libgit2, distributed under the GNU GPL v2 with
5 * a Linking Exception. For full terms see the included COPYING file.
6 */
7
8 #include "common.h"
9
10 #include "vector.h"
11 #include "diff.h"
12 #include "patch_generate.h"
13
14 #define DIFF_RENAME_FILE_SEPARATOR " => "
15 #define STATS_FULL_MIN_SCALE 7
16
17 typedef struct {
18 size_t insertions;
19 size_t deletions;
20 } diff_file_stats;
21
22 struct git_diff_stats {
23 git_diff *diff;
24 diff_file_stats *filestats;
25
26 size_t files_changed;
27 size_t insertions;
28 size_t deletions;
29 size_t renames;
30
31 size_t max_name;
32 size_t max_filestat;
33 int max_digits;
34 };
35
digits_for_value(size_t val)36 static int digits_for_value(size_t val)
37 {
38 int count = 1;
39 size_t placevalue = 10;
40
41 while (val >= placevalue) {
42 ++count;
43 placevalue *= 10;
44 }
45
46 return count;
47 }
48
git_diff_file_stats__full_to_buf(git_buf * out,const git_diff_delta * delta,const diff_file_stats * filestat,const git_diff_stats * stats,size_t width)49 int git_diff_file_stats__full_to_buf(
50 git_buf *out,
51 const git_diff_delta *delta,
52 const diff_file_stats *filestat,
53 const git_diff_stats *stats,
54 size_t width)
55 {
56 const char *old_path = NULL, *new_path = NULL;
57 size_t padding, old_size, new_size;
58
59 old_path = delta->old_file.path;
60 new_path = delta->new_file.path;
61 old_size = delta->old_file.size;
62 new_size = delta->new_file.size;
63
64 if (git_buf_printf(out, " %s", old_path) < 0)
65 goto on_error;
66
67 if (strcmp(old_path, new_path) != 0) {
68 padding = stats->max_name - strlen(old_path) - strlen(new_path);
69
70 if (git_buf_printf(out, DIFF_RENAME_FILE_SEPARATOR "%s", new_path) < 0)
71 goto on_error;
72 } else {
73 padding = stats->max_name - strlen(old_path);
74
75 if (stats->renames > 0)
76 padding += strlen(DIFF_RENAME_FILE_SEPARATOR);
77 }
78
79 if (git_buf_putcn(out, ' ', padding) < 0 ||
80 git_buf_puts(out, " | ") < 0)
81 goto on_error;
82
83 if (delta->flags & GIT_DIFF_FLAG_BINARY) {
84 if (git_buf_printf(out,
85 "Bin %" PRIuZ " -> %" PRIuZ " bytes", old_size, new_size) < 0)
86 goto on_error;
87 }
88 else {
89 if (git_buf_printf(out,
90 "%*" PRIuZ, stats->max_digits,
91 filestat->insertions + filestat->deletions) < 0)
92 goto on_error;
93
94 if (filestat->insertions || filestat->deletions) {
95 if (git_buf_putc(out, ' ') < 0)
96 goto on_error;
97
98 if (!width) {
99 if (git_buf_putcn(out, '+', filestat->insertions) < 0 ||
100 git_buf_putcn(out, '-', filestat->deletions) < 0)
101 goto on_error;
102 } else {
103 size_t total = filestat->insertions + filestat->deletions;
104 size_t full = (total * width + stats->max_filestat / 2) /
105 stats->max_filestat;
106 size_t plus = full * filestat->insertions / total;
107 size_t minus = full - plus;
108
109 if (git_buf_putcn(out, '+', max(plus, 1)) < 0 ||
110 git_buf_putcn(out, '-', max(minus, 1)) < 0)
111 goto on_error;
112 }
113 }
114 }
115
116 git_buf_putc(out, '\n');
117
118 on_error:
119 return (git_buf_oom(out) ? -1 : 0);
120 }
121
git_diff_file_stats__number_to_buf(git_buf * out,const git_diff_delta * delta,const diff_file_stats * filestats)122 int git_diff_file_stats__number_to_buf(
123 git_buf *out,
124 const git_diff_delta *delta,
125 const diff_file_stats *filestats)
126 {
127 int error;
128 const char *path = delta->new_file.path;
129
130 if (delta->flags & GIT_DIFF_FLAG_BINARY)
131 error = git_buf_printf(out, "%-8c" "%-8c" "%s\n", '-', '-', path);
132 else
133 error = git_buf_printf(out, "%-8" PRIuZ "%-8" PRIuZ "%s\n",
134 filestats->insertions, filestats->deletions, path);
135
136 return error;
137 }
138
git_diff_file_stats__summary_to_buf(git_buf * out,const git_diff_delta * delta)139 int git_diff_file_stats__summary_to_buf(
140 git_buf *out,
141 const git_diff_delta *delta)
142 {
143 if (delta->old_file.mode != delta->new_file.mode) {
144 if (delta->old_file.mode == 0) {
145 git_buf_printf(out, " create mode %06o %s\n",
146 delta->new_file.mode, delta->new_file.path);
147 }
148 else if (delta->new_file.mode == 0) {
149 git_buf_printf(out, " delete mode %06o %s\n",
150 delta->old_file.mode, delta->old_file.path);
151 }
152 else {
153 git_buf_printf(out, " mode change %06o => %06o %s\n",
154 delta->old_file.mode, delta->new_file.mode, delta->new_file.path);
155 }
156 }
157
158 return 0;
159 }
160
git_diff_get_stats(git_diff_stats ** out,git_diff * diff)161 int git_diff_get_stats(
162 git_diff_stats **out,
163 git_diff *diff)
164 {
165 size_t i, deltas;
166 size_t total_insertions = 0, total_deletions = 0;
167 git_diff_stats *stats = NULL;
168 int error = 0;
169
170 assert(out && diff);
171
172 stats = git__calloc(1, sizeof(git_diff_stats));
173 GITERR_CHECK_ALLOC(stats);
174
175 deltas = git_diff_num_deltas(diff);
176
177 stats->filestats = git__calloc(deltas, sizeof(diff_file_stats));
178 if (!stats->filestats) {
179 git__free(stats);
180 return -1;
181 }
182
183 stats->diff = diff;
184 GIT_REFCOUNT_INC(diff);
185
186 for (i = 0; i < deltas && !error; ++i) {
187 git_patch *patch = NULL;
188 size_t add = 0, remove = 0, namelen;
189 const git_diff_delta *delta;
190
191 if ((error = git_patch_from_diff(&patch, diff, i)) < 0)
192 break;
193
194 /* keep a count of renames because it will affect formatting */
195 delta = patch->delta;
196
197 /* TODO ugh */
198 namelen = strlen(delta->new_file.path);
199 if (strcmp(delta->old_file.path, delta->new_file.path) != 0) {
200 namelen += strlen(delta->old_file.path);
201 stats->renames++;
202 }
203
204 /* and, of course, count the line stats */
205 error = git_patch_line_stats(NULL, &add, &remove, patch);
206
207 git_patch_free(patch);
208
209 stats->filestats[i].insertions = add;
210 stats->filestats[i].deletions = remove;
211
212 total_insertions += add;
213 total_deletions += remove;
214
215 if (stats->max_name < namelen)
216 stats->max_name = namelen;
217 if (stats->max_filestat < add + remove)
218 stats->max_filestat = add + remove;
219 }
220
221 stats->files_changed = deltas;
222 stats->insertions = total_insertions;
223 stats->deletions = total_deletions;
224 stats->max_digits = digits_for_value(stats->max_filestat + 1);
225
226 if (error < 0) {
227 git_diff_stats_free(stats);
228 stats = NULL;
229 }
230
231 *out = stats;
232 return error;
233 }
234
git_diff_stats_files_changed(const git_diff_stats * stats)235 size_t git_diff_stats_files_changed(
236 const git_diff_stats *stats)
237 {
238 assert(stats);
239
240 return stats->files_changed;
241 }
242
git_diff_stats_insertions(const git_diff_stats * stats)243 size_t git_diff_stats_insertions(
244 const git_diff_stats *stats)
245 {
246 assert(stats);
247
248 return stats->insertions;
249 }
250
git_diff_stats_deletions(const git_diff_stats * stats)251 size_t git_diff_stats_deletions(
252 const git_diff_stats *stats)
253 {
254 assert(stats);
255
256 return stats->deletions;
257 }
258
git_diff_stats_to_buf(git_buf * out,const git_diff_stats * stats,git_diff_stats_format_t format,size_t width)259 int git_diff_stats_to_buf(
260 git_buf *out,
261 const git_diff_stats *stats,
262 git_diff_stats_format_t format,
263 size_t width)
264 {
265 int error = 0;
266 size_t i;
267 const git_diff_delta *delta;
268
269 assert(out && stats);
270
271 if (format & GIT_DIFF_STATS_NUMBER) {
272 for (i = 0; i < stats->files_changed; ++i) {
273 if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
274 continue;
275
276 error = git_diff_file_stats__number_to_buf(
277 out, delta, &stats->filestats[i]);
278 if (error < 0)
279 return error;
280 }
281 }
282
283 if (format & GIT_DIFF_STATS_FULL) {
284 if (width > 0) {
285 if (width > stats->max_name + stats->max_digits + 5)
286 width -= (stats->max_name + stats->max_digits + 5);
287 if (width < STATS_FULL_MIN_SCALE)
288 width = STATS_FULL_MIN_SCALE;
289 }
290 if (width > stats->max_filestat)
291 width = 0;
292
293 for (i = 0; i < stats->files_changed; ++i) {
294 if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
295 continue;
296
297 error = git_diff_file_stats__full_to_buf(
298 out, delta, &stats->filestats[i], stats, width);
299 if (error < 0)
300 return error;
301 }
302 }
303
304 if (format & GIT_DIFF_STATS_FULL || format & GIT_DIFF_STATS_SHORT) {
305 git_buf_printf(
306 out, " %" PRIuZ " file%s changed",
307 stats->files_changed, stats->files_changed != 1 ? "s" : "");
308
309 if (stats->insertions || stats->deletions == 0)
310 git_buf_printf(
311 out, ", %" PRIuZ " insertion%s(+)",
312 stats->insertions, stats->insertions != 1 ? "s" : "");
313
314 if (stats->deletions || stats->insertions == 0)
315 git_buf_printf(
316 out, ", %" PRIuZ " deletion%s(-)",
317 stats->deletions, stats->deletions != 1 ? "s" : "");
318
319 git_buf_putc(out, '\n');
320
321 if (git_buf_oom(out))
322 return -1;
323 }
324
325 if (format & GIT_DIFF_STATS_INCLUDE_SUMMARY) {
326 for (i = 0; i < stats->files_changed; ++i) {
327 if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
328 continue;
329
330 error = git_diff_file_stats__summary_to_buf(out, delta);
331 if (error < 0)
332 return error;
333 }
334 }
335
336 return error;
337 }
338
git_diff_stats_free(git_diff_stats * stats)339 void git_diff_stats_free(git_diff_stats *stats)
340 {
341 if (stats == NULL)
342 return;
343
344 git_diff_free(stats->diff); /* bumped refcount in constructor */
345 git__free(stats->filestats);
346 git__free(stats);
347 }
348