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