status.c 12.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 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 22
#include "git2/diff.h"
#include "diff.h"

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

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

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

	return st;
}

54
static unsigned int workdir_delta2status(
55
	git_diff *diff, git_diff_delta *idx2wd)
56
{
57
	git_status_t st = GIT_STATUS_CURRENT;
58

59
	switch (idx2wd->status) {
60
	case GIT_DELTA_ADDED:
61
	case GIT_DELTA_COPIED:
62 63 64 65 66 67 68 69 70 71 72 73
	case GIT_DELTA_UNTRACKED:
		st = GIT_STATUS_WT_NEW;
		break;
	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;
74 75
	case GIT_DELTA_RENAMED:
		st = GIT_STATUS_WT_RENAMED;
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97

		if (!git_oid_equal(&idx2wd->old_file.oid, &idx2wd->new_file.oid)) {
			/* if OIDs don't match, we might need to calculate them now to
			 * discern between RENAMED vs RENAMED+MODIFED
			 */
			if (git_oid_iszero(&idx2wd->old_file.oid) &&
				diff->old_src == GIT_ITERATOR_TYPE_WORKDIR &&
				!git_diff__oid_for_file(
					diff->repo, idx2wd->old_file.path, idx2wd->old_file.mode,
					idx2wd->old_file.size, &idx2wd->old_file.oid))
			idx2wd->old_file.flags |= GIT_DIFF_FLAG_VALID_OID;

			if (git_oid_iszero(&idx2wd->new_file.oid) &&
				diff->new_src == GIT_ITERATOR_TYPE_WORKDIR &&
				!git_diff__oid_for_file(
					diff->repo, idx2wd->new_file.path, idx2wd->new_file.mode,
					idx2wd->new_file.size, &idx2wd->new_file.oid))
				idx2wd->new_file.flags |= GIT_DIFF_FLAG_VALID_OID;

			if (!git_oid_equal(&idx2wd->old_file.oid, &idx2wd->new_file.oid))
				st |= GIT_STATUS_WT_MODIFIED;
		}
98
		break;
99 100 101
	case GIT_DELTA_TYPECHANGE:
		st = GIT_STATUS_WT_TYPECHANGE;
		break;
102 103 104 105 106 107 108
	default:
		break;
	}

	return st;
}

109
static bool status_is_included(
110
	git_status_list *status,
111 112
	git_diff_delta *head2idx,
	git_diff_delta *idx2wd)
113
{
114 115 116
	if (!(status->opts.flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES))
		return 1;

117
	/* if excluding submodules and this is a submodule everywhere */
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
	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;
133 134
	}

135 136
	/* only get here if every valid mode is GIT_FILEMODE_COMMIT */
	return 0;
137 138
}

139
static git_status_t status_compute(
140
	git_status_list *status,
141 142 143
	git_diff_delta *head2idx,
	git_diff_delta *idx2wd)
{
144
	git_status_t st = GIT_STATUS_CURRENT;
145 146

	if (head2idx)
147
		st |= index_delta2status(head2idx);
148 149

	if (idx2wd)
150
		st |= workdir_delta2status(status->idx2wd, idx2wd);
151

152
	return st;
153 154 155 156 157
}

static int status_collect(
	git_diff_delta *head2idx,
	git_diff_delta *idx2wd,
158
	void *payload)
159
{
160
	git_status_list *status = payload;
161
	git_status_entry *status_entry;
162 163

	if (!status_is_included(status, head2idx, idx2wd))
164
		return 0;
165

166 167 168
	status_entry = git__malloc(sizeof(git_status_entry));
	GITERR_CHECK_ALLOC(status_entry);

169
	status_entry->status = status_compute(status, head2idx, idx2wd);
170 171 172
	status_entry->head_to_index = head2idx;
	status_entry->index_to_workdir = idx2wd;

173
	return git_vector_insert(&status->paired, status_entry);
174 175
}

176 177 178 179 180 181 182 183 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
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)
211
{
212
	git_status_list *status = NULL;
213 214
	int (*entrycmp)(const void *a, const void *b);

215 216 217
	if (!(status = git__calloc(1, sizeof(git_status_list))))
		return NULL;

218
	entrycmp = index->ignore_case ? status_entry_icmp : status_entry_cmp;
219

220 221
	if (git_vector_init(&status->paired, 0, entrycmp) < 0) {
		git__free(status);
222
		return NULL;
223
	}
224

225
	return status;
226 227 228 229 230 231 232
}

int git_status_list_new(
	git_status_list **out,
	git_repository *repo,
	const git_status_options *opts)
{
233
	git_index *index = NULL;
234
	git_status_list *status = NULL;
235
	git_diff_options diffopt = GIT_DIFF_OPTIONS_INIT;
236
	git_diff_find_options findopt = GIT_DIFF_FIND_OPTIONS_INIT;
237 238 239
	git_tree *head = NULL;
	git_status_show_t show =
		opts ? opts->show : GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
240
	int error = 0;
241
	unsigned int flags = opts ? opts->flags : GIT_STATUS_OPT_DEFAULTS;
242

243
	assert(show <= GIT_STATUS_SHOW_WORKDIR_ONLY);
244

245 246
	*out = NULL;

Ben Straub committed
247
	GITERR_CHECK_VERSION(opts, GIT_STATUS_OPTIONS_VERSION, "git_status_options");
248

249 250
	if ((error = git_repository__ensure_not_bare(repo, "status")) < 0 ||
		(error = git_repository_index(&index, repo)) < 0)
251
		return error;
252

253
	/* if there is no HEAD, that's okay - we'll make an empty iterator */
254 255 256 257
	if ((error = git_repository_head_tree(&head, repo)) < 0) {
		if (error != GIT_ENOTFOUND && error != GIT_EUNBORNBRANCH)
			goto done;
		giterr_clear();
258
	}
259

260 261
	/* refresh index from disk unless prevented */
	if ((flags & GIT_STATUS_OPT_NO_REFRESH) == 0 &&
262
		git_index_read(index, false) < 0)
263 264
		giterr_clear();

265 266
	status = git_status_list_alloc(index);
	GITERR_CHECK_ALLOC(status);
267

268 269 270 271
	if (opts) {
		memcpy(&status->opts, opts, sizeof(git_status_options));
		memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec));
	}
272

273
	diffopt.flags = GIT_DIFF_INCLUDE_TYPECHANGE;
274
	findopt.flags = GIT_DIFF_FIND_FOR_UNTRACKED;
275

276
	if ((flags & GIT_STATUS_OPT_INCLUDE_UNTRACKED) != 0)
Russell Belfer committed
277
		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED;
278
	if ((flags & GIT_STATUS_OPT_INCLUDE_IGNORED) != 0)
Russell Belfer committed
279
		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_IGNORED;
280
	if ((flags & GIT_STATUS_OPT_INCLUDE_UNMODIFIED) != 0)
Russell Belfer committed
281
		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNMODIFIED;
282
	if ((flags & GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS) != 0)
283
		diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_UNTRACKED_DIRS;
284
	if ((flags & GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH) != 0)
285
		diffopt.flags = diffopt.flags | GIT_DIFF_DISABLE_PATHSPEC_MATCH;
286
	if ((flags & GIT_STATUS_OPT_RECURSE_IGNORED_DIRS) != 0)
287
		diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_IGNORED_DIRS;
288
	if ((flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0)
289
		diffopt.flags = diffopt.flags | GIT_DIFF_IGNORE_SUBMODULES;
290

291
	if ((flags & GIT_STATUS_OPT_RENAMES_FROM_REWRITES) != 0)
292 293 294 295
		findopt.flags = findopt.flags |
			GIT_DIFF_FIND_AND_BREAK_REWRITES |
			GIT_DIFF_FIND_RENAMES_FROM_REWRITES |
			GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY;
296

297
	if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) {
298
		if ((error = git_diff_tree_to_index(
299
				&status->head2idx, repo, head, index, &diffopt)) < 0)
300
			goto done;
301

302
		if ((flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0 &&
303
			(error = git_diff_find_similar(status->head2idx, &findopt)) < 0)
304
			goto done;
305
	}
306

307
	if (show != GIT_STATUS_SHOW_INDEX_ONLY) {
308
		if ((error = git_diff_index_to_workdir(
309
				&status->idx2wd, repo, index, &diffopt)) < 0)
310
			goto done;
311

312
		if ((flags & GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) != 0 &&
313
			(error = git_diff_find_similar(status->idx2wd, &findopt)) < 0)
314
			goto done;
315
	}
316

317
	if ((error = git_diff__paired_foreach(
318
			status->head2idx, status->idx2wd, status_collect, status)) < 0)
319
		goto done;
320

321 322 323 324 325 326 327 328 329 330
	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)
331
		git_vector_sort(&status->paired);
332

333 334 335 336 337
done:
	if (error < 0) {
		git_status_list_free(status);
		status = NULL;
	}
338

339
	*out = status;
340

341
	git_tree_free(head);
342
	git_index_free(index);
343

344 345 346
	return error;
}

347
size_t git_status_list_entrycount(git_status_list *status)
348
{
349
	assert(status);
350

351
	return status->paired.length;
352 353
}

354
const git_status_entry *git_status_byindex(git_status_list *status, size_t i)
355
{
356
	assert(status);
357

358
	return git_vector_get(&status->paired, i);
359 360
}

361
void git_status_list_free(git_status_list *status)
362 363 364 365
{
	git_status_entry *status_entry;
	size_t i;

366
	if (status == NULL)
367 368
		return;

369 370
	git_diff_free(status->head2idx);
	git_diff_free(status->idx2wd);
371

372
	git_vector_foreach(&status->paired, i, status_entry)
373 374
		git__free(status_entry);

375
	git_vector_free(&status->paired);
376

377 378
	git__memzero(status, sizeof(*status));
	git__free(status);
379 380 381 382 383 384 385 386
}

int git_status_foreach_ext(
	git_repository *repo,
	const git_status_options *opts,
	git_status_cb cb,
	void *payload)
{
387
	git_status_list *status;
388 389 390 391
	const git_status_entry *status_entry;
	size_t i;
	int error = 0;

392
	if ((error = git_status_list_new(&status, repo, opts)) < 0)
393 394
		return error;

395
	git_vector_foreach(&status->paired, i, status_entry) {
396 397 398 399 400 401 402 403 404 405 406
		const char *path = status_entry->head_to_index ?
			status_entry->head_to_index->old_file.path :
			status_entry->index_to_workdir->old_file.path;

		if (cb(path, status_entry->status, payload) != 0) {
			error = GIT_EUSER;
			giterr_clear();
			break;
		}
	}

407
	git_status_list_free(status);
Russell Belfer committed
408

409
	return error;
410 411
}

412
int git_status_foreach(git_repository *repo, git_status_cb cb, void *payload)
413
{
414
	return git_status_foreach_ext(repo, NULL, cb, payload);
415 416
}

417
struct status_file_info {
418
	char *expected;
419 420
	unsigned int count;
	unsigned int status;
421
	int fnm_flags;
422
	int ambiguous;
423 424
};

425
static int get_one_status(const char *path, unsigned int status, void *data)
426
{
427
	struct status_file_info *sfi = data;
428
	int (*strcomp)(const char *a, const char *b);
429

430 431
	sfi->count++;
	sfi->status = status;
432

433 434
	strcomp = (sfi->fnm_flags & FNM_CASEFOLD) ? git__strcasecmp : git__strcmp;

435
	if (sfi->count > 1 ||
436 437
		(strcomp(sfi->expected, path) != 0 &&
		 p_fnmatch(sfi->expected, path, sfi->fnm_flags) != 0))
438
	{
439
		sfi->ambiguous = true;
440
		return GIT_EAMBIGUOUS; /* giterr_set will be done by caller */
441
	}
442

443
	return 0;
444 445
}

446
int git_status_file(
447 448 449
	unsigned int *status_flags,
	git_repository *repo,
	const char *path)
450
{
451
	int error;
452 453
	git_status_options opts = GIT_STATUS_OPTIONS_INIT;
	struct status_file_info sfi = {0};
454
	git_index *index;
455

456 457
	assert(status_flags && repo && path);

458 459 460
	if ((error = git_repository_index__weakptr(&index, repo)) < 0)
		return error;

461
	if ((sfi.expected = git__strdup(path)) == NULL)
462
		return -1;
463 464
	if (index->ignore_case)
		sfi.fnm_flags = FNM_CASEFOLD;
465

466
	opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
467
	opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED |
468
		GIT_STATUS_OPT_RECURSE_IGNORED_DIRS |
469 470 471 472 473
		GIT_STATUS_OPT_INCLUDE_UNTRACKED |
		GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS |
		GIT_STATUS_OPT_INCLUDE_UNMODIFIED;
	opts.pathspec.count = 1;
	opts.pathspec.strings = &sfi.expected;
474

475
	error = git_status_foreach_ext(repo, &opts, get_one_status, &sfi);
476

477 478 479
	if (error < 0 && sfi.ambiguous) {
		giterr_set(GITERR_INVALID,
			"Ambiguous path '%s' given to git_status_file", sfi.expected);
480
		error = GIT_EAMBIGUOUS;
481
	}
482

483
	if (!error && !sfi.count) {
484 485 486
		giterr_set(GITERR_INVALID,
			"Attempt to get status of nonexistent file '%s'", path);
		error = GIT_ENOTFOUND;
487 488
	}

489
	*status_flags = sfi.status;
490

491
	git__free(sfi.expected);
492

493
	return error;
494
}
495

496
int git_status_should_ignore(
497 498 499
	int *ignored,
	git_repository *repo,
	const char *path)
Russell Belfer committed
500
{
501
	return git_ignore_path_is_ignored(ignored, repo, path);
Russell Belfer committed
502 503
}