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;
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 (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 
git_diff_file_stats__number_to_buf(git_buf * out,const git_diff_delta * delta,const diff_file_stats * filestats)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 
git_diff_file_stats__summary_to_buf(git_buf * out,const git_diff_delta * delta)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 
git_diff_get_stats(git_diff_stats ** out,git_diff * diff)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 
git_diff_stats_files_changed(const git_diff_stats * stats)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 
git_diff_stats_insertions(const git_diff_stats * stats)258 size_t git_diff_stats_insertions(
259 	const git_diff_stats *stats)
260 {
261 	assert(stats);
262 
263 	return stats->insertions;
264 }
265 
git_diff_stats_deletions(const git_diff_stats * stats)266 size_t git_diff_stats_deletions(
267 	const git_diff_stats *stats)
268 {
269 	assert(stats);
270 
271 	return stats->deletions;
272 }
273 
git_diff_stats_to_buf(git_buf * out,const git_diff_stats * stats,git_diff_stats_format_t format,size_t width)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 
git_diff_stats_free(git_diff_stats * stats)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 }
363