diff_stats.c 7.54 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
/*
 * 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.
 */
#include "common.h"
#include "vector.h"
#include "diff.h"
#include "diff_patch.h"

#define DIFF_RENAME_FILE_SEPARATOR " => "
13 14 15 16 17 18
#define STATS_FULL_MIN_SCALE 7

typedef struct {
	size_t insertions;
	size_t deletions;
} diff_file_stats;
19 20

struct git_diff_stats {
21 22
	git_diff *diff;
	diff_file_stats *filestats;
23 24 25 26

	size_t files_changed;
	size_t insertions;
	size_t deletions;
27
	size_t renames;
28

29 30 31 32
	size_t max_name;
	size_t max_filestat;
	int max_digits;
};
33

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

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

	return count;
}

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

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

62
	if (git_buf_printf(out, " %s", old_path) < 0)
63 64 65
		goto on_error;

	if (strcmp(old_path, new_path) != 0) {
66
		padding = stats->max_name - strlen(old_path) - strlen(new_path);
67

68
		if (git_buf_printf(out, DIFF_RENAME_FILE_SEPARATOR "%s", new_path) < 0)
69
			goto on_error;
70 71
	} else {
		padding = stats->max_name - strlen(old_path);
72

73
		if (stats->renames > 0)
74 75 76
			padding += strlen(DIFF_RENAME_FILE_SEPARATOR);
	}

77 78 79
	if (git_buf_putcn(out, ' ', padding) < 0 ||
		git_buf_puts(out, " | ") < 0)
		goto on_error;
80 81

	if (delta->flags & GIT_DIFF_FLAG_BINARY) {
82 83
		if (git_buf_printf(out,
				"Bin %" PRIuZ " -> %" PRIuZ " bytes", old_size, new_size) < 0)
84 85 86
			goto on_error;
	}
	else {
87 88 89
		if (git_buf_printf(out,
				"%*" PRIuZ, stats->max_digits,
				filestat->insertions + filestat->deletions) < 0)
90 91
			goto on_error;

92 93 94
		if (filestat->insertions || filestat->deletions) {
			if (git_buf_putc(out, ' ') < 0)
				goto on_error;
95

96
			if (!width) {
97 98
				if (git_buf_putcn(out, '+', filestat->insertions) < 0 ||
					git_buf_putcn(out, '-', filestat->deletions) < 0)
99
					goto on_error;
100 101
			} else {
				size_t total = filestat->insertions + filestat->deletions;
102 103
				size_t full = (total * width + stats->max_filestat / 2) /
					stats->max_filestat;
104
				size_t plus = full * filestat->insertions / total;
105
				size_t minus = full - plus;
106 107 108 109 110

				if (git_buf_putcn(out, '+', max(plus,  1)) < 0 ||
					git_buf_putcn(out, '-', max(minus, 1)) < 0)
					goto on_error;
			}
111 112 113
		}
	}

114
	git_buf_putc(out, '\n');
115 116

on_error:
117
	return (git_buf_oom(out) ? -1 : 0);
118 119 120 121
}

int git_diff_file_stats__number_to_buf(
	git_buf *out,
122 123
	const git_diff_delta *delta,
	const diff_file_stats *filestats)
124 125
{
	int error;
126
	const char *path = delta->new_file.path;
127 128 129 130

	if (delta->flags & GIT_DIFF_FLAG_BINARY)
		error = git_buf_printf(out, "%-8c" "%-8c" "%s\n", '-', '-', path);
	else
131 132
		error = git_buf_printf(out, "%-8" PRIuZ "%-8" PRIuZ "%s\n",
			filestats->insertions, filestats->deletions, path);
133 134 135 136 137 138

	return error;
}

int git_diff_file_stats__summary_to_buf(
	git_buf *out,
139
	const git_diff_delta *delta)
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
{
	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));
	GITERR_CHECK_ALLOC(stats);

	deltas = git_diff_num_deltas(diff);

175 176 177 178 179 180 181 182 183 184
	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) {
185
		git_patch *patch = NULL;
186 187
		size_t add = 0, remove = 0, namelen;
		const git_diff_delta *delta;
188 189

		if ((error = git_patch_from_diff(&patch, diff, i)) < 0)
190
			break;
191

192 193 194 195 196 197 198
		/* keep a count of renames because it will affect formatting */
		delta = git_patch_get_delta(patch);

		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++;
199 200
		}

201 202 203 204 205 206 207 208
		/* 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;

209 210
		total_insertions += add;
		total_deletions += remove;
211 212 213 214 215

		if (stats->max_name < namelen)
			stats->max_name = namelen;
		if (stats->max_filestat < add + remove)
			stats->max_filestat = add + remove;
216 217 218 219 220
	}

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

223 224 225 226
	if (error < 0) {
		git_diff_stats_free(stats);
		stats = NULL;
	}
227

228
	*out = stats;
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
	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,
259 260
	git_diff_stats_format_t format,
	size_t width)
261
{
262
	int error = 0;
263
	size_t i;
264
	const git_diff_delta *delta;
265 266 267

	assert(out && stats);

268 269 270 271
	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;
272

273 274 275 276
			error = git_diff_file_stats__number_to_buf(
				out, delta, &stats->filestats[i]);
			if (error < 0)
				return error;
277
		}
278 279 280 281 282
	}

	if (format & GIT_DIFF_STATS_FULL) {
		if (width > 0) {
			if (width > stats->max_name + stats->max_digits + 5)
283 284 285
				width -= (stats->max_name + stats->max_digits + 5);
			if (width < STATS_FULL_MIN_SCALE)
				width = STATS_FULL_MIN_SCALE;
286
		}
287 288
		if (width > stats->max_filestat)
			width = 0;
289

290 291 292 293 294
		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(
295
				out, delta, &stats->filestats[i], stats, width);
296 297 298
			if (error < 0)
				return error;
		}
299 300 301
	}

	if (format & GIT_DIFF_STATS_FULL || format & GIT_DIFF_STATS_SHORT) {
302 303 304 305 306 307
		error = git_buf_printf(
			out, " %" PRIuZ " file%s changed, %" PRIuZ
			" insertion%s(+), %" PRIuZ " deletion%s(-)\n",
			stats->files_changed, stats->files_changed != 1 ? "s" : "",
			stats->insertions, stats->insertions != 1 ? "s" : "",
			stats->deletions, stats->deletions != 1 ? "s" : "");
308 309 310 311 312 313

		if (error < 0)
			return error;
	}

	if (format & GIT_DIFF_STATS_INCLUDE_SUMMARY) {
314 315 316 317 318 319
		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)
320 321 322 323 324 325 326 327 328 329 330 331
				return error;
		}
	}

	return error;
}

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

332 333
	git_diff_free(stats->diff); /* bumped refcount in constructor */
	git__free(stats->filestats);
334 335 336
	git__free(stats);
}