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