status.c 14.2 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 10 11
 */

#include "common.h"
#include "git2.h"
#include "fileops.h"
#include "hash.h"
12 13
#include "vector.h"
#include "tree.h"
14
#include "status.h"
15
#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
	/* if there is no HEAD, that's okay - we'll make an empty iterator */
285 286 287 288
	if ((error = git_repository_head_tree(&head, repo)) < 0) {
		if (error != GIT_ENOTFOUND && error != GIT_EUNBORNBRANCH)
			goto done;
		giterr_clear();
289
	}
290

291 292
	/* refresh index from disk unless prevented */
	if ((flags & GIT_STATUS_OPT_NO_REFRESH) == 0 &&
293
		git_index_read(index, false) < 0)
294 295
		giterr_clear();

296 297
	status = git_status_list_alloc(index);
	GITERR_CHECK_ALLOC(status);
298

299 300 301 302
	if (opts) {
		memcpy(&status->opts, opts, sizeof(git_status_options));
		memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec));
	}
303

304
	diffopt.flags = GIT_DIFF_INCLUDE_TYPECHANGE;
305
	findopt.flags = GIT_DIFF_FIND_FOR_UNTRACKED;
306

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

328
	if ((flags & GIT_STATUS_OPT_RENAMES_FROM_REWRITES) != 0)
329 330 331 332
		findopt.flags = findopt.flags |
			GIT_DIFF_FIND_AND_BREAK_REWRITES |
			GIT_DIFF_FIND_RENAMES_FROM_REWRITES |
			GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY;
333

334
	if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) {
335
		if ((error = git_diff_tree_to_index(
336
				&status->head2idx, repo, head, index, &diffopt)) < 0)
337
			goto done;
338

339
		if ((flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0 &&
340
			(error = git_diff_find_similar(status->head2idx, &findopt)) < 0)
341
			goto done;
342
	}
343

344
	if (show != GIT_STATUS_SHOW_INDEX_ONLY) {
345
		if ((error = git_diff_index_to_workdir(
346
				&status->idx2wd, repo, index, &diffopt)) < 0) {
347
			goto done;
348
		}
349

350
		if ((flags & GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) != 0 &&
351
			(error = git_diff_find_similar(status->idx2wd, &findopt)) < 0)
352
			goto done;
353
	}
354

355 356 357 358
	error = git_diff__paired_foreach(
		status->head2idx, status->idx2wd, status_collect, status);
	if (error < 0)
		goto done;
359

360 361 362 363 364 365 366 367 368 369
	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)
370
		git_vector_sort(&status->paired);
371

372 373 374 375 376
done:
	if (error < 0) {
		git_status_list_free(status);
		status = NULL;
	}
377

378
	*out = status;
379

380
	git_tree_free(head);
381
	git_index_free(index);
382

383 384 385
	return error;
}

386
size_t git_status_list_entrycount(git_status_list *status)
387
{
388
	assert(status);
389

390
	return status->paired.length;
391 392
}

393
const git_status_entry *git_status_byindex(git_status_list *status, size_t i)
394
{
395
	assert(status);
396

397
	return git_vector_get(&status->paired, i);
398 399
}

400
void git_status_list_free(git_status_list *status)
401
{
402
	if (status == NULL)
403 404
		return;

405 406
	git_diff_free(status->head2idx);
	git_diff_free(status->idx2wd);
407

408
	git_vector_free_deep(&status->paired);
409

410 411
	git__memzero(status, sizeof(*status));
	git__free(status);
412 413 414 415 416 417 418 419
}

int git_status_foreach_ext(
	git_repository *repo,
	const git_status_options *opts,
	git_status_cb cb,
	void *payload)
{
420
	git_status_list *status;
421 422 423 424
	const git_status_entry *status_entry;
	size_t i;
	int error = 0;

425
	if ((error = git_status_list_new(&status, repo, opts)) < 0) {
426
		return error;
427
	}
428

429
	git_vector_foreach(&status->paired, i, status_entry) {
430 431 432 433
		const char *path = status_entry->head_to_index ?
			status_entry->head_to_index->old_file.path :
			status_entry->index_to_workdir->old_file.path;

434
		if ((error = cb(path, status_entry->status, payload)) != 0) {
435
			giterr_set_after_callback(error);
436
			break;
437
		}
438 439
	}

440
	git_status_list_free(status);
Russell Belfer committed
441

442
	return error;
443 444
}

445
int git_status_foreach(git_repository *repo, git_status_cb cb, void *payload)
446
{
447
	return git_status_foreach_ext(repo, NULL, cb, payload);
448 449
}

450
struct status_file_info {
451
	char *expected;
452 453
	unsigned int count;
	unsigned int status;
454
	int fnm_flags;
455
	int ambiguous;
456 457
};

458
static int get_one_status(const char *path, unsigned int status, void *data)
459
{
460
	struct status_file_info *sfi = data;
461
	int (*strcomp)(const char *a, const char *b);
462

463 464
	sfi->count++;
	sfi->status = status;
465

466 467
	strcomp = (sfi->fnm_flags & FNM_CASEFOLD) ? git__strcasecmp : git__strcmp;

468
	if (sfi->count > 1 ||
469 470
		(strcomp(sfi->expected, path) != 0 &&
		 p_fnmatch(sfi->expected, path, sfi->fnm_flags) != 0))
471
	{
472
		sfi->ambiguous = true;
473
		return GIT_EAMBIGUOUS; /* giterr_set will be done by caller */
474
	}
475

476
	return 0;
477 478
}

479
int git_status_file(
480 481 482
	unsigned int *status_flags,
	git_repository *repo,
	const char *path)
483
{
484
	int error;
485 486
	git_status_options opts = GIT_STATUS_OPTIONS_INIT;
	struct status_file_info sfi = {0};
487
	git_index *index;
488

489 490
	assert(status_flags && repo && path);

491 492 493
	if ((error = git_repository_index__weakptr(&index, repo)) < 0)
		return error;

494
	if ((sfi.expected = git__strdup(path)) == NULL)
495
		return -1;
496 497
	if (index->ignore_case)
		sfi.fnm_flags = FNM_CASEFOLD;
498

499
	opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
500
	opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED |
501
		GIT_STATUS_OPT_RECURSE_IGNORED_DIRS |
502 503
		GIT_STATUS_OPT_INCLUDE_UNTRACKED |
		GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS |
504 505
		GIT_STATUS_OPT_INCLUDE_UNMODIFIED |
		GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
506 507
	opts.pathspec.count = 1;
	opts.pathspec.strings = &sfi.expected;
508

509
	error = git_status_foreach_ext(repo, &opts, get_one_status, &sfi);
510

511 512
	if (error < 0 && sfi.ambiguous) {
		giterr_set(GITERR_INVALID,
513
			"ambiguous path '%s' given to git_status_file", sfi.expected);
514
		error = GIT_EAMBIGUOUS;
515
	}
516

517
	if (!error && !sfi.count) {
518
		giterr_set(GITERR_INVALID,
519
			"attempt to get status of nonexistent file '%s'", path);
520
		error = GIT_ENOTFOUND;
521 522
	}

523
	*status_flags = sfi.status;
524

525
	git__free(sfi.expected);
526

527
	return error;
528
}
529

530
int git_status_should_ignore(
531 532 533
	int *ignored,
	git_repository *repo,
	const char *path)
Russell Belfer committed
534
{
535
	return git_ignore_path_is_ignored(ignored, repo, path);
Russell Belfer committed
536 537
}

538
int git_status_init_options(git_status_options *opts, unsigned int version)
539
{
540 541
	GIT_INIT_STRUCTURE_FROM_TEMPLATE(
		opts, version, git_status_options, GIT_STATUS_OPTIONS_INIT);
542 543 544 545 546 547
	return 0;
}

int git_status_list_get_perfdata(
	git_diff_perfdata *out, const git_status_list *status)
{
548 549
	assert(out);
	GITERR_CHECK_VERSION(out, GIT_DIFF_PERFDATA_VERSION, "git_diff_perfdata");
550 551 552 553 554 555 556 557 558 559 560 561 562 563

	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;
564
}
565