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

8 9
#include "status.h"

10
#include "git2.h"
11
#include "futils.h"
12
#include "hash.h"
13 14 15
#include "vector.h"
#include "tree.h"
#include "git2/status.h"
16
#include "repository.h"
17
#include "ignore.h"
18
#include "index.h"
19
#include "wildmatch.h"
20

21 22
#include "git2/diff.h"
#include "diff.h"
23
#include "diff_generate.h"
24

25
static unsigned int index_delta2status(const git_diff_delta *head2idx)
26
{
27
	git_status_t st = GIT_STATUS_CURRENT;
28

29
	switch (head2idx->status) {
30 31 32 33 34 35 36 37 38 39
	case GIT_DELTA_ADDED:
	case GIT_DELTA_COPIED:
		st = GIT_STATUS_INDEX_NEW;
		break;
	case GIT_DELTA_DELETED:
		st = GIT_STATUS_INDEX_DELETED;
		break;
	case GIT_DELTA_MODIFIED:
		st = GIT_STATUS_INDEX_MODIFIED;
		break;
40 41
	case GIT_DELTA_RENAMED:
		st = GIT_STATUS_INDEX_RENAMED;
42

43
		if (!git_oid_equal(&head2idx->old_file.id, &head2idx->new_file.id))
44
			st |= GIT_STATUS_INDEX_MODIFIED;
45 46 47 48
		break;
	case GIT_DELTA_TYPECHANGE:
		st = GIT_STATUS_INDEX_TYPECHANGE;
		break;
49 50 51
	case GIT_DELTA_CONFLICTED:
		st = GIT_STATUS_CONFLICTED;
		break;
52 53 54 55 56 57 58
	default:
		break;
	}

	return st;
}

59
static unsigned int workdir_delta2status(
60
	git_diff *diff, git_diff_delta *idx2wd)
61
{
62
	git_status_t st = GIT_STATUS_CURRENT;
63

64
	switch (idx2wd->status) {
65
	case GIT_DELTA_ADDED:
66
	case GIT_DELTA_COPIED:
67 68 69
	case GIT_DELTA_UNTRACKED:
		st = GIT_STATUS_WT_NEW;
		break;
70 71 72
	case GIT_DELTA_UNREADABLE:
		st = GIT_STATUS_WT_UNREADABLE;
		break;
73 74 75 76 77 78 79 80 81
	case GIT_DELTA_DELETED:
		st = GIT_STATUS_WT_DELETED;
		break;
	case GIT_DELTA_MODIFIED:
		st = GIT_STATUS_WT_MODIFIED;
		break;
	case GIT_DELTA_IGNORED:
		st = GIT_STATUS_IGNORED;
		break;
82 83
	case GIT_DELTA_RENAMED:
		st = GIT_STATUS_WT_RENAMED;
84

85
		if (!git_oid_equal(&idx2wd->old_file.id, &idx2wd->new_file.id)) {
86
			/* if OIDs don't match, we might need to calculate them now to
Dimitris Apostolou committed
87
			 * discern between RENAMED vs RENAMED+MODIFIED
88
			 */
89
			if (git_oid_is_zero(&idx2wd->old_file.id) &&
90
				diff->old_src == GIT_ITERATOR_WORKDIR &&
91
				!git_diff__oid_for_file(
92 93
					&idx2wd->old_file.id, diff, idx2wd->old_file.path,
					idx2wd->old_file.mode, idx2wd->old_file.size))
94
			idx2wd->old_file.flags |= GIT_DIFF_FLAG_VALID_ID;
95

96
			if (git_oid_is_zero(&idx2wd->new_file.id) &&
97
				diff->new_src == GIT_ITERATOR_WORKDIR &&
98
				!git_diff__oid_for_file(
99 100
					&idx2wd->new_file.id, diff, idx2wd->new_file.path,
					idx2wd->new_file.mode, idx2wd->new_file.size))
101
				idx2wd->new_file.flags |= GIT_DIFF_FLAG_VALID_ID;
102

103
			if (!git_oid_equal(&idx2wd->old_file.id, &idx2wd->new_file.id))
104 105
				st |= GIT_STATUS_WT_MODIFIED;
		}
106
		break;
107 108 109
	case GIT_DELTA_TYPECHANGE:
		st = GIT_STATUS_WT_TYPECHANGE;
		break;
110 111 112
	case GIT_DELTA_CONFLICTED:
		st = GIT_STATUS_CONFLICTED;
		break;
113 114 115 116 117 118 119
	default:
		break;
	}

	return st;
}

120
static bool status_is_included(
121
	git_status_list *status,
122 123
	git_diff_delta *head2idx,
	git_diff_delta *idx2wd)
124
{
125 126 127
	if (!(status->opts.flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES))
		return 1;

128
	/* if excluding submodules and this is a submodule everywhere */
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
	if (head2idx) {
		if (head2idx->status != GIT_DELTA_ADDED &&
			head2idx->old_file.mode != GIT_FILEMODE_COMMIT)
			return 1;
		if (head2idx->status != GIT_DELTA_DELETED &&
			head2idx->new_file.mode != GIT_FILEMODE_COMMIT)
			return 1;
	}
	if (idx2wd) {
		if (idx2wd->status != GIT_DELTA_ADDED &&
			idx2wd->old_file.mode != GIT_FILEMODE_COMMIT)
			return 1;
		if (idx2wd->status != GIT_DELTA_DELETED &&
			idx2wd->new_file.mode != GIT_FILEMODE_COMMIT)
			return 1;
144 145
	}

146 147
	/* only get here if every valid mode is GIT_FILEMODE_COMMIT */
	return 0;
148 149
}

150
static git_status_t status_compute(
151
	git_status_list *status,
152 153 154
	git_diff_delta *head2idx,
	git_diff_delta *idx2wd)
{
155
	git_status_t st = GIT_STATUS_CURRENT;
156 157

	if (head2idx)
158
		st |= index_delta2status(head2idx);
159 160

	if (idx2wd)
161
		st |= workdir_delta2status(status->idx2wd, idx2wd);
162

163
	return st;
164 165 166 167 168
}

static int status_collect(
	git_diff_delta *head2idx,
	git_diff_delta *idx2wd,
169
	void *payload)
170
{
171
	git_status_list *status = payload;
172
	git_status_entry *status_entry;
173

174
	if (!status_is_included(status, head2idx, idx2wd))
175
		return 0;
176

177
	status_entry = git__malloc(sizeof(git_status_entry));
178
	GIT_ERROR_CHECK_ALLOC(status_entry);
179

180
	status_entry->status = status_compute(status, head2idx, idx2wd);
181 182 183
	status_entry->head_to_index = head2idx;
	status_entry->index_to_workdir = idx2wd;

184
	return git_vector_insert(&status->paired, status_entry);
185 186
}

187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
GIT_INLINE(int) status_entry_cmp_base(
	const void *a,
	const void *b,
	int (*strcomp)(const char *a, const char *b))
{
	const git_status_entry *entry_a = a;
	const git_status_entry *entry_b = b;
	const git_diff_delta *delta_a, *delta_b;

	delta_a = entry_a->index_to_workdir ? entry_a->index_to_workdir :
		entry_a->head_to_index;
	delta_b = entry_b->index_to_workdir ? entry_b->index_to_workdir :
		entry_b->head_to_index;

	if (!delta_a && delta_b)
		return -1;
	if (delta_a && !delta_b)
		return 1;
	if (!delta_a && !delta_b)
		return 0;

	return strcomp(delta_a->new_file.path, delta_b->new_file.path);
}

static int status_entry_icmp(const void *a, const void *b)
{
	return status_entry_cmp_base(a, b, git__strcasecmp);
}

static int status_entry_cmp(const void *a, const void *b)
{
	return status_entry_cmp_base(a, b, git__strcmp);
}

static git_status_list *git_status_list_alloc(git_index *index)
222
{
223
	git_status_list *status = NULL;
224 225
	int (*entrycmp)(const void *a, const void *b);

226 227 228
	if (!(status = git__calloc(1, sizeof(git_status_list))))
		return NULL;

229
	entrycmp = index->ignore_case ? status_entry_icmp : status_entry_cmp;
230

231 232
	if (git_vector_init(&status->paired, 0, entrycmp) < 0) {
		git__free(status);
233
		return NULL;
234
	}
235

236
	return status;
237 238
}

239 240 241 242 243
static int status_validate_options(const git_status_options *opts)
{
	if (!opts)
		return 0;

244
	GIT_ERROR_CHECK_VERSION(opts, GIT_STATUS_OPTIONS_VERSION, "git_status_options");
245 246

	if (opts->show > GIT_STATUS_SHOW_WORKDIR_ONLY) {
247
		git_error_set(GIT_ERROR_INVALID, "unknown status 'show' option");
248 249 250 251 252
		return -1;
	}

	if ((opts->flags & GIT_STATUS_OPT_NO_REFRESH) != 0 &&
		(opts->flags & GIT_STATUS_OPT_UPDATE_INDEX) != 0) {
253
		git_error_set(GIT_ERROR_INVALID, "updating index from status "
254 255 256 257 258 259 260
			"is not allowed when index refresh is disabled");
		return -1;
	}

	return 0;
}

261 262 263 264 265
int git_status_list_new(
	git_status_list **out,
	git_repository *repo,
	const git_status_options *opts)
{
266
	git_index *index = NULL;
267
	git_status_list *status = NULL;
268
	git_diff_options diffopt = GIT_DIFF_OPTIONS_INIT;
269
	git_diff_find_options findopt = GIT_DIFF_FIND_OPTIONS_INIT;
270 271 272
	git_tree *head = NULL;
	git_status_show_t show =
		opts ? opts->show : GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
273
	int error = 0;
274
	unsigned int flags = opts ? opts->flags : GIT_STATUS_OPT_DEFAULTS;
275

276 277
	*out = NULL;

278 279
	if (status_validate_options(opts) < 0)
		return -1;
280

281 282
	if ((error = git_repository__ensure_not_bare(repo, "status")) < 0 ||
		(error = git_repository_index(&index, repo)) < 0)
283
		return error;
284

285 286 287 288 289 290 291
	if (opts != NULL && opts->baseline != NULL) {
		head = opts->baseline;
	} else {
		/* if there is no HEAD, that's okay - we'll make an empty iterator */
		if ((error = git_repository_head_tree(&head, repo)) < 0) {
			if (error != GIT_ENOTFOUND && error != GIT_EUNBORNBRANCH)
				goto done;
292
			git_error_clear();
293
		}
294
	}
295

296 297
	/* refresh index from disk unless prevented */
	if ((flags & GIT_STATUS_OPT_NO_REFRESH) == 0 &&
298
		git_index_read_safely(index) < 0)
299
		git_error_clear();
300

301
	status = git_status_list_alloc(index);
302
	GIT_ERROR_CHECK_ALLOC(status);
303

304 305 306 307
	if (opts) {
		memcpy(&status->opts, opts, sizeof(git_status_options));
		memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec));
	}
308

309
	diffopt.flags = GIT_DIFF_INCLUDE_TYPECHANGE;
310
	findopt.flags = GIT_DIFF_FIND_FOR_UNTRACKED;
311

312
	if ((flags & GIT_STATUS_OPT_INCLUDE_UNTRACKED) != 0)
Russell Belfer committed
313
		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED;
314
	if ((flags & GIT_STATUS_OPT_INCLUDE_IGNORED) != 0)
Russell Belfer committed
315
		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_IGNORED;
316
	if ((flags & GIT_STATUS_OPT_INCLUDE_UNMODIFIED) != 0)
Russell Belfer committed
317
		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNMODIFIED;
318
	if ((flags & GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS) != 0)
319
		diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_UNTRACKED_DIRS;
320
	if ((flags & GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH) != 0)
321
		diffopt.flags = diffopt.flags | GIT_DIFF_DISABLE_PATHSPEC_MATCH;
322
	if ((flags & GIT_STATUS_OPT_RECURSE_IGNORED_DIRS) != 0)
323
		diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_IGNORED_DIRS;
324
	if ((flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0)
325
		diffopt.flags = diffopt.flags | GIT_DIFF_IGNORE_SUBMODULES;
326 327
	if ((flags & GIT_STATUS_OPT_UPDATE_INDEX) != 0)
		diffopt.flags = diffopt.flags | GIT_DIFF_UPDATE_INDEX;
328 329
	if ((flags & GIT_STATUS_OPT_INCLUDE_UNREADABLE) != 0)
		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNREADABLE;
330 331
	if ((flags & GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED) != 0)
		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED;
332

333
	if ((flags & GIT_STATUS_OPT_RENAMES_FROM_REWRITES) != 0)
334 335 336 337
		findopt.flags = findopt.flags |
			GIT_DIFF_FIND_AND_BREAK_REWRITES |
			GIT_DIFF_FIND_RENAMES_FROM_REWRITES |
			GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY;
338

339 340 341
	if (opts != NULL && opts->rename_threshold != 0)
		findopt.rename_threshold = opts->rename_threshold;

342
	if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) {
343
		if ((error = git_diff_tree_to_index(
344
				&status->head2idx, repo, head, index, &diffopt)) < 0)
345
			goto done;
346

347
		if ((flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0 &&
348
			(error = git_diff_find_similar(status->head2idx, &findopt)) < 0)
349
			goto done;
350
	}
351

352
	if (show != GIT_STATUS_SHOW_INDEX_ONLY) {
353
		if ((error = git_diff_index_to_workdir(
354
				&status->idx2wd, repo, index, &diffopt)) < 0) {
355
			goto done;
356
		}
357

358
		if ((flags & GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) != 0 &&
359
			(error = git_diff_find_similar(status->idx2wd, &findopt)) < 0)
360
			goto done;
361
	}
362

363 364 365 366
	error = git_diff__paired_foreach(
		status->head2idx, status->idx2wd, status_collect, status);
	if (error < 0)
		goto done;
367

368 369 370 371 372 373 374 375 376 377
	if (flags & GIT_STATUS_OPT_SORT_CASE_SENSITIVELY)
		git_vector_set_cmp(&status->paired, status_entry_cmp);
	if (flags & GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY)
		git_vector_set_cmp(&status->paired, status_entry_icmp);

	if ((flags &
		 (GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX |
		  GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR |
		  GIT_STATUS_OPT_SORT_CASE_SENSITIVELY |
		  GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY)) != 0)
378
		git_vector_sort(&status->paired);
379

380 381 382 383 384
done:
	if (error < 0) {
		git_status_list_free(status);
		status = NULL;
	}
385

386
	*out = status;
387

388 389
	if (opts == NULL || opts->baseline != head)
		git_tree_free(head);
390
	git_index_free(index);
391

392 393 394
	return error;
}

395
size_t git_status_list_entrycount(git_status_list *status)
396
{
397
	GIT_ASSERT_ARG_WITH_RETVAL(status, 0);
398

399
	return status->paired.length;
400 401
}

402
const git_status_entry *git_status_byindex(git_status_list *status, size_t i)
403
{
404
	GIT_ASSERT_ARG_WITH_RETVAL(status, NULL);
405

406
	return git_vector_get(&status->paired, i);
407 408
}

409
void git_status_list_free(git_status_list *status)
410
{
411
	if (status == NULL)
412 413
		return;

414 415
	git_diff_free(status->head2idx);
	git_diff_free(status->idx2wd);
416

417
	git_vector_free_deep(&status->paired);
418

419 420
	git__memzero(status, sizeof(*status));
	git__free(status);
421 422 423 424 425 426 427 428
}

int git_status_foreach_ext(
	git_repository *repo,
	const git_status_options *opts,
	git_status_cb cb,
	void *payload)
{
429
	git_status_list *status;
430 431 432 433
	const git_status_entry *status_entry;
	size_t i;
	int error = 0;

434
	if ((error = git_status_list_new(&status, repo, opts)) < 0) {
435
		return error;
436
	}
437

438
	git_vector_foreach(&status->paired, i, status_entry) {
439 440 441 442
		const char *path = status_entry->head_to_index ?
			status_entry->head_to_index->old_file.path :
			status_entry->index_to_workdir->old_file.path;

443
		if ((error = cb(path, status_entry->status, payload)) != 0) {
444
			git_error_set_after_callback(error);
445
			break;
446
		}
447 448
	}

449
	git_status_list_free(status);
Russell Belfer committed
450

451
	return error;
452 453
}

454
int git_status_foreach(git_repository *repo, git_status_cb cb, void *payload)
455
{
456
	return git_status_foreach_ext(repo, NULL, cb, payload);
457 458
}

459
struct status_file_info {
460
	char *expected;
461 462
	unsigned int count;
	unsigned int status;
463
	int wildmatch_flags;
464
	int ambiguous;
465 466
};

467
static int get_one_status(const char *path, unsigned int status, void *data)
468
{
469
	struct status_file_info *sfi = data;
470
	int (*strcomp)(const char *a, const char *b);
471

472 473
	sfi->count++;
	sfi->status = status;
474

475
	strcomp = (sfi->wildmatch_flags & WM_CASEFOLD) ? git__strcasecmp : git__strcmp;
476

477
	if (sfi->count > 1 ||
478
		(strcomp(sfi->expected, path) != 0 &&
479
		 wildmatch(sfi->expected, path, sfi->wildmatch_flags) != 0))
480
	{
481
		sfi->ambiguous = true;
482
		return GIT_EAMBIGUOUS; /* git_error_set will be done by caller */
483
	}
484

485
	return 0;
486 487
}

488
int git_status_file(
489 490 491
	unsigned int *status_flags,
	git_repository *repo,
	const char *path)
492
{
493
	int error;
494 495
	git_status_options opts = GIT_STATUS_OPTIONS_INIT;
	struct status_file_info sfi = {0};
496
	git_index *index;
497

498 499 500
	GIT_ASSERT_ARG(status_flags);
	GIT_ASSERT_ARG(repo);
	GIT_ASSERT_ARG(path);
501

502 503 504
	if ((error = git_repository_index__weakptr(&index, repo)) < 0)
		return error;

505
	if ((sfi.expected = git__strdup(path)) == NULL)
506
		return -1;
507
	if (index->ignore_case)
508
		sfi.wildmatch_flags = WM_CASEFOLD;
509

510
	opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
511
	opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED |
512
		GIT_STATUS_OPT_RECURSE_IGNORED_DIRS |
513 514
		GIT_STATUS_OPT_INCLUDE_UNTRACKED |
		GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS |
515 516
		GIT_STATUS_OPT_INCLUDE_UNMODIFIED |
		GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
517 518
	opts.pathspec.count = 1;
	opts.pathspec.strings = &sfi.expected;
519

520
	error = git_status_foreach_ext(repo, &opts, get_one_status, &sfi);
521

522
	if (error < 0 && sfi.ambiguous) {
523
		git_error_set(GIT_ERROR_INVALID,
524
			"ambiguous path '%s' given to git_status_file", sfi.expected);
525
		error = GIT_EAMBIGUOUS;
526
	}
527

528
	if (!error && !sfi.count) {
529
		git_error_set(GIT_ERROR_INVALID,
530
			"attempt to get status of nonexistent file '%s'", path);
531
		error = GIT_ENOTFOUND;
532 533
	}

534
	*status_flags = sfi.status;
535

536
	git__free(sfi.expected);
537

538
	return error;
539
}
540

541
int git_status_should_ignore(
542 543 544
	int *ignored,
	git_repository *repo,
	const char *path)
Russell Belfer committed
545
{
546
	return git_ignore_path_is_ignored(ignored, repo, path);
Russell Belfer committed
547 548
}

549
int git_status_options_init(git_status_options *opts, unsigned int version)
550
{
551 552
	GIT_INIT_STRUCTURE_FROM_TEMPLATE(
		opts, version, git_status_options, GIT_STATUS_OPTIONS_INIT);
553 554 555
	return 0;
}

556
#ifndef GIT_DEPRECATE_HARD
557 558 559 560
int git_status_init_options(git_status_options *opts, unsigned int version)
{
	return git_status_options_init(opts, version);
}
561
#endif
562

563 564 565
int git_status_list_get_perfdata(
	git_diff_perfdata *out, const git_status_list *status)
{
566 567
	GIT_ASSERT_ARG(out);

568
	GIT_ERROR_CHECK_VERSION(out, GIT_DIFF_PERFDATA_VERSION, "git_diff_perfdata");
569 570 571 572 573 574 575 576 577 578 579 580 581 582

	out->stat_calls = 0;
	out->oid_calculations = 0;

	if (status->head2idx) {
		out->stat_calls += status->head2idx->perf.stat_calls;
		out->oid_calculations += status->head2idx->perf.oid_calculations;
	}
	if (status->idx2wd) {
		out->stat_calls += status->idx2wd->perf.stat_calls;
		out->oid_calculations += status->idx2wd->perf.oid_calculations;
	}

	return 0;
583
}
584