diff_stats.c 8.14 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 "common.h"
9

10 11
#include "vector.h"
#include "diff.h"
12
#include "patch_generate.h"
13 14

#define DIFF_RENAME_FILE_SEPARATOR " => "
15 16 17 18 19 20
#define STATS_FULL_MIN_SCALE 7

typedef struct {
	size_t insertions;
	size_t deletions;
} diff_file_stats;
21 22

struct git_diff_stats {
23 24
	git_diff *diff;
	diff_file_stats *filestats;
25 26 27 28

	size_t files_changed;
	size_t insertions;
	size_t deletions;
29
	size_t renames;
30

31 32 33 34
	size_t max_name;
	size_t max_filestat;
	int max_digits;
};
35

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

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

	return count;
}

49 50
int git_diff_file_stats__full_to_buf(
	git_buf *out,
51 52 53
	const git_diff_delta *delta,
	const diff_file_stats *filestat,
	const git_diff_stats *stats,
54
	size_t width)
55 56
{
	const char *old_path = NULL, *new_path = NULL;
57 58
	size_t padding;
	git_off_t old_size, new_size;
59 60 61 62 63 64 65

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

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

69
		padding = stats->max_name - strlen(old_path) - strlen(new_path);
70

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

		if (error < 0)
83
			goto on_error;
84
	} else {
85 86 87
		if (git_buf_printf(out, " %s", old_path) < 0)
			goto on_error;

88
		padding = stats->max_name - strlen(old_path);
89

90
		if (stats->renames > 0)
91 92 93
			padding += strlen(DIFF_RENAME_FILE_SEPARATOR);
	}

94 95 96
	if (git_buf_putcn(out, ' ', padding) < 0 ||
		git_buf_puts(out, " | ") < 0)
		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 102 103
			goto on_error;
	}
	else {
104 105 106
		if (git_buf_printf(out,
				"%*" PRIuZ, stats->max_digits,
				filestat->insertions + filestat->deletions) < 0)
107 108
			goto on_error;

109 110 111
		if (filestat->insertions || filestat->deletions) {
			if (git_buf_putc(out, ' ') < 0)
				goto on_error;
112

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

				if (git_buf_putcn(out, '+', max(plus,  1)) < 0 ||
					git_buf_putcn(out, '-', max(minus, 1)) < 0)
					goto on_error;
			}
128 129 130
		}
	}

131
	git_buf_putc(out, '\n');
132 133

on_error:
134
	return (git_buf_oom(out) ? -1 : 0);
135 136 137 138
}

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

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

	return error;
}

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

	assert(out && diff);

	stats = git__calloc(1, sizeof(git_diff_stats));
188
	GIT_ERROR_CHECK_ALLOC(stats);
189 190 191

	deltas = git_diff_num_deltas(diff);

192 193 194 195 196 197 198 199 200 201
	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) {
202
		git_patch *patch = NULL;
203 204
		size_t add = 0, remove = 0, namelen;
		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 214 215 216
		namelen = strlen(delta->new_file.path);
		if (strcmp(delta->old_file.path, delta->new_file.path) != 0) {
			namelen += strlen(delta->old_file.path);
			stats->renames++;
217 218
		}

219 220 221 222 223 224 225 226
		/* 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;

227 228
		total_insertions += add;
		total_deletions += remove;
229 230 231 232 233

		if (stats->max_name < namelen)
			stats->max_name = namelen;
		if (stats->max_filestat < add + remove)
			stats->max_filestat = add + remove;
234 235 236 237 238
	}

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

241 242 243 244
	if (error < 0) {
		git_diff_stats_free(stats);
		stats = NULL;
	}
245

246
	*out = stats;
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
	return error;
}

size_t git_diff_stats_files_changed(
	const git_diff_stats *stats)
{
	assert(stats);

	return stats->files_changed;
}

size_t git_diff_stats_insertions(
	const git_diff_stats *stats)
{
	assert(stats);

	return stats->insertions;
}

size_t git_diff_stats_deletions(
	const git_diff_stats *stats)
{
	assert(stats);

	return stats->deletions;
}

int git_diff_stats_to_buf(
	git_buf *out,
	const git_diff_stats *stats,
277 278
	git_diff_stats_format_t format,
	size_t width)
279
{
280
	int error = 0;
281
	size_t i;
282
	const git_diff_delta *delta;
283 284 285

	assert(out && stats);

286 287 288 289
	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;
290

291 292 293 294
			error = git_diff_file_stats__number_to_buf(
				out, delta, &stats->filestats[i]);
			if (error < 0)
				return error;
295
		}
296 297 298 299 300
	}

	if (format & GIT_DIFF_STATS_FULL) {
		if (width > 0) {
			if (width > stats->max_name + stats->max_digits + 5)
301 302 303
				width -= (stats->max_name + stats->max_digits + 5);
			if (width < STATS_FULL_MIN_SCALE)
				width = STATS_FULL_MIN_SCALE;
304
		}
305 306
		if (width > stats->max_filestat)
			width = 0;
307

308 309 310 311 312
		for (i = 0; i < stats->files_changed; ++i) {
			if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
				continue;

			error = git_diff_file_stats__full_to_buf(
313
				out, delta, &stats->filestats[i], stats, width);
314 315 316
			if (error < 0)
				return error;
		}
317 318 319
	}

	if (format & GIT_DIFF_STATS_FULL || format & GIT_DIFF_STATS_SHORT) {
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
		git_buf_printf(
			out, " %" PRIuZ " file%s changed",
			stats->files_changed, stats->files_changed != 1 ? "s" : "");

		if (stats->insertions || stats->deletions == 0)
			git_buf_printf(
				out, ", %" PRIuZ " insertion%s(+)",
				stats->insertions, stats->insertions != 1 ? "s" : "");

		if (stats->deletions || stats->insertions == 0)
			git_buf_printf(
				out, ", %" PRIuZ " deletion%s(-)",
				stats->deletions, stats->deletions != 1 ? "s" : "");

		git_buf_putc(out, '\n');

		if (git_buf_oom(out))
			return -1;
338 339 340
	}

	if (format & GIT_DIFF_STATS_INCLUDE_SUMMARY) {
341 342 343 344 345 346
		for (i = 0; i < stats->files_changed; ++i) {
			if ((delta = git_diff_get_delta(stats->diff, i)) == NULL)
				continue;

			error = git_diff_file_stats__summary_to_buf(out, delta);
			if (error < 0)
347 348 349 350 351 352 353 354 355 356 357 358
				return error;
		}
	}

	return error;
}

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

359 360
	git_diff_free(stats->diff); /* bumped refcount in constructor */
	git__free(stats->filestats);
361 362
	git__free(stats);
}