diff_stats.c 8.13 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 57 58 59 60 61 62 63 64
{
	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;

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

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

70 71 72 73 74 75 76 77 78 79 80 81
		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)
82
			goto on_error;
83
	} else {
84 85 86
		if (git_buf_printf(out, " %s", old_path) < 0)
			goto on_error;

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

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

93 94 95
	if (git_buf_putcn(out, ' ', padding) < 0 ||
		git_buf_puts(out, " | ") < 0)
		goto on_error;
96 97

	if (delta->flags & GIT_DIFF_FLAG_BINARY) {
98 99
		if (git_buf_printf(out,
				"Bin %" PRIuZ " -> %" PRIuZ " bytes", old_size, new_size) < 0)
100 101 102
			goto on_error;
	}
	else {
103 104 105
		if (git_buf_printf(out,
				"%*" PRIuZ, stats->max_digits,
				filestat->insertions + filestat->deletions) < 0)
106 107
			goto on_error;

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

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

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

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

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

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

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

	return error;
}

int git_diff_file_stats__summary_to_buf(
	git_buf *out,
155
	const git_diff_delta *delta)
156 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 188 189 190
{
	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);

191 192 193 194 195 196 197 198 199 200
	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) {
201
		git_patch *patch = NULL;
202 203
		size_t add = 0, remove = 0, namelen;
		const git_diff_delta *delta;
204 205

		if ((error = git_patch_from_diff(&patch, diff, i)) < 0)
206
			break;
207

208
		/* keep a count of renames because it will affect formatting */
209
		delta = patch->delta;
210

211
		/* TODO ugh */
212 213 214 215
		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++;
216 217
		}

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

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

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

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

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

245
	*out = stats;
246 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
	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,
276 277
	git_diff_stats_format_t format,
	size_t width)
278
{
279
	int error = 0;
280
	size_t i;
281
	const git_diff_delta *delta;
282 283 284

	assert(out && stats);

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

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

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

307 308 309 310 311
		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(
312
				out, delta, &stats->filestats[i], stats, width);
313 314 315
			if (error < 0)
				return error;
		}
316 317 318
	}

	if (format & GIT_DIFF_STATS_FULL || format & GIT_DIFF_STATS_SHORT) {
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
		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;
337 338 339
	}

	if (format & GIT_DIFF_STATS_INCLUDE_SUMMARY) {
340 341 342 343 344 345
		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)
346 347 348 349 350 351 352 353 354 355 356 357
				return error;
		}
	}

	return error;
}

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

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