2 * Copyright (C) the libgit2 contributors. All rights reserved.
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.
15 #include "git2/status.h"
16 #include "repository.h"
19 #include "wildmatch.h"
21 #include "git2/diff.h"
23 #include "diff_generate.h"
25 static unsigned int index_delta2status(const git_diff_delta
*head2idx
)
27 git_status_t st
= GIT_STATUS_CURRENT
;
29 switch (head2idx
->status
) {
31 case GIT_DELTA_COPIED
:
32 st
= GIT_STATUS_INDEX_NEW
;
34 case GIT_DELTA_DELETED
:
35 st
= GIT_STATUS_INDEX_DELETED
;
37 case GIT_DELTA_MODIFIED
:
38 st
= GIT_STATUS_INDEX_MODIFIED
;
40 case GIT_DELTA_RENAMED
:
41 st
= GIT_STATUS_INDEX_RENAMED
;
43 if (!git_oid_equal(&head2idx
->old_file
.id
, &head2idx
->new_file
.id
))
44 st
|= GIT_STATUS_INDEX_MODIFIED
;
46 case GIT_DELTA_TYPECHANGE
:
47 st
= GIT_STATUS_INDEX_TYPECHANGE
;
49 case GIT_DELTA_CONFLICTED
:
50 st
= GIT_STATUS_CONFLICTED
;
59 static unsigned int workdir_delta2status(
60 git_diff
*diff
, git_diff_delta
*idx2wd
)
62 git_status_t st
= GIT_STATUS_CURRENT
;
64 switch (idx2wd
->status
) {
66 case GIT_DELTA_COPIED
:
67 case GIT_DELTA_UNTRACKED
:
68 st
= GIT_STATUS_WT_NEW
;
70 case GIT_DELTA_UNREADABLE
:
71 st
= GIT_STATUS_WT_UNREADABLE
;
73 case GIT_DELTA_DELETED
:
74 st
= GIT_STATUS_WT_DELETED
;
76 case GIT_DELTA_MODIFIED
:
77 st
= GIT_STATUS_WT_MODIFIED
;
79 case GIT_DELTA_IGNORED
:
80 st
= GIT_STATUS_IGNORED
;
82 case GIT_DELTA_RENAMED
:
83 st
= GIT_STATUS_WT_RENAMED
;
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
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
;
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
;
103 if (!git_oid_equal(&idx2wd
->old_file
.id
, &idx2wd
->new_file
.id
))
104 st
|= GIT_STATUS_WT_MODIFIED
;
107 case GIT_DELTA_TYPECHANGE
:
108 st
= GIT_STATUS_WT_TYPECHANGE
;
110 case GIT_DELTA_CONFLICTED
:
111 st
= GIT_STATUS_CONFLICTED
;
120 static bool status_is_included(
121 git_status_list
*status
,
122 git_diff_delta
*head2idx
,
123 git_diff_delta
*idx2wd
)
125 if (!(status
->opts
.flags
& GIT_STATUS_OPT_EXCLUDE_SUBMODULES
))
128 /* if excluding submodules and this is a submodule everywhere */
130 if (head2idx
->status
!= GIT_DELTA_ADDED
&&
131 head2idx
->old_file
.mode
!= GIT_FILEMODE_COMMIT
)
133 if (head2idx
->status
!= GIT_DELTA_DELETED
&&
134 head2idx
->new_file
.mode
!= GIT_FILEMODE_COMMIT
)
138 if (idx2wd
->status
!= GIT_DELTA_ADDED
&&
139 idx2wd
->old_file
.mode
!= GIT_FILEMODE_COMMIT
)
141 if (idx2wd
->status
!= GIT_DELTA_DELETED
&&
142 idx2wd
->new_file
.mode
!= GIT_FILEMODE_COMMIT
)
146 /* only get here if every valid mode is GIT_FILEMODE_COMMIT */
150 static git_status_t
status_compute(
151 git_status_list
*status
,
152 git_diff_delta
*head2idx
,
153 git_diff_delta
*idx2wd
)
155 git_status_t st
= GIT_STATUS_CURRENT
;
158 st
|= index_delta2status(head2idx
);
161 st
|= workdir_delta2status(status
->idx2wd
, idx2wd
);
166 static int status_collect(
167 git_diff_delta
*head2idx
,
168 git_diff_delta
*idx2wd
,
171 git_status_list
*status
= payload
;
172 git_status_entry
*status_entry
;
174 if (!status_is_included(status
, head2idx
, idx2wd
))
177 status_entry
= git__malloc(sizeof(git_status_entry
));
178 GIT_ERROR_CHECK_ALLOC(status_entry
);
180 status_entry
->status
= status_compute(status
, head2idx
, idx2wd
);
181 status_entry
->head_to_index
= head2idx
;
182 status_entry
->index_to_workdir
= idx2wd
;
184 return git_vector_insert(&status
->paired
, status_entry
);
187 GIT_INLINE(int) status_entry_cmp_base(
190 int (*strcomp
)(const char *a
, const char *b
))
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
;
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
;
201 if (!delta_a
&& delta_b
)
203 if (delta_a
&& !delta_b
)
205 if (!delta_a
&& !delta_b
)
208 return strcomp(delta_a
->new_file
.path
, delta_b
->new_file
.path
);
211 static int status_entry_icmp(const void *a
, const void *b
)
213 return status_entry_cmp_base(a
, b
, git__strcasecmp
);
216 static int status_entry_cmp(const void *a
, const void *b
)
218 return status_entry_cmp_base(a
, b
, git__strcmp
);
221 static git_status_list
*git_status_list_alloc(git_index
*index
)
223 git_status_list
*status
= NULL
;
224 int (*entrycmp
)(const void *a
, const void *b
);
226 if (!(status
= git__calloc(1, sizeof(git_status_list
))))
229 entrycmp
= index
->ignore_case
? status_entry_icmp
: status_entry_cmp
;
231 if (git_vector_init(&status
->paired
, 0, entrycmp
) < 0) {
239 static int status_validate_options(const git_status_options
*opts
)
244 GIT_ERROR_CHECK_VERSION(opts
, GIT_STATUS_OPTIONS_VERSION
, "git_status_options");
246 if (opts
->show
> GIT_STATUS_SHOW_WORKDIR_ONLY
) {
247 git_error_set(GIT_ERROR_INVALID
, "unknown status 'show' option");
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");
261 int git_status_list_new(
262 git_status_list
**out
,
263 git_repository
*repo
,
264 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 git_tree
*head
= NULL
;
271 git_status_show_t show
=
272 opts
? opts
->show
: GIT_STATUS_SHOW_INDEX_AND_WORKDIR
;
274 unsigned int flags
= opts
? opts
->flags
: GIT_STATUS_OPT_DEFAULTS
;
278 if (status_validate_options(opts
) < 0)
281 if ((error
= git_repository__ensure_not_bare(repo
, "status")) < 0 ||
282 (error
= git_repository_index(&index
, repo
)) < 0)
285 if (opts
!= NULL
&& opts
->baseline
!= NULL
) {
286 head
= opts
->baseline
;
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
)
296 /* refresh index from disk unless prevented */
297 if ((flags
& GIT_STATUS_OPT_NO_REFRESH
) == 0 &&
298 git_index_read_safely(index
) < 0)
301 status
= git_status_list_alloc(index
);
302 GIT_ERROR_CHECK_ALLOC(status
);
305 memcpy(&status
->opts
, opts
, sizeof(git_status_options
));
306 memcpy(&diffopt
.pathspec
, &opts
->pathspec
, sizeof(diffopt
.pathspec
));
309 diffopt
.flags
= GIT_DIFF_INCLUDE_TYPECHANGE
;
310 findopt
.flags
= GIT_DIFF_FIND_FOR_UNTRACKED
;
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
;
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
;
339 if (show
!= GIT_STATUS_SHOW_WORKDIR_ONLY
) {
340 if ((error
= git_diff_tree_to_index(
341 &status
->head2idx
, repo
, head
, index
, &diffopt
)) < 0)
344 if ((flags
& GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX
) != 0 &&
345 (error
= git_diff_find_similar(status
->head2idx
, &findopt
)) < 0)
349 if (show
!= GIT_STATUS_SHOW_INDEX_ONLY
) {
350 if ((error
= git_diff_index_to_workdir(
351 &status
->idx2wd
, repo
, index
, &diffopt
)) < 0) {
355 if ((flags
& GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR
) != 0 &&
356 (error
= git_diff_find_similar(status
->idx2wd
, &findopt
)) < 0)
360 error
= git_diff__paired_foreach(
361 status
->head2idx
, status
->idx2wd
, status_collect
, status
);
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
);
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
);
379 git_status_list_free(status
);
385 if (opts
== NULL
|| opts
->baseline
!= head
)
387 git_index_free(index
);
392 size_t git_status_list_entrycount(git_status_list
*status
)
396 return status
->paired
.length
;
399 const git_status_entry
*git_status_byindex(git_status_list
*status
, size_t i
)
403 return git_vector_get(&status
->paired
, i
);
406 void git_status_list_free(git_status_list
*status
)
411 git_diff_free(status
->head2idx
);
412 git_diff_free(status
->idx2wd
);
414 git_vector_free_deep(&status
->paired
);
416 git__memzero(status
, sizeof(*status
));
420 int git_status_foreach_ext(
421 git_repository
*repo
,
422 const git_status_options
*opts
,
426 git_status_list
*status
;
427 const git_status_entry
*status_entry
;
431 if ((error
= git_status_list_new(&status
, repo
, opts
)) < 0) {
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
;
440 if ((error
= cb(path
, status_entry
->status
, payload
)) != 0) {
441 git_error_set_after_callback(error
);
446 git_status_list_free(status
);
451 int git_status_foreach(git_repository
*repo
, git_status_cb cb
, void *payload
)
453 return git_status_foreach_ext(repo
, NULL
, cb
, payload
);
456 struct status_file_info
{
464 static int get_one_status(const char *path
, unsigned int status
, void *data
)
466 struct status_file_info
*sfi
= data
;
467 int (*strcomp
)(const char *a
, const char *b
);
470 sfi
->status
= status
;
472 strcomp
= (sfi
->wildmatch_flags
& WM_CASEFOLD
) ? git__strcasecmp
: git__strcmp
;
474 if (sfi
->count
> 1 ||
475 (strcomp(sfi
->expected
, path
) != 0 &&
476 wildmatch(sfi
->expected
, path
, sfi
->wildmatch_flags
) != 0))
478 sfi
->ambiguous
= true;
479 return GIT_EAMBIGUOUS
; /* git_error_set will be done by caller */
486 unsigned int *status_flags
,
487 git_repository
*repo
,
491 git_status_options opts
= GIT_STATUS_OPTIONS_INIT
;
492 struct status_file_info sfi
= {0};
495 assert(status_flags
&& repo
&& path
);
497 if ((error
= git_repository_index__weakptr(&index
, repo
)) < 0)
500 if ((sfi
.expected
= git__strdup(path
)) == NULL
)
502 if (index
->ignore_case
)
503 sfi
.wildmatch_flags
= WM_CASEFOLD
;
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 GIT_STATUS_OPT_INCLUDE_UNTRACKED
|
509 GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS
|
510 GIT_STATUS_OPT_INCLUDE_UNMODIFIED
|
511 GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH
;
512 opts
.pathspec
.count
= 1;
513 opts
.pathspec
.strings
= &sfi
.expected
;
515 error
= git_status_foreach_ext(repo
, &opts
, get_one_status
, &sfi
);
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
;
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
;
529 *status_flags
= sfi
.status
;
531 git__free(sfi
.expected
);
536 int git_status_should_ignore(
538 git_repository
*repo
,
541 return git_ignore_path_is_ignored(ignored
, repo
, path
);
544 int git_status_options_init(git_status_options
*opts
, unsigned int version
)
546 GIT_INIT_STRUCTURE_FROM_TEMPLATE(
547 opts
, version
, git_status_options
, GIT_STATUS_OPTIONS_INIT
);
551 #ifndef GIT_DEPRECATE_HARD
552 int git_status_init_options(git_status_options
*opts
, unsigned int version
)
554 return git_status_options_init(opts
, version
);
558 int git_status_list_get_perfdata(
559 git_diff_perfdata
*out
, const git_status_list
*status
)
562 GIT_ERROR_CHECK_VERSION(out
, GIT_DIFF_PERFDATA_VERSION
, "git_diff_perfdata");
565 out
->oid_calculations
= 0;
567 if (status
->head2idx
) {
568 out
->stat_calls
+= status
->head2idx
->perf
.stat_calls
;
569 out
->oid_calculations
+= status
->head2idx
->perf
.oid_calculations
;
571 if (status
->idx2wd
) {
572 out
->stat_calls
+= status
->idx2wd
->perf
.stat_calls
;
573 out
->oid_calculations
+= status
->idx2wd
->perf
.oid_calculations
;