diff_stats.c 8.57 KB
Newer Older
1 2 3 4 5 6
/*
 * Copyright (C) the libgit2 contributors. All rights reserved.
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */
7

8
#include "diff_stats.h"
9

10 11
#include "buf.h"
#include "common.h"
12 13
#include "vector.h"
#include "diff.h"
14
#include "patch_generate.h"
15 16

#define DIFF_RENAME_FILE_SEPARATOR " => "
17 18 19 20 21 22
#define STATS_FULL_MIN_SCALE 7

typedef struct {
	size_t insertions;
	size_t deletions;
} diff_file_stats;
23 24

struct git_diff_stats {
25 26
	git_diff *diff;
	diff_file_stats *filestats;
27 28 29 30

	size_t files_changed;
	size_t insertions;
	size_t deletions;
31
	size_t renames;
32

33 34 35 36
	size_t max_name;
	size_t max_filestat;
	int max_digits;
};
37

38 39 40 41 42 43 44 45 46 47 48 49 50
static int digits_for_value(size_t val)
{
	int count = 1;
	size_t placevalue = 10;

	while (val >= placevalue) {
		++count;
		placevalue *= 10;
	}

	return count;
}

51
static int diff_file_stats_full_to_buf(
52
	git_str *out,
53 54 55
	const git_diff_delta *delta,
	const diff_file_stats *filestat,
	const git_diff_stats *stats,
56
	size_t width)
57
{
58
	const char *old_path = NULL, *new_path = NULL, *adddel_path = NULL;
59
	size_t padding;
60
	git_object_size_t old_size, new_size;
61 62 63 64 65 66

	old_path = delta->old_file.path;
	new_path = delta->new_file.path;
	old_size = delta->old_file.size;
	new_size = delta->new_file.size;

67
	if (old_path && new_path && strcmp(old_path, new_path) != 0) {
68 69 70
		size_t common_dirlen;
		int error;

71
		padding = stats->max_name - strlen(old_path) - strlen(new_path);
72

73
		if ((common_dirlen = git_fs_path_common_dirlen(old_path, new_path)) &&
74
		    common_dirlen <= INT_MAX) {
75
			error = git_str_printf(out, " %.*s{%s"DIFF_RENAME_FILE_SEPARATOR"%s}",
76 77 78 79
					       (int) common_dirlen, old_path,
					       old_path + common_dirlen,
					       new_path + common_dirlen);
		} else {
80
			error = git_str_printf(out, " %s" DIFF_RENAME_FILE_SEPARATOR "%s",
81 82 83 84
					       old_path, new_path);
		}

		if (error < 0)
85
			goto on_error;
86
	} else {
87
		adddel_path = new_path ? new_path : old_path;
88
		if (git_str_printf(out, " %s", adddel_path) < 0)
89 90
			goto on_error;

91
		padding = stats->max_name - strlen(adddel_path);
92

93
		if (stats->renames > 0)
94 95 96
			padding += strlen(DIFF_RENAME_FILE_SEPARATOR);
	}

97 98
	if (git_str_putcn(out, ' ', padding) < 0 ||
		git_str_puts(out, " | ") < 0)
99
		goto on_error;
100 101

	if (delta->flags & GIT_DIFF_FLAG_BINARY) {
102
		if (git_str_printf(out,
103
				"Bin %" PRId64 " -> %" PRId64 " bytes", old_size, new_size) < 0)
104 105 106
			goto on_error;
	}
	else {
107
		if (git_str_printf(out,
108 109
				"%*" PRIuZ, stats->max_digits,
				filestat->insertions + filestat->deletions) < 0)
110 111
			goto on_error;

112
		if (filestat->insertions || filestat->deletions) {
113
			if (git_str_putc(out, ' ') < 0)
114
				goto on_error;
115

116
			if (!width) {
117 118
				if (git_str_putcn(out, '+', filestat->insertions) < 0 ||
					git_str_putcn(out, '-', filestat->deletions) < 0)
119
					goto on_error;
120 121
			} else {
				size_t total = filestat->insertions + filestat->deletions;
122 123
				size_t full = (total * width + stats->max_filestat / 2) /
					stats->max_filestat;
124
				size_t plus = full * filestat->insertions / total;
125
				size_t minus = full - plus;
126

127 128
				if (git_str_putcn(out, '+', max(plus,  1)) < 0 ||
					git_str_putcn(out, '-', max(minus, 1)) < 0)
129 130
					goto on_error;
			}
131 132 133
		}
	}

134
	git_str_putc(out, '\n');
135 136

on_error:
137
	return (git_str_oom(out) ? -1 : 0);
138 139
}

140
static int diff_file_stats_number_to_buf(
141
	git_str *out,
142 143
	const git_diff_delta *delta,
	const diff_file_stats *filestats)
144 145
{
	int error;
146
	const char *path = delta->new_file.path;
147 148

	if (delta->flags & GIT_DIFF_FLAG_BINARY)
149
		error = git_str_printf(out, "%-8c" "%-8c" "%s\n", '-', '-', path);
150
	else
151
		error = git_str_printf(out, "%-8" PRIuZ "%-8" PRIuZ "%s\n",
152
			filestats->insertions, filestats->deletions, path);
153 154 155 156

	return error;
}

157
static int diff_file_stats_summary_to_buf(
158
	git_str *out,
159
	const git_diff_delta *delta)
160 161 162
{
	if (delta->old_file.mode != delta->new_file.mode) {
		if (delta->old_file.mode == 0) {
163
			git_str_printf(out, " create mode %06o %s\n",
164 165 166
				delta->new_file.mode, delta->new_file.path);
		}
		else if (delta->new_file.mode == 0) {
167
			git_str_printf(out, " delete mode %06o %s\n",
168 169 170
				delta->old_file.mode, delta->old_file.path);
		}
		else {
171
			git_str_printf(out, " mode change %06o => %06o %s\n",
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
				delta->old_file.mode, delta->new_file.mode, delta->new_file.path);
		}
	}

	return 0;
}

int git_diff_get_stats(
	git_diff_stats **out,
	git_diff *diff)
{
	size_t i, deltas;
	size_t total_insertions = 0, total_deletions = 0;
	git_diff_stats *stats = NULL;
	int error = 0;

Edward Thomson committed
188 189
	GIT_ASSERT_ARG(out);
	GIT_ASSERT_ARG(diff);
190 191

	stats = git__calloc(1, sizeof(git_diff_stats));
192
	GIT_ERROR_CHECK_ALLOC(stats);
193 194 195

	deltas = git_diff_num_deltas(diff);

196 197 198 199 200 201 202 203 204 205
	stats->filestats = git__calloc(deltas, sizeof(diff_file_stats));
	if (!stats->filestats) {
		git__free(stats);
		return -1;
	}

	stats->diff = diff;
	GIT_REFCOUNT_INC(diff);

	for (i = 0; i < deltas && !error; ++i) {
206
		git_patch *patch = NULL;
207 208
		size_t add = 0, remove = 0, namelen;
		const git_diff_delta *delta;
209 210

		if ((error = git_patch_from_diff(&patch, diff, i)) < 0)
211
			break;
212

213
		/* keep a count of renames because it will affect formatting */
214
		delta = patch->delta;
215

216
		/* TODO ugh */
217
		namelen = strlen(delta->new_file.path);
218
		if (delta->old_file.path && strcmp(delta->old_file.path, delta->new_file.path) != 0) {
219 220
			namelen += strlen(delta->old_file.path);
			stats->renames++;
221 222
		}

223 224 225 226 227 228 229 230
		/* and, of course, count the line stats */
		error = git_patch_line_stats(NULL, &add, &remove, patch);

		git_patch_free(patch);

		stats->filestats[i].insertions = add;
		stats->filestats[i].deletions = remove;

231 232
		total_insertions += add;
		total_deletions += remove;
233 234 235 236 237

		if (stats->max_name < namelen)
			stats->max_name = namelen;
		if (stats->max_filestat < add + remove)
			stats->max_filestat = add + remove;
238 239 240 241 242
	}

	stats->files_changed = deltas;
	stats->insertions = total_insertions;
	stats->deletions = total_deletions;
243
	stats->max_digits = digits_for_value(stats->max_filestat + 1);
244

245 246 247 248
	if (error < 0) {
		git_diff_stats_free(stats);
		stats = NULL;
	}
249

250
	*out = stats;
251 252 253 254 255 256
	return error;
}

size_t git_diff_stats_files_changed(
	const git_diff_stats *stats)
{
Edward Thomson committed
257
	GIT_ASSERT_ARG(stats);
258 259 260 261 262 263 264

	return stats->files_changed;
}

size_t git_diff_stats_insertions(
	const git_diff_stats *stats)
{
Edward Thomson committed
265
	GIT_ASSERT_ARG(stats);
266 267 268 269 270 271 272

	return stats->insertions;
}

size_t git_diff_stats_deletions(
	const git_diff_stats *stats)
{
Edward Thomson committed
273
	GIT_ASSERT_ARG(stats);
274 275 276 277 278 279 280

	return stats->deletions;
}

int git_diff_stats_to_buf(
	git_buf *out,
	const git_diff_stats *stats,
281 282
	git_diff_stats_format_t format,
	size_t width)
283
{
284 285 286 287 288 289 290 291 292
	GIT_BUF_WRAP_PRIVATE(out, git_diff__stats_to_buf, stats, format, width);
}

int git_diff__stats_to_buf(
	git_str *out,
	const git_diff_stats *stats,
	git_diff_stats_format_t format,
	size_t width)
{
293
	int error = 0;
294
	size_t i;
295
	const git_diff_delta *delta;
296

Edward Thomson committed
297 298
	GIT_ASSERT_ARG(out);
	GIT_ASSERT_ARG(stats);
299

300 301 302 303
	if (format & GIT_DIFF_STATS_NUMBER) {
		for (i = 0; i < stats->files_changed; ++i) {
			if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
				continue;
304

305
			error = diff_file_stats_number_to_buf(
306 307 308
				out, delta, &stats->filestats[i]);
			if (error < 0)
				return error;
309
		}
310 311 312 313 314
	}

	if (format & GIT_DIFF_STATS_FULL) {
		if (width > 0) {
			if (width > stats->max_name + stats->max_digits + 5)
315 316 317
				width -= (stats->max_name + stats->max_digits + 5);
			if (width < STATS_FULL_MIN_SCALE)
				width = STATS_FULL_MIN_SCALE;
318
		}
319 320
		if (width > stats->max_filestat)
			width = 0;
321

322 323 324 325
		for (i = 0; i < stats->files_changed; ++i) {
			if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
				continue;

326
			error = diff_file_stats_full_to_buf(
327
				out, delta, &stats->filestats[i], stats, width);
328 329 330
			if (error < 0)
				return error;
		}
331 332 333
	}

	if (format & GIT_DIFF_STATS_FULL || format & GIT_DIFF_STATS_SHORT) {
334
		git_str_printf(
335 336 337 338
			out, " %" PRIuZ " file%s changed",
			stats->files_changed, stats->files_changed != 1 ? "s" : "");

		if (stats->insertions || stats->deletions == 0)
339
			git_str_printf(
340 341 342 343
				out, ", %" PRIuZ " insertion%s(+)",
				stats->insertions, stats->insertions != 1 ? "s" : "");

		if (stats->deletions || stats->insertions == 0)
344
			git_str_printf(
345 346 347
				out, ", %" PRIuZ " deletion%s(-)",
				stats->deletions, stats->deletions != 1 ? "s" : "");

348
		git_str_putc(out, '\n');
349

350
		if (git_str_oom(out))
351
			return -1;
352 353 354
	}

	if (format & GIT_DIFF_STATS_INCLUDE_SUMMARY) {
355 356 357 358
		for (i = 0; i < stats->files_changed; ++i) {
			if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
				continue;

359
			error = diff_file_stats_summary_to_buf(out, delta);
360
			if (error < 0)
361 362 363 364 365 366 367 368 369 370 371 372
				return error;
		}
	}

	return error;
}

void git_diff_stats_free(git_diff_stats *stats)
{
	if (stats == NULL)
		return;

373 374
	git_diff_free(stats->diff); /* bumped refcount in constructor */
	git__free(stats->filestats);
375 376
	git__free(stats);
}