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 
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 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 
diff_file_stats_number_to_buf(git_buf * out,const git_diff_delta * delta,const diff_file_stats * filestats)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 
diff_file_stats_summary_to_buf(git_buf * out,const git_diff_delta * delta)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 
git_diff_get_stats(git_diff_stats ** out,git_diff * diff)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 
git_diff_stats_files_changed(const git_diff_stats * stats)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 
git_diff_stats_insertions(const git_diff_stats * stats)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 
git_diff_stats_deletions(const git_diff_stats * stats)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 
git_diff_stats_to_buf(git_buf * out,const git_diff_stats * stats,git_diff_stats_format_t format,size_t width)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 
git_diff_stats_free(git_diff_stats * stats)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 }
366