]> git.proxmox.com Git - libgit2.git/blob - src/status.c
ef32a0a8ea1202fd79d6a8a91837e9588914280b
[libgit2.git] / src / status.c
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 "fileops.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
20 #include "git2/diff.h"
21 #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 case GIT_DELTA_ADDED:
30 case GIT_DELTA_COPIED:
31 st = GIT_STATUS_INDEX_NEW;
32 break;
33 case GIT_DELTA_DELETED:
34 st = GIT_STATUS_INDEX_DELETED;
35 break;
36 case GIT_DELTA_MODIFIED:
37 st = GIT_STATUS_INDEX_MODIFIED;
38 break;
39 case GIT_DELTA_RENAMED:
40 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 break;
45 case GIT_DELTA_TYPECHANGE:
46 st = GIT_STATUS_INDEX_TYPECHANGE;
47 break;
48 case GIT_DELTA_CONFLICTED:
49 st = GIT_STATUS_CONFLICTED;
50 break;
51 default:
52 break;
53 }
54
55 return st;
56 }
57
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 case GIT_DELTA_UNTRACKED:
67 st = GIT_STATUS_WT_NEW;
68 break;
69 case GIT_DELTA_UNREADABLE:
70 st = GIT_STATUS_WT_UNREADABLE;
71 break;
72 case GIT_DELTA_DELETED:
73 st = GIT_STATUS_WT_DELETED;
74 break;
75 case GIT_DELTA_MODIFIED:
76 st = GIT_STATUS_WT_MODIFIED;
77 break;
78 case GIT_DELTA_IGNORED:
79 st = GIT_STATUS_IGNORED;
80 break;
81 case GIT_DELTA_RENAMED:
82 st = GIT_STATUS_WT_RENAMED;
83
84 if (!git_oid_equal(&idx2wd->old_file.id, &idx2wd->new_file.id)) {
85 /* if OIDs don't match, we might need to calculate them now to
86 * discern between RENAMED vs RENAMED+MODIFED
87 */
88 if (git_oid_iszero(&idx2wd->old_file.id) &&
89 diff->old_src == GIT_ITERATOR_TYPE_WORKDIR &&
90 !git_diff__oid_for_file(
91 &idx2wd->old_file.id, diff, idx2wd->old_file.path,
92 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 diff->new_src == GIT_ITERATOR_TYPE_WORKDIR &&
97 !git_diff__oid_for_file(
98 &idx2wd->new_file.id, diff, idx2wd->new_file.path,
99 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 st |= GIT_STATUS_WT_MODIFIED;
104 }
105 break;
106 case GIT_DELTA_TYPECHANGE:
107 st = GIT_STATUS_WT_TYPECHANGE;
108 break;
109 case GIT_DELTA_CONFLICTED:
110 st = GIT_STATUS_CONFLICTED;
111 break;
112 default:
113 break;
114 }
115
116 return st;
117 }
118
119 static bool status_is_included(
120 git_status_list *status,
121 git_diff_delta *head2idx,
122 git_diff_delta *idx2wd)
123 {
124 if (!(status->opts.flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES))
125 return 1;
126
127 /* if excluding submodules and this is a submodule everywhere */
128 if (head2idx) {
129 if (head2idx->status != GIT_DELTA_ADDED &&
130 head2idx->old_file.mode != GIT_FILEMODE_COMMIT)
131 return 1;
132 if (head2idx->status != GIT_DELTA_DELETED &&
133 head2idx->new_file.mode != GIT_FILEMODE_COMMIT)
134 return 1;
135 }
136 if (idx2wd) {
137 if (idx2wd->status != GIT_DELTA_ADDED &&
138 idx2wd->old_file.mode != GIT_FILEMODE_COMMIT)
139 return 1;
140 if (idx2wd->status != GIT_DELTA_DELETED &&
141 idx2wd->new_file.mode != GIT_FILEMODE_COMMIT)
142 return 1;
143 }
144
145 /* only get here if every valid mode is GIT_FILEMODE_COMMIT */
146 return 0;
147 }
148
149 static git_status_t status_compute(
150 git_status_list *status,
151 git_diff_delta *head2idx,
152 git_diff_delta *idx2wd)
153 {
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 static int status_collect(
166 git_diff_delta *head2idx,
167 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 GIT_ERROR_CHECK_ALLOC(status_entry);
178
179 status_entry->status = status_compute(status, head2idx, idx2wd);
180 status_entry->head_to_index = head2idx;
181 status_entry->index_to_workdir = idx2wd;
182
183 return git_vector_insert(&status->paired, status_entry);
184 }
185
186 GIT_INLINE(int) status_entry_cmp_base(
187 const void *a,
188 const void *b,
189 int (*strcomp)(const char *a, const char *b))
190 {
191 const git_status_entry *entry_a = a;
192 const git_status_entry *entry_b = b;
193 const git_diff_delta *delta_a, *delta_b;
194
195 delta_a = entry_a->index_to_workdir ? entry_a->index_to_workdir :
196 entry_a->head_to_index;
197 delta_b = entry_b->index_to_workdir ? entry_b->index_to_workdir :
198 entry_b->head_to_index;
199
200 if (!delta_a && delta_b)
201 return -1;
202 if (delta_a && !delta_b)
203 return 1;
204 if (!delta_a && !delta_b)
205 return 0;
206
207 return strcomp(delta_a->new_file.path, delta_b->new_file.path);
208 }
209
210 static int status_entry_icmp(const void *a, const void *b)
211 {
212 return status_entry_cmp_base(a, b, git__strcasecmp);
213 }
214
215 static int status_entry_cmp(const void *a, const void *b)
216 {
217 return status_entry_cmp_base(a, b, git__strcmp);
218 }
219
220 static git_status_list *git_status_list_alloc(git_index *index)
221 {
222 git_status_list *status = NULL;
223 int (*entrycmp)(const void *a, const void *b);
224
225 if (!(status = git__calloc(1, sizeof(git_status_list))))
226 return NULL;
227
228 entrycmp = index->ignore_case ? status_entry_icmp : status_entry_cmp;
229
230 if (git_vector_init(&status->paired, 0, entrycmp) < 0) {
231 git__free(status);
232 return NULL;
233 }
234
235 return status;
236 }
237
238 static int status_validate_options(const git_status_options *opts)
239 {
240 if (!opts)
241 return 0;
242
243 GIT_ERROR_CHECK_VERSION(opts, GIT_STATUS_OPTIONS_VERSION, "git_status_options");
244
245 if (opts->show > GIT_STATUS_SHOW_WORKDIR_ONLY) {
246 git_error_set(GIT_ERROR_INVALID, "unknown status 'show' option");
247 return -1;
248 }
249
250 if ((opts->flags & GIT_STATUS_OPT_NO_REFRESH) != 0 &&
251 (opts->flags & GIT_STATUS_OPT_UPDATE_INDEX) != 0) {
252 git_error_set(GIT_ERROR_INVALID, "updating index from status "
253 "is not allowed when index refresh is disabled");
254 return -1;
255 }
256
257 return 0;
258 }
259
260 int git_status_list_new(
261 git_status_list **out,
262 git_repository *repo,
263 const git_status_options *opts)
264 {
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 git_tree *head = NULL;
270 git_status_show_t show =
271 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 *out = NULL;
276
277 if (status_validate_options(opts) < 0)
278 return -1;
279
280 if ((error = git_repository__ensure_not_bare(repo, "status")) < 0 ||
281 (error = git_repository_index(&index, repo)) < 0)
282 return error;
283
284 if (opts != NULL && opts->baseline != NULL) {
285 head = opts->baseline;
286 } else {
287 /* if there is no HEAD, that's okay - we'll make an empty iterator */
288 if ((error = git_repository_head_tree(&head, repo)) < 0) {
289 if (error != GIT_ENOTFOUND && error != GIT_EUNBORNBRANCH)
290 goto done;
291 git_error_clear();
292 }
293 }
294
295 /* refresh index from disk unless prevented */
296 if ((flags & GIT_STATUS_OPT_NO_REFRESH) == 0 &&
297 git_index_read_safely(index) < 0)
298 git_error_clear();
299
300 status = git_status_list_alloc(index);
301 GIT_ERROR_CHECK_ALLOC(status);
302
303 if (opts) {
304 memcpy(&status->opts, opts, sizeof(git_status_options));
305 memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec));
306 }
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)
312 diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED;
313 if ((flags & GIT_STATUS_OPT_INCLUDE_IGNORED) != 0)
314 diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_IGNORED;
315 if ((flags & GIT_STATUS_OPT_INCLUDE_UNMODIFIED) != 0)
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 if ((flags & GIT_STATUS_OPT_UPDATE_INDEX) != 0)
326 diffopt.flags = diffopt.flags | GIT_DIFF_UPDATE_INDEX;
327 if ((flags & GIT_STATUS_OPT_INCLUDE_UNREADABLE) != 0)
328 diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNREADABLE;
329 if ((flags & GIT_STATUS_OPT_INCLUDE_UNREADABLE_AS_UNTRACKED) != 0)
330 diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNREADABLE_AS_UNTRACKED;
331
332 if ((flags & GIT_STATUS_OPT_RENAMES_FROM_REWRITES) != 0)
333 findopt.flags = findopt.flags |
334 GIT_DIFF_FIND_AND_BREAK_REWRITES |
335 GIT_DIFF_FIND_RENAMES_FROM_REWRITES |
336 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 error = git_diff__paired_foreach(
360 status->head2idx, status->idx2wd, status_collect, status);
361 if (error < 0)
362 goto done;
363
364 if (flags & GIT_STATUS_OPT_SORT_CASE_SENSITIVELY)
365 git_vector_set_cmp(&status->paired, status_entry_cmp);
366 if (flags & GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY)
367 git_vector_set_cmp(&status->paired, status_entry_icmp);
368
369 if ((flags &
370 (GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX |
371 GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR |
372 GIT_STATUS_OPT_SORT_CASE_SENSITIVELY |
373 GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY)) != 0)
374 git_vector_sort(&status->paired);
375
376 done:
377 if (error < 0) {
378 git_status_list_free(status);
379 status = NULL;
380 }
381
382 *out = status;
383
384 if (opts == NULL || opts->baseline != head)
385 git_tree_free(head);
386 git_index_free(index);
387
388 return error;
389 }
390
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 return;
409
410 git_diff_free(status->head2idx);
411 git_diff_free(status->idx2wd);
412
413 git_vector_free_deep(&status->paired);
414
415 git__memzero(status, sizeof(*status));
416 git__free(status);
417 }
418
419 int git_status_foreach_ext(
420 git_repository *repo,
421 const git_status_options *opts,
422 git_status_cb cb,
423 void *payload)
424 {
425 git_status_list *status;
426 const git_status_entry *status_entry;
427 size_t i;
428 int error = 0;
429
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 const char *path = status_entry->head_to_index ?
436 status_entry->head_to_index->old_file.path :
437 status_entry->index_to_workdir->old_file.path;
438
439 if ((error = cb(path, status_entry->status, payload)) != 0) {
440 git_error_set_after_callback(error);
441 break;
442 }
443 }
444
445 git_status_list_free(status);
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 unsigned int count;
458 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 sfi->count++;
469 sfi->status = status;
470
471 strcomp = (sfi->fnm_flags & FNM_CASEFOLD) ? git__strcasecmp : git__strcmp;
472
473 if (sfi->count > 1 ||
474 (strcomp(sfi->expected, path) != 0 &&
475 p_fnmatch(sfi->expected, path, sfi->fnm_flags) != 0))
476 {
477 sfi->ambiguous = true;
478 return GIT_EAMBIGUOUS; /* git_error_set will be done by caller */
479 }
480
481 return 0;
482 }
483
484 int git_status_file(
485 unsigned int *status_flags,
486 git_repository *repo,
487 const char *path)
488 {
489 int error;
490 git_status_options opts = GIT_STATUS_OPTIONS_INIT;
491 struct status_file_info sfi = {0};
492 git_index *index;
493
494 assert(status_flags && repo && path);
495
496 if ((error = git_repository_index__weakptr(&index, repo)) < 0)
497 return error;
498
499 if ((sfi.expected = git__strdup(path)) == NULL)
500 return -1;
501 if (index->ignore_case)
502 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 GIT_STATUS_OPT_INCLUDE_UNTRACKED |
508 GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS |
509 GIT_STATUS_OPT_INCLUDE_UNMODIFIED |
510 GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
511 opts.pathspec.count = 1;
512 opts.pathspec.strings = &sfi.expected;
513
514 error = git_status_foreach_ext(repo, &opts, get_one_status, &sfi);
515
516 if (error < 0 && sfi.ambiguous) {
517 git_error_set(GIT_ERROR_INVALID,
518 "ambiguous path '%s' given to git_status_file", sfi.expected);
519 error = GIT_EAMBIGUOUS;
520 }
521
522 if (!error && !sfi.count) {
523 git_error_set(GIT_ERROR_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 int *ignored,
537 git_repository *repo,
538 const char *path)
539 {
540 return git_ignore_path_is_ignored(ignored, repo, path);
541 }
542
543 int git_status_init_options(git_status_options *opts, unsigned int version)
544 {
545 GIT_INIT_STRUCTURE_FROM_TEMPLATE(
546 opts, version, git_status_options, GIT_STATUS_OPTIONS_INIT);
547 return 0;
548 }
549
550 int git_status_list_get_perfdata(
551 git_diff_perfdata *out, const git_status_list *status)
552 {
553 assert(out);
554 GIT_ERROR_CHECK_VERSION(out, GIT_DIFF_PERFDATA_VERSION, "git_diff_perfdata");
555
556 out->stat_calls = 0;
557 out->oid_calculations = 0;
558
559 if (status->head2idx) {
560 out->stat_calls += status->head2idx->perf.stat_calls;
561 out->oid_calculations += status->head2idx->perf.oid_calculations;
562 }
563 if (status->idx2wd) {
564 out->stat_calls += status->idx2wd->perf.stat_calls;
565 out->oid_calculations += status->idx2wd->perf.oid_calculations;
566 }
567
568 return 0;
569 }
570