]> git.proxmox.com Git - libgit2.git/blob - src/diff_stats.c
41a25bf8acaaf0574a153878a554b7167882d1ac
[libgit2.git] / src / diff_stats.c
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
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
49 static int 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, *adddel_path = NULL;
57 size_t padding;
58 git_object_size_t old_size, new_size;
59
60 old_path = delta->old_file.path;
61 new_path = delta->new_file.path;
62 old_size = delta->old_file.size;
63 new_size = delta->new_file.size;
64
65 if (old_path && new_path && strcmp(old_path, new_path) != 0) {
66 size_t common_dirlen;
67 int error;
68
69 padding = stats->max_name - strlen(old_path) - strlen(new_path);
70
71 if ((common_dirlen = git_path_common_dirlen(old_path, new_path)) &&
72 common_dirlen <= INT_MAX) {
73 error = git_buf_printf(out, " %.*s{%s"DIFF_RENAME_FILE_SEPARATOR"%s}",
74 (int) common_dirlen, old_path,
75 old_path + common_dirlen,
76 new_path + common_dirlen);
77 } else {
78 error = git_buf_printf(out, " %s" DIFF_RENAME_FILE_SEPARATOR "%s",
79 old_path, new_path);
80 }
81
82 if (error < 0)
83 goto on_error;
84 } else {
85 adddel_path = new_path ? new_path : old_path;
86 if (git_buf_printf(out, " %s", adddel_path) < 0)
87 goto on_error;
88
89 padding = stats->max_name - strlen(adddel_path);
90
91 if (stats->renames > 0)
92 padding += strlen(DIFF_RENAME_FILE_SEPARATOR);
93 }
94
95 if (git_buf_putcn(out, ' ', padding) < 0 ||
96 git_buf_puts(out, " | ") < 0)
97 goto on_error;
98
99 if (delta->flags & GIT_DIFF_FLAG_BINARY) {
100 if (git_buf_printf(out,
101 "Bin %" PRId64 " -> %" PRId64 " bytes", old_size, new_size) < 0)
102 goto on_error;
103 }
104 else {
105 if (git_buf_printf(out,
106 "%*" PRIuZ, stats->max_digits,
107 filestat->insertions + filestat->deletions) < 0)
108 goto on_error;
109
110 if (filestat->insertions || filestat->deletions) {
111 if (git_buf_putc(out, ' ') < 0)
112 goto on_error;
113
114 if (!width) {
115 if (git_buf_putcn(out, '+', filestat->insertions) < 0 ||
116 git_buf_putcn(out, '-', filestat->deletions) < 0)
117 goto on_error;
118 } else {
119 size_t total = filestat->insertions + filestat->deletions;
120 size_t full = (total * width + stats->max_filestat / 2) /
121 stats->max_filestat;
122 size_t plus = full * filestat->insertions / total;
123 size_t minus = full - plus;
124
125 if (git_buf_putcn(out, '+', max(plus, 1)) < 0 ||
126 git_buf_putcn(out, '-', max(minus, 1)) < 0)
127 goto on_error;
128 }
129 }
130 }
131
132 git_buf_putc(out, '\n');
133
134 on_error:
135 return (git_buf_oom(out) ? -1 : 0);
136 }
137
138 static int diff_file_stats_number_to_buf(
139 git_buf *out,
140 const git_diff_delta *delta,
141 const diff_file_stats *filestats)
142 {
143 int error;
144 const char *path = delta->new_file.path;
145
146 if (delta->flags & GIT_DIFF_FLAG_BINARY)
147 error = git_buf_printf(out, "%-8c" "%-8c" "%s\n", '-', '-', path);
148 else
149 error = git_buf_printf(out, "%-8" PRIuZ "%-8" PRIuZ "%s\n",
150 filestats->insertions, filestats->deletions, path);
151
152 return error;
153 }
154
155 static int diff_file_stats_summary_to_buf(
156 git_buf *out,
157 const git_diff_delta *delta)
158 {
159 if (delta->old_file.mode != delta->new_file.mode) {
160 if (delta->old_file.mode == 0) {
161 git_buf_printf(out, " create mode %06o %s\n",
162 delta->new_file.mode, delta->new_file.path);
163 }
164 else if (delta->new_file.mode == 0) {
165 git_buf_printf(out, " delete mode %06o %s\n",
166 delta->old_file.mode, delta->old_file.path);
167 }
168 else {
169 git_buf_printf(out, " mode change %06o => %06o %s\n",
170 delta->old_file.mode, delta->new_file.mode, delta->new_file.path);
171 }
172 }
173
174 return 0;
175 }
176
177 int git_diff_get_stats(
178 git_diff_stats **out,
179 git_diff *diff)
180 {
181 size_t i, deltas;
182 size_t total_insertions = 0, total_deletions = 0;
183 git_diff_stats *stats = NULL;
184 int error = 0;
185
186 GIT_ASSERT_ARG(out);
187 GIT_ASSERT_ARG(diff);
188
189 stats = git__calloc(1, sizeof(git_diff_stats));
190 GIT_ERROR_CHECK_ALLOC(stats);
191
192 deltas = git_diff_num_deltas(diff);
193
194 stats->filestats = git__calloc(deltas, sizeof(diff_file_stats));
195 if (!stats->filestats) {
196 git__free(stats);
197 return -1;
198 }
199
200 stats->diff = diff;
201 GIT_REFCOUNT_INC(diff);
202
203 for (i = 0; i < deltas && !error; ++i) {
204 git_patch *patch = NULL;
205 size_t add = 0, remove = 0, namelen;
206 const git_diff_delta *delta;
207
208 if ((error = git_patch_from_diff(&patch, diff, i)) < 0)
209 break;
210
211 /* keep a count of renames because it will affect formatting */
212 delta = patch->delta;
213
214 /* TODO ugh */
215 namelen = strlen(delta->new_file.path);
216 if (delta->old_file.path && strcmp(delta->old_file.path, delta->new_file.path) != 0) {
217 namelen += strlen(delta->old_file.path);
218 stats->renames++;
219 }
220
221 /* and, of course, count the line stats */
222 error = git_patch_line_stats(NULL, &add, &remove, patch);
223
224 git_patch_free(patch);
225
226 stats->filestats[i].insertions = add;
227 stats->filestats[i].deletions = remove;
228
229 total_insertions += add;
230 total_deletions += remove;
231
232 if (stats->max_name < namelen)
233 stats->max_name = namelen;
234 if (stats->max_filestat < add + remove)
235 stats->max_filestat = add + remove;
236 }
237
238 stats->files_changed = deltas;
239 stats->insertions = total_insertions;
240 stats->deletions = total_deletions;
241 stats->max_digits = digits_for_value(stats->max_filestat + 1);
242
243 if (error < 0) {
244 git_diff_stats_free(stats);
245 stats = NULL;
246 }
247
248 *out = stats;
249 return error;
250 }
251
252 size_t git_diff_stats_files_changed(
253 const git_diff_stats *stats)
254 {
255 GIT_ASSERT_ARG(stats);
256
257 return stats->files_changed;
258 }
259
260 size_t git_diff_stats_insertions(
261 const git_diff_stats *stats)
262 {
263 GIT_ASSERT_ARG(stats);
264
265 return stats->insertions;
266 }
267
268 size_t git_diff_stats_deletions(
269 const git_diff_stats *stats)
270 {
271 GIT_ASSERT_ARG(stats);
272
273 return stats->deletions;
274 }
275
276 int git_diff_stats_to_buf(
277 git_buf *out,
278 const git_diff_stats *stats,
279 git_diff_stats_format_t format,
280 size_t width)
281 {
282 int error = 0;
283 size_t i;
284 const git_diff_delta *delta;
285
286 GIT_ASSERT_ARG(out);
287 GIT_ASSERT_ARG(stats);
288
289 if (format & GIT_DIFF_STATS_NUMBER) {
290 for (i = 0; i < stats->files_changed; ++i) {
291 if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
292 continue;
293
294 error = diff_file_stats_number_to_buf(
295 out, delta, &stats->filestats[i]);
296 if (error < 0)
297 return error;
298 }
299 }
300
301 if (format & GIT_DIFF_STATS_FULL) {
302 if (width > 0) {
303 if (width > stats->max_name + stats->max_digits + 5)
304 width -= (stats->max_name + stats->max_digits + 5);
305 if (width < STATS_FULL_MIN_SCALE)
306 width = STATS_FULL_MIN_SCALE;
307 }
308 if (width > stats->max_filestat)
309 width = 0;
310
311 for (i = 0; i < stats->files_changed; ++i) {
312 if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
313 continue;
314
315 error = diff_file_stats_full_to_buf(
316 out, delta, &stats->filestats[i], stats, width);
317 if (error < 0)
318 return error;
319 }
320 }
321
322 if (format & GIT_DIFF_STATS_FULL || format & GIT_DIFF_STATS_SHORT) {
323 git_buf_printf(
324 out, " %" PRIuZ " file%s changed",
325 stats->files_changed, stats->files_changed != 1 ? "s" : "");
326
327 if (stats->insertions || stats->deletions == 0)
328 git_buf_printf(
329 out, ", %" PRIuZ " insertion%s(+)",
330 stats->insertions, stats->insertions != 1 ? "s" : "");
331
332 if (stats->deletions || stats->insertions == 0)
333 git_buf_printf(
334 out, ", %" PRIuZ " deletion%s(-)",
335 stats->deletions, stats->deletions != 1 ? "s" : "");
336
337 git_buf_putc(out, '\n');
338
339 if (git_buf_oom(out))
340 return -1;
341 }
342
343 if (format & GIT_DIFF_STATS_INCLUDE_SUMMARY) {
344 for (i = 0; i < stats->files_changed; ++i) {
345 if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
346 continue;
347
348 error = diff_file_stats_summary_to_buf(out, delta);
349 if (error < 0)
350 return error;
351 }
352 }
353
354 return error;
355 }
356
357 void git_diff_stats_free(git_diff_stats *stats)
358 {
359 if (stats == NULL)
360 return;
361
362 git_diff_free(stats->diff); /* bumped refcount in constructor */
363 git__free(stats->filestats);
364 git__free(stats);
365 }