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"
20 #include "git2/diff.h"
22 #include "diff_generate.h"
24 static unsigned int index_delta2status(const git_diff_delta
*head2idx
)
26 git_status_t st
= GIT_STATUS_CURRENT
;
28 switch (head2idx
->status
) {
30 case GIT_DELTA_COPIED
:
31 st
= GIT_STATUS_INDEX_NEW
;
33 case GIT_DELTA_DELETED
:
34 st
= GIT_STATUS_INDEX_DELETED
;
36 case GIT_DELTA_MODIFIED
:
37 st
= GIT_STATUS_INDEX_MODIFIED
;
39 case GIT_DELTA_RENAMED
:
40 st
= GIT_STATUS_INDEX_RENAMED
;
42 if (!git_oid_equal(&head2idx
->old_file
.id
, &head2idx
->new_file
.id
))
43 st
|= GIT_STATUS_INDEX_MODIFIED
;
45 case GIT_DELTA_TYPECHANGE
:
46 st
= GIT_STATUS_INDEX_TYPECHANGE
;
48 case GIT_DELTA_CONFLICTED
:
49 st
= GIT_STATUS_CONFLICTED
;
58 static unsigned int workdir_delta2status(
59 git_diff
*diff
, git_diff_delta
*idx2wd
)
61 git_status_t st
= GIT_STATUS_CURRENT
;
63 switch (idx2wd
->status
) {
65 case GIT_DELTA_COPIED
:
66 case GIT_DELTA_UNTRACKED
:
67 st
= GIT_STATUS_WT_NEW
;
69 case GIT_DELTA_UNREADABLE
:
70 st
= GIT_STATUS_WT_UNREADABLE
;
72 case GIT_DELTA_DELETED
:
73 st
= GIT_STATUS_WT_DELETED
;
75 case GIT_DELTA_MODIFIED
:
76 st
= GIT_STATUS_WT_MODIFIED
;
78 case GIT_DELTA_IGNORED
:
79 st
= GIT_STATUS_IGNORED
;
81 case GIT_DELTA_RENAMED
:
82 st
= GIT_STATUS_WT_RENAMED
;
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
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
;
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
;
102 if (!git_oid_equal(&idx2wd
->old_file
.id
, &idx2wd
->new_file
.id
))
103 st
|= GIT_STATUS_WT_MODIFIED
;
106 case GIT_DELTA_TYPECHANGE
:
107 st
= GIT_STATUS_WT_TYPECHANGE
;
109 case GIT_DELTA_CONFLICTED
:
110 st
= GIT_STATUS_CONFLICTED
;
119 static bool status_is_included(
120 git_status_list
*status
,
121 git_diff_delta
*head2idx
,
122 git_diff_delta
*idx2wd
)
124 if (!(status
->opts
.flags
& GIT_STATUS_OPT_EXCLUDE_SUBMODULES
))
127 /* if excluding submodules and this is a submodule everywhere */
129 if (head2idx
->status
!= GIT_DELTA_ADDED
&&
130 head2idx
->old_file
.mode
!= GIT_FILEMODE_COMMIT
)
132 if (head2idx
->status
!= GIT_DELTA_DELETED
&&
133 head2idx
->new_file
.mode
!= GIT_FILEMODE_COMMIT
)
137 if (idx2wd
->status
!= GIT_DELTA_ADDED
&&
138 idx2wd
->old_file
.mode
!= GIT_FILEMODE_COMMIT
)
140 if (idx2wd
->status
!= GIT_DELTA_DELETED
&&
141 idx2wd
->new_file
.mode
!= GIT_FILEMODE_COMMIT
)
145 /* only get here if every valid mode is GIT_FILEMODE_COMMIT */
149 static git_status_t
status_compute(
150 git_status_list
*status
,
151 git_diff_delta
*head2idx
,
152 git_diff_delta
*idx2wd
)
154 git_status_t st
= GIT_STATUS_CURRENT
;
157 st
|= index_delta2status(head2idx
);
160 st
|= workdir_delta2status(status
->idx2wd
, idx2wd
);
165 static int status_collect(
166 git_diff_delta
*head2idx
,
167 git_diff_delta
*idx2wd
,
170 git_status_list
*status
= payload
;
171 git_status_entry
*status_entry
;
173 if (!status_is_included(status
, head2idx
, idx2wd
))
176 status_entry
= git__malloc(sizeof(git_status_entry
));
177 GIT_ERROR_CHECK_ALLOC(status_entry
);
179 status_entry
->status
= status_compute(status
, head2idx
, idx2wd
);
180 status_entry
->head_to_index
= head2idx
;
181 status_entry
->index_to_workdir
= idx2wd
;
183 return git_vector_insert(&status
->paired
, status_entry
);
186 GIT_INLINE(int) status_entry_cmp_base(
189 int (*strcomp
)(const char *a
, const char *b
))
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
;
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
;
200 if (!delta_a
&& delta_b
)
202 if (delta_a
&& !delta_b
)
204 if (!delta_a
&& !delta_b
)
207 return strcomp(delta_a
->new_file
.path
, delta_b
->new_file
.path
);
210 static int status_entry_icmp(const void *a
, const void *b
)
212 return status_entry_cmp_base(a
, b
, git__strcasecmp
);
215 static int status_entry_cmp(const void *a
, const void *b
)
217 return status_entry_cmp_base(a
, b
, git__strcmp
);
220 static git_status_list
*git_status_list_alloc(git_index
*index
)
222 git_status_list
*status
= NULL
;
223 int (*entrycmp
)(const void *a
, const void *b
);
225 if (!(status
= git__calloc(1, sizeof(git_status_list
))))
228 entrycmp
= index
->ignore_case
? status_entry_icmp
: status_entry_cmp
;
230 if (git_vector_init(&status
->paired
, 0, entrycmp
) < 0) {
238 static int status_validate_options(const git_status_options
*opts
)
243 GIT_ERROR_CHECK_VERSION(opts
, GIT_STATUS_OPTIONS_VERSION
, "git_status_options");
245 if (opts
->show
> GIT_STATUS_SHOW_WORKDIR_ONLY
) {
246 git_error_set(GIT_ERROR_INVALID
, "unknown status 'show' option");
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");
260 int git_status_list_new(
261 git_status_list
**out
,
262 git_repository
*repo
,
263 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 git_tree
*head
= NULL
;
270 git_status_show_t show
=
271 opts
? opts
->show
: GIT_STATUS_SHOW_INDEX_AND_WORKDIR
;
273 unsigned int flags
= opts
? opts
->flags
: GIT_STATUS_OPT_DEFAULTS
;
277 if (status_validate_options(opts
) < 0)
280 if ((error
= git_repository__ensure_not_bare(repo
, "status")) < 0 ||
281 (error
= git_repository_index(&index
, repo
)) < 0)
284 if (opts
!= NULL
&& opts
->baseline
!= NULL
) {
285 head
= opts
->baseline
;
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
)
295 /* refresh index from disk unless prevented */
296 if ((flags
& GIT_STATUS_OPT_NO_REFRESH
) == 0 &&
297 git_index_read_safely(index
) < 0)
300 status
= git_status_list_alloc(index
);
301 GIT_ERROR_CHECK_ALLOC(status
);
304 memcpy(&status
->opts
, opts
, sizeof(git_status_options
));
305 memcpy(&diffopt
.pathspec
, &opts
->pathspec
, sizeof(diffopt
.pathspec
));
308 diffopt
.flags
= GIT_DIFF_INCLUDE_TYPECHANGE
;
309 findopt
.flags
= GIT_DIFF_FIND_FOR_UNTRACKED
;
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
;
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
;
338 if (show
!= GIT_STATUS_SHOW_WORKDIR_ONLY
) {
339 if ((error
= git_diff_tree_to_index(
340 &status
->head2idx
, repo
, head
, index
, &diffopt
)) < 0)
343 if ((flags
& GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX
) != 0 &&
344 (error
= git_diff_find_similar(status
->head2idx
, &findopt
)) < 0)
348 if (show
!= GIT_STATUS_SHOW_INDEX_ONLY
) {
349 if ((error
= git_diff_index_to_workdir(
350 &status
->idx2wd
, repo
, index
, &diffopt
)) < 0) {
354 if ((flags
& GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR
) != 0 &&
355 (error
= git_diff_find_similar(status
->idx2wd
, &findopt
)) < 0)
359 error
= git_diff__paired_foreach(
360 status
->head2idx
, status
->idx2wd
, status_collect
, status
);
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
);
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
);
378 git_status_list_free(status
);
384 if (opts
== NULL
|| opts
->baseline
!= head
)
386 git_index_free(index
);
391 size_t git_status_list_entrycount(git_status_list
*status
)
395 return status
->paired
.length
;
398 const git_status_entry
*git_status_byindex(git_status_list
*status
, size_t i
)
402 return git_vector_get(&status
->paired
, i
);
405 void git_status_list_free(git_status_list
*status
)
410 git_diff_free(status
->head2idx
);
411 git_diff_free(status
->idx2wd
);
413 git_vector_free_deep(&status
->paired
);
415 git__memzero(status
, sizeof(*status
));
419 int git_status_foreach_ext(
420 git_repository
*repo
,
421 const git_status_options
*opts
,
425 git_status_list
*status
;
426 const git_status_entry
*status_entry
;
430 if ((error
= git_status_list_new(&status
, repo
, opts
)) < 0) {
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
;
439 if ((error
= cb(path
, status_entry
->status
, payload
)) != 0) {
440 git_error_set_after_callback(error
);
445 git_status_list_free(status
);
450 int git_status_foreach(git_repository
*repo
, git_status_cb cb
, void *payload
)
452 return git_status_foreach_ext(repo
, NULL
, cb
, payload
);
455 struct status_file_info
{
463 static int get_one_status(const char *path
, unsigned int status
, void *data
)
465 struct status_file_info
*sfi
= data
;
466 int (*strcomp
)(const char *a
, const char *b
);
469 sfi
->status
= status
;
471 strcomp
= (sfi
->fnm_flags
& FNM_CASEFOLD
) ? git__strcasecmp
: git__strcmp
;
473 if (sfi
->count
> 1 ||
474 (strcomp(sfi
->expected
, path
) != 0 &&
475 p_fnmatch(sfi
->expected
, path
, sfi
->fnm_flags
) != 0))
477 sfi
->ambiguous
= true;
478 return GIT_EAMBIGUOUS
; /* git_error_set will be done by caller */
485 unsigned int *status_flags
,
486 git_repository
*repo
,
490 git_status_options opts
= GIT_STATUS_OPTIONS_INIT
;
491 struct status_file_info sfi
= {0};
494 assert(status_flags
&& repo
&& path
);
496 if ((error
= git_repository_index__weakptr(&index
, repo
)) < 0)
499 if ((sfi
.expected
= git__strdup(path
)) == NULL
)
501 if (index
->ignore_case
)
502 sfi
.fnm_flags
= FNM_CASEFOLD
;
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
;
514 error
= git_status_foreach_ext(repo
, &opts
, get_one_status
, &sfi
);
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
;
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
;
528 *status_flags
= sfi
.status
;
530 git__free(sfi
.expected
);
535 int git_status_should_ignore(
537 git_repository
*repo
,
540 return git_ignore_path_is_ignored(ignored
, repo
, path
);
543 int git_status_init_options(git_status_options
*opts
, unsigned int version
)
545 GIT_INIT_STRUCTURE_FROM_TEMPLATE(
546 opts
, version
, git_status_options
, GIT_STATUS_OPTIONS_INIT
);
550 int git_status_list_get_perfdata(
551 git_diff_perfdata
*out
, const git_status_list
*status
)
554 GIT_ERROR_CHECK_VERSION(out
, GIT_DIFF_PERFDATA_VERSION
, "git_diff_perfdata");
557 out
->oid_calculations
= 0;
559 if (status
->head2idx
) {
560 out
->stat_calls
+= status
->head2idx
->perf
.stat_calls
;
561 out
->oid_calculations
+= status
->head2idx
->perf
.oid_calculations
;
563 if (status
->idx2wd
) {
564 out
->stat_calls
+= status
->idx2wd
->perf
.stat_calls
;
565 out
->oid_calculations
+= status
->idx2wd
->perf
.oid_calculations
;