status.c 14.5 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 87 88
			/* if OIDs don't match, we might need to calculate them now to
			 * discern between RENAMED vs RENAMED+MODIFED
			 */
89
			if (git_oid_is_zero(&idx2wd->old_file.id) &&
90 91
				diff->old_src == GIT_ITERATOR_TYPE_WORKDIR &&
				!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 98
				diff->new_src == GIT_ITERATOR_TYPE_WORKDIR &&
				!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
	if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) {
340
		if ((error = git_diff_tree_to_index(
341
				&status->head2idx, repo, head, index, &diffopt)) < 0)
342
			goto done;
343

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

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

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

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

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

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

383
	*out = status;
384

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

389 390 391
	return error;
}

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

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

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

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

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

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

414
	git_vector_free_deep(&status->paired);
415

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

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

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

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

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

446
	git_status_list_free(status);
Russell Belfer committed
447

448
	return error;
449 450
}

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

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

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

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

472
	strcomp = (sfi->wildmatch_flags & WM_CASEFOLD) ? git__strcasecmp : git__strcmp;
473

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

482
	return 0;
483 484
}

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

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

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

500
	if ((sfi.expected = git__strdup(path)) == NULL)
501
		return -1;
502
	if (index->ignore_case)
503
		sfi.wildmatch_flags = WM_CASEFOLD;
504

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

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

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

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

529
	*status_flags = sfi.status;
530

531
	git__free(sfi.expected);
532

533
	return error;
534
}
535

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

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

551 552 553 554 555
int git_status_init_options(git_status_options *opts, unsigned int version)
{
	return git_status_options_init(opts, version);
}

556 557 558
int git_status_list_get_perfdata(
	git_diff_perfdata *out, const git_status_list *status)
{
559
	assert(out);
560
	GIT_ERROR_CHECK_VERSION(out, GIT_DIFF_PERFDATA_VERSION, "git_diff_perfdata");
561 562 563 564 565 566 567 568 569 570 571 572 573 574

	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;
575
}
576