1 /*
2  * Copyright (C) the libgit2 contributors. All rights reserved.
3  *
4  * This file is part of libgit2, distributed under the GNU GPL v2 with
5  * a Linking Exception. For full terms see the included COPYING file.
6  */
7 
8 #include "status.h"
9 
10 #include "git2.h"
11 #include "futils.h"
12 #include "hash.h"
13 #include "vector.h"
14 #include "tree.h"
15 #include "git2/status.h"
16 #include "repository.h"
17 #include "ignore.h"
18 #include "index.h"
19 #include "wildmatch.h"
20 
21 #include "git2/diff.h"
22 #include "diff.h"
23 #include "diff_generate.h"
24 
index_delta2status(const git_diff_delta * head2idx)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 	case GIT_DELTA_ADDED:
31 	case GIT_DELTA_COPIED:
32 		st = GIT_STATUS_INDEX_NEW;
33 		break;
34 	case GIT_DELTA_DELETED:
35 		st = GIT_STATUS_INDEX_DELETED;
36 		break;
37 	case GIT_DELTA_MODIFIED:
38 		st = GIT_STATUS_INDEX_MODIFIED;
39 		break;
40 	case GIT_DELTA_RENAMED:
41 		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 		break;
46 	case GIT_DELTA_TYPECHANGE:
47 		st = GIT_STATUS_INDEX_TYPECHANGE;
48 		break;
49 	case GIT_DELTA_CONFLICTED:
50 		st = GIT_STATUS_CONFLICTED;
51 		break;
52 	default:
53 		break;
54 	}
55 
56 	return st;
57 }
58 
workdir_delta2status(git_diff * diff,git_diff_delta * idx2wd)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 	case GIT_DELTA_UNTRACKED:
68 		st = GIT_STATUS_WT_NEW;
69 		break;
70 	case GIT_DELTA_UNREADABLE:
71 		st = GIT_STATUS_WT_UNREADABLE;
72 		break;
73 	case GIT_DELTA_DELETED:
74 		st = GIT_STATUS_WT_DELETED;
75 		break;
76 	case GIT_DELTA_MODIFIED:
77 		st = GIT_STATUS_WT_MODIFIED;
78 		break;
79 	case GIT_DELTA_IGNORED:
80 		st = GIT_STATUS_IGNORED;
81 		break;
82 	case GIT_DELTA_RENAMED:
83 		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
87 			 * discern between RENAMED vs RENAMED+MODIFED
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 					&idx2wd->old_file.id, diff, idx2wd->old_file.path,
93 					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 					&idx2wd->new_file.id, diff, idx2wd->new_file.path,
100 					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 				st |= GIT_STATUS_WT_MODIFIED;
105 		}
106 		break;
107 	case GIT_DELTA_TYPECHANGE:
108 		st = GIT_STATUS_WT_TYPECHANGE;
109 		break;
110 	case GIT_DELTA_CONFLICTED:
111 		st = GIT_STATUS_CONFLICTED;
112 		break;
113 	default:
114 		break;
115 	}
116 
117 	return st;
118 }
119 
status_is_included(git_status_list * status,git_diff_delta * head2idx,git_diff_delta * idx2wd)120 static bool status_is_included(
121 	git_status_list *status,
122 	git_diff_delta *head2idx,
123 	git_diff_delta *idx2wd)
124 {
125 	if (!(status->opts.flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES))
126 		return 1;
127 
128 	/* if excluding submodules and this is a submodule everywhere */
129 	if (head2idx) {
130 		if (head2idx->status != GIT_DELTA_ADDED &&
131 			head2idx->old_file.mode != GIT_FILEMODE_COMMIT)
132 			return 1;
133 		if (head2idx->status != GIT_DELTA_DELETED &&
134 			head2idx->new_file.mode != GIT_FILEMODE_COMMIT)
135 			return 1;
136 	}
137 	if (idx2wd) {
138 		if (idx2wd->status != GIT_DELTA_ADDED &&
139 			idx2wd->old_file.mode != GIT_FILEMODE_COMMIT)
140 			return 1;
141 		if (idx2wd->status != GIT_DELTA_DELETED &&
142 			idx2wd->new_file.mode != GIT_FILEMODE_COMMIT)
143 			return 1;
144 	}
145 
146 	/* only get here if every valid mode is GIT_FILEMODE_COMMIT */
147 	return 0;
148 }
149 
status_compute(git_status_list * status,git_diff_delta * head2idx,git_diff_delta * idx2wd)150 static git_status_t status_compute(
151 	git_status_list *status,
152 	git_diff_delta *head2idx,
153 	git_diff_delta *idx2wd)
154 {
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 
status_collect(git_diff_delta * head2idx,git_diff_delta * idx2wd,void * payload)166 static int status_collect(
167 	git_diff_delta *head2idx,
168 	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 	status_entry->head_to_index = head2idx;
182 	status_entry->index_to_workdir = idx2wd;
183 
184 	return git_vector_insert(&status->paired, status_entry);
185 }
186 
status_entry_cmp_base(const void * a,const void * b,int (* strcomp)(const char * a,const char * b))187 GIT_INLINE(int) status_entry_cmp_base(
188 	const void *a,
189 	const void *b,
190 	int (*strcomp)(const char *a, const char *b))
191 {
192 	const git_status_entry *entry_a = a;
193 	const git_status_entry *entry_b = b;
194 	const git_diff_delta *delta_a, *delta_b;
195 
196 	delta_a = entry_a->index_to_workdir ? entry_a->index_to_workdir :
197 		entry_a->head_to_index;
198 	delta_b = entry_b->index_to_workdir ? entry_b->index_to_workdir :
199 		entry_b->head_to_index;
200 
201 	if (!delta_a && delta_b)
202 		return -1;
203 	if (delta_a && !delta_b)
204 		return 1;
205 	if (!delta_a && !delta_b)
206 		return 0;
207 
208 	return strcomp(delta_a->new_file.path, delta_b->new_file.path);
209 }
210 
status_entry_icmp(const void * a,const void * b)211 static int status_entry_icmp(const void *a, const void *b)
212 {
213 	return status_entry_cmp_base(a, b, git__strcasecmp);
214 }
215 
status_entry_cmp(const void * a,const void * b)216 static int status_entry_cmp(const void *a, const void *b)
217 {
218 	return status_entry_cmp_base(a, b, git__strcmp);
219 }
220 
git_status_list_alloc(git_index * index)221 static git_status_list *git_status_list_alloc(git_index *index)
222 {
223 	git_status_list *status = NULL;
224 	int (*entrycmp)(const void *a, const void *b);
225 
226 	if (!(status = git__calloc(1, sizeof(git_status_list))))
227 		return NULL;
228 
229 	entrycmp = index->ignore_case ? status_entry_icmp : status_entry_cmp;
230 
231 	if (git_vector_init(&status->paired, 0, entrycmp) < 0) {
232 		git__free(status);
233 		return NULL;
234 	}
235 
236 	return status;
237 }
238 
status_validate_options(const git_status_options * opts)239 static int status_validate_options(const git_status_options *opts)
240 {
241 	if (!opts)
242 		return 0;
243 
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 		return -1;
249 	}
250 
251 	if ((opts->flags & GIT_STATUS_OPT_NO_REFRESH) != 0 &&
252 		(opts->flags & GIT_STATUS_OPT_UPDATE_INDEX) != 0) {
253 		git_error_set(GIT_ERROR_INVALID, "updating index from status "
254 			"is not allowed when index refresh is disabled");
255 		return -1;
256 	}
257 
258 	return 0;
259 }
260 
git_status_list_new(git_status_list ** out,git_repository * repo,const git_status_options * opts)261 int git_status_list_new(
262 	git_status_list **out,
263 	git_repository *repo,
264 	const git_status_options *opts)
265 {
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 	git_tree *head = NULL;
271 	git_status_show_t show =
272 		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 	*out = NULL;
277 
278 	if (status_validate_options(opts) < 0)
279 		return -1;
280 
281 	if ((error = git_repository__ensure_not_bare(repo, "status")) < 0 ||
282 		(error = git_repository_index(&index, repo)) < 0)
283 		return error;
284 
285 	if (opts != NULL && opts->baseline != NULL) {
286 		head = opts->baseline;
287 	} else {
288 		/* if there is no HEAD, that's okay - we'll make an empty iterator */
289 		if ((error = git_repository_head_tree(&head, repo)) < 0) {
290 			if (error != GIT_ENOTFOUND && error != GIT_EUNBORNBRANCH)
291 				goto done;
292 			git_error_clear();
293 		}
294 	}
295 
296 	/* refresh index from disk unless prevented */
297 	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 	if (opts) {
305 		memcpy(&status->opts, opts, sizeof(git_status_options));
306 		memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec));
307 	}
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)
313 		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED;
314 	if ((flags & GIT_STATUS_OPT_INCLUDE_IGNORED) != 0)
315 		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_IGNORED;
316 	if ((flags & GIT_STATUS_OPT_INCLUDE_UNMODIFIED) != 0)
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 	if ((flags & GIT_STATUS_OPT_UPDATE_INDEX) != 0)
327 		diffopt.flags = diffopt.flags | GIT_DIFF_UPDATE_INDEX;
328 	if ((flags & GIT_STATUS_OPT_INCLUDE_UNREADABLE) != 0)
329 		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNREADABLE;
330 	if ((flags & GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED) != 0)
331 		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED;
332 
333 	if ((flags & GIT_STATUS_OPT_RENAMES_FROM_REWRITES) != 0)
334 		findopt.flags = findopt.flags |
335 			GIT_DIFF_FIND_AND_BREAK_REWRITES |
336 			GIT_DIFF_FIND_RENAMES_FROM_REWRITES |
337 			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 	error = git_diff__paired_foreach(
361 		status->head2idx, status->idx2wd, status_collect, status);
362 	if (error < 0)
363 		goto done;
364 
365 	if (flags & GIT_STATUS_OPT_SORT_CASE_SENSITIVELY)
366 		git_vector_set_cmp(&status->paired, status_entry_cmp);
367 	if (flags & GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY)
368 		git_vector_set_cmp(&status->paired, status_entry_icmp);
369 
370 	if ((flags &
371 		 (GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX |
372 		  GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR |
373 		  GIT_STATUS_OPT_SORT_CASE_SENSITIVELY |
374 		  GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY)) != 0)
375 		git_vector_sort(&status->paired);
376 
377 done:
378 	if (error < 0) {
379 		git_status_list_free(status);
380 		status = NULL;
381 	}
382 
383 	*out = status;
384 
385 	if (opts == NULL || opts->baseline != head)
386 		git_tree_free(head);
387 	git_index_free(index);
388 
389 	return error;
390 }
391 
git_status_list_entrycount(git_status_list * status)392 size_t git_status_list_entrycount(git_status_list *status)
393 {
394 	GIT_ASSERT_ARG_WITH_RETVAL(status, 0);
395 
396 	return status->paired.length;
397 }
398 
git_status_byindex(git_status_list * status,size_t i)399 const git_status_entry *git_status_byindex(git_status_list *status, size_t i)
400 {
401 	GIT_ASSERT_ARG_WITH_RETVAL(status, NULL);
402 
403 	return git_vector_get(&status->paired, i);
404 }
405 
git_status_list_free(git_status_list * status)406 void git_status_list_free(git_status_list *status)
407 {
408 	if (status == NULL)
409 		return;
410 
411 	git_diff_free(status->head2idx);
412 	git_diff_free(status->idx2wd);
413 
414 	git_vector_free_deep(&status->paired);
415 
416 	git__memzero(status, sizeof(*status));
417 	git__free(status);
418 }
419 
git_status_foreach_ext(git_repository * repo,const git_status_options * opts,git_status_cb cb,void * payload)420 int git_status_foreach_ext(
421 	git_repository *repo,
422 	const git_status_options *opts,
423 	git_status_cb cb,
424 	void *payload)
425 {
426 	git_status_list *status;
427 	const git_status_entry *status_entry;
428 	size_t i;
429 	int error = 0;
430 
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 		const char *path = status_entry->head_to_index ?
437 			status_entry->head_to_index->old_file.path :
438 			status_entry->index_to_workdir->old_file.path;
439 
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);
447 
448 	return error;
449 }
450 
git_status_foreach(git_repository * repo,git_status_cb cb,void * payload)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 	unsigned int count;
459 	unsigned int status;
460 	int wildmatch_flags;
461 	int ambiguous;
462 };
463 
get_one_status(const char * path,unsigned int status,void * data)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 	sfi->count++;
470 	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 
git_status_file(unsigned int * status_flags,git_repository * repo,const char * path)485 int git_status_file(
486 	unsigned int *status_flags,
487 	git_repository *repo,
488 	const char *path)
489 {
490 	int error;
491 	git_status_options opts = GIT_STATUS_OPTIONS_INIT;
492 	struct status_file_info sfi = {0};
493 	git_index *index;
494 
495 	GIT_ASSERT_ARG(status_flags);
496 	GIT_ASSERT_ARG(repo);
497 	GIT_ASSERT_ARG(path);
498 
499 	if ((error = git_repository_index__weakptr(&index, repo)) < 0)
500 		return error;
501 
502 	if ((sfi.expected = git__strdup(path)) == NULL)
503 		return -1;
504 	if (index->ignore_case)
505 		sfi.wildmatch_flags = WM_CASEFOLD;
506 
507 	opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
508 	opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED |
509 		GIT_STATUS_OPT_RECURSE_IGNORED_DIRS |
510 		GIT_STATUS_OPT_INCLUDE_UNTRACKED |
511 		GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS |
512 		GIT_STATUS_OPT_INCLUDE_UNMODIFIED |
513 		GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
514 	opts.pathspec.count = 1;
515 	opts.pathspec.strings = &sfi.expected;
516 
517 	error = git_status_foreach_ext(repo, &opts, get_one_status, &sfi);
518 
519 	if (error < 0 && sfi.ambiguous) {
520 		git_error_set(GIT_ERROR_INVALID,
521 			"ambiguous path '%s' given to git_status_file", sfi.expected);
522 		error = GIT_EAMBIGUOUS;
523 	}
524 
525 	if (!error && !sfi.count) {
526 		git_error_set(GIT_ERROR_INVALID,
527 			"attempt to get status of nonexistent file '%s'", path);
528 		error = GIT_ENOTFOUND;
529 	}
530 
531 	*status_flags = sfi.status;
532 
533 	git__free(sfi.expected);
534 
535 	return error;
536 }
537 
git_status_should_ignore(int * ignored,git_repository * repo,const char * path)538 int git_status_should_ignore(
539 	int *ignored,
540 	git_repository *repo,
541 	const char *path)
542 {
543 	return git_ignore_path_is_ignored(ignored, repo, path);
544 }
545 
git_status_options_init(git_status_options * opts,unsigned int version)546 int git_status_options_init(git_status_options *opts, unsigned int version)
547 {
548 	GIT_INIT_STRUCTURE_FROM_TEMPLATE(
549 		opts, version, git_status_options, GIT_STATUS_OPTIONS_INIT);
550 	return 0;
551 }
552 
553 #ifndef GIT_DEPRECATE_HARD
git_status_init_options(git_status_options * opts,unsigned int version)554 int git_status_init_options(git_status_options *opts, unsigned int version)
555 {
556 	return git_status_options_init(opts, version);
557 }
558 #endif
559 
git_status_list_get_perfdata(git_diff_perfdata * out,const git_status_list * status)560 int git_status_list_get_perfdata(
561 	git_diff_perfdata *out, const git_status_list *status)
562 {
563 	GIT_ASSERT_ARG(out);
564 
565 	GIT_ERROR_CHECK_VERSION(out, GIT_DIFF_PERFDATA_VERSION, "git_diff_perfdata");
566 
567 	out->stat_calls = 0;
568 	out->oid_calculations = 0;
569 
570 	if (status->head2idx) {
571 		out->stat_calls += status->head2idx->perf.stat_calls;
572 		out->oid_calculations += status->head2idx->perf.oid_calculations;
573 	}
574 	if (status->idx2wd) {
575 		out->stat_calls += status->idx2wd->perf.stat_calls;
576 		out->oid_calculations += status->idx2wd->perf.oid_calculations;
577 	}
578 
579 	return 0;
580 }
581 
582