status.c 14.3 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 11 12
#include "git2.h"
#include "fileops.h"
#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

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

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

28
	switch (head2idx->status) {
29 30 31 32 33 34 35 36 37 38
	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;
39 40
	case GIT_DELTA_RENAMED:
		st = GIT_STATUS_INDEX_RENAMED;
41

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

	return st;
}

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

63
	switch (idx2wd->status) {
64
	case GIT_DELTA_ADDED:
65
	case GIT_DELTA_COPIED:
66 67 68
	case GIT_DELTA_UNTRACKED:
		st = GIT_STATUS_WT_NEW;
		break;
69 70 71
	case GIT_DELTA_UNREADABLE:
		st = GIT_STATUS_WT_UNREADABLE;
		break;
72 73 74 75 76 77 78 79 80
	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;
81 82
	case GIT_DELTA_RENAMED:
		st = GIT_STATUS_WT_RENAMED;
83

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

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

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

	return st;
}

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

127
	/* if excluding submodules and this is a submodule everywhere */
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
	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;
143 144
	}

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

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

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

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

162
	return st;
163 164 165 166 167
}

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

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

176
	status_entry = git__malloc(sizeof(git_status_entry));
177
	GITERR_CHECK_ALLOC(status_entry);
178

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

183
	return git_vector_insert(&status->paired, status_entry);
184 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
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)
221
{
222
	git_status_list *status = NULL;
223 224
	int (*entrycmp)(const void *a, const void *b);

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

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

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

235
	return status;
236 237
}

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

	GITERR_CHECK_VERSION(opts, GIT_STATUS_OPTIONS_VERSION, "git_status_options");

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

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

	return 0;
}

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

275 276
	*out = NULL;

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

280 281
	if ((error = git_repository__ensure_not_bare(repo, "status")) < 0 ||
		(error = git_repository_index(&index, repo)) < 0)
282
		return error;
283 284 285 286 287 288 289 290 291 292
	
	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;
			giterr_clear();
		}
293
	}
294

295 296
	/* refresh index from disk unless prevented */
	if ((flags & GIT_STATUS_OPT_NO_REFRESH) == 0 &&
297
		git_index_read(index, false) < 0)
298 299
		giterr_clear();

300 301
	status = git_status_list_alloc(index);
	GITERR_CHECK_ALLOC(status);
302

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

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

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

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

338
	if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) {
339
		if ((error = git_diff_tree_to_index(
340
				&status->head2idx, repo, head, index, &diffopt)) < 0)
341
			goto done;
342

343
		if ((flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0 &&
344
			(error = git_diff_find_similar(status->head2idx, &findopt)) < 0)
345
			goto done;
346
	}
347

348
	if (show != GIT_STATUS_SHOW_INDEX_ONLY) {
349
		if ((error = git_diff_index_to_workdir(
350
				&status->idx2wd, repo, index, &diffopt)) < 0) {
351
			goto done;
352
		}
353

354
		if ((flags & GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) != 0 &&
355
			(error = git_diff_find_similar(status->idx2wd, &findopt)) < 0)
356
			goto done;
357
	}
358

359 360 361 362
	error = git_diff__paired_foreach(
		status->head2idx, status->idx2wd, status_collect, status);
	if (error < 0)
		goto done;
363

364 365 366 367 368 369 370 371 372 373
	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)
374
		git_vector_sort(&status->paired);
375

376 377 378 379 380
done:
	if (error < 0) {
		git_status_list_free(status);
		status = NULL;
	}
381

382
	*out = status;
383

384 385
	if (opts == NULL || opts->baseline != head)
		git_tree_free(head);
386
	git_index_free(index);
387

388 389 390
	return error;
}

391
size_t git_status_list_entrycount(git_status_list *status)
392
{
393
	assert(status);
394

395
	return status->paired.length;
396 397
}

398
const git_status_entry *git_status_byindex(git_status_list *status, size_t i)
399
{
400
	assert(status);
401

402
	return git_vector_get(&status->paired, i);
403 404
}

405
void git_status_list_free(git_status_list *status)
406
{
407
	if (status == NULL)
408 409
		return;

410 411
	git_diff_free(status->head2idx);
	git_diff_free(status->idx2wd);
412

413
	git_vector_free_deep(&status->paired);
414

415 416
	git__memzero(status, sizeof(*status));
	git__free(status);
417 418 419 420 421 422 423 424
}

int git_status_foreach_ext(
	git_repository *repo,
	const git_status_options *opts,
	git_status_cb cb,
	void *payload)
{
425
	git_status_list *status;
426 427 428 429
	const git_status_entry *status_entry;
	size_t i;
	int error = 0;

430
	if ((error = git_status_list_new(&status, repo, opts)) < 0) {
431
		return error;
432
	}
433

434
	git_vector_foreach(&status->paired, i, status_entry) {
435 436 437 438
		const char *path = status_entry->head_to_index ?
			status_entry->head_to_index->old_file.path :
			status_entry->index_to_workdir->old_file.path;

439
		if ((error = cb(path, status_entry->status, payload)) != 0) {
440
			giterr_set_after_callback(error);
441
			break;
442
		}
443 444
	}

445
	git_status_list_free(status);
Russell Belfer committed
446

447
	return error;
448 449
}

450
int git_status_foreach(git_repository *repo, git_status_cb cb, void *payload)
451
{
452
	return git_status_foreach_ext(repo, NULL, cb, payload);
453 454
}

455
struct status_file_info {
456
	char *expected;
457 458
	unsigned int count;
	unsigned int status;
459
	int fnm_flags;
460
	int ambiguous;
461 462
};

463
static int get_one_status(const char *path, unsigned int status, void *data)
464
{
465
	struct status_file_info *sfi = data;
466
	int (*strcomp)(const char *a, const char *b);
467

468 469
	sfi->count++;
	sfi->status = status;
470

471 472
	strcomp = (sfi->fnm_flags & FNM_CASEFOLD) ? git__strcasecmp : git__strcmp;

473
	if (sfi->count > 1 ||
474 475
		(strcomp(sfi->expected, path) != 0 &&
		 p_fnmatch(sfi->expected, path, sfi->fnm_flags) != 0))
476
	{
477
		sfi->ambiguous = true;
478
		return GIT_EAMBIGUOUS; /* giterr_set will be done by caller */
479
	}
480

481
	return 0;
482 483
}

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

494 495
	assert(status_flags && repo && path);

496 497 498
	if ((error = git_repository_index__weakptr(&index, repo)) < 0)
		return error;

499
	if ((sfi.expected = git__strdup(path)) == NULL)
500
		return -1;
501 502
	if (index->ignore_case)
		sfi.fnm_flags = FNM_CASEFOLD;
503

504
	opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
505
	opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED |
506
		GIT_STATUS_OPT_RECURSE_IGNORED_DIRS |
507 508
		GIT_STATUS_OPT_INCLUDE_UNTRACKED |
		GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS |
509 510
		GIT_STATUS_OPT_INCLUDE_UNMODIFIED |
		GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
511 512
	opts.pathspec.count = 1;
	opts.pathspec.strings = &sfi.expected;
513

514
	error = git_status_foreach_ext(repo, &opts, get_one_status, &sfi);
515

516 517
	if (error < 0 && sfi.ambiguous) {
		giterr_set(GITERR_INVALID,
518
			"ambiguous path '%s' given to git_status_file", sfi.expected);
519
		error = GIT_EAMBIGUOUS;
520
	}
521

522
	if (!error && !sfi.count) {
523
		giterr_set(GITERR_INVALID,
524
			"attempt to get status of nonexistent file '%s'", path);
525
		error = GIT_ENOTFOUND;
526 527
	}

528
	*status_flags = sfi.status;
529

530
	git__free(sfi.expected);
531

532
	return error;
533
}
534

535
int git_status_should_ignore(
536 537 538
	int *ignored,
	git_repository *repo,
	const char *path)
Russell Belfer committed
539
{
540
	return git_ignore_path_is_ignored(ignored, repo, path);
Russell Belfer committed
541 542
}

543
int git_status_init_options(git_status_options *opts, unsigned int version)
544
{
545 546
	GIT_INIT_STRUCTURE_FROM_TEMPLATE(
		opts, version, git_status_options, GIT_STATUS_OPTIONS_INIT);
547 548 549 550 551 552
	return 0;
}

int git_status_list_get_perfdata(
	git_diff_perfdata *out, const git_status_list *status)
{
553 554
	assert(out);
	GITERR_CHECK_VERSION(out, GIT_DIFF_PERFDATA_VERSION, "git_diff_perfdata");
555 556 557 558 559 560 561 562 563 564 565 566 567 568

	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;
569
}
570