2 * Copyright (C) 2009-2011 the libgit2 contributors
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.
14 #include "git2/status.h"
15 #include "repository.h"
24 unsigned int status_flags
:6;
26 char path
[GIT_FLEX_ARRAY
]; /* more */
29 static struct status_entry
*status_entry_new(git_vector
*entries
, const char *path
)
31 struct status_entry
*e
= git__calloc(sizeof(*e
) + strlen(path
) + 1, 1);
36 git_vector_insert(entries
, e
);
38 strcpy(e
->path
, path
);
43 GIT_INLINE(void) status_entry_update_from_tree_entry(struct status_entry
*e
, const git_tree_entry
*tree_entry
)
45 assert(e
&& tree_entry
);
47 git_oid_cpy(&e
->head_oid
, &tree_entry
->oid
);
50 GIT_INLINE(void) status_entry_update_from_index_entry(struct status_entry
*e
, const git_index_entry
*index_entry
)
52 assert(e
&& index_entry
);
54 git_oid_cpy(&e
->index_oid
, &index_entry
->oid
);
55 e
->mtime
= index_entry
->mtime
;
58 static void status_entry_update_from_index(struct status_entry
*e
, git_index
*index
)
61 git_index_entry
*index_entry
;
65 idx
= git_index_find(index
, e
->path
);
69 index_entry
= git_index_get(index
, idx
);
71 status_entry_update_from_index_entry(e
, index_entry
);
74 static int status_entry_update_from_workdir(struct status_entry
*e
, const char* full_path
)
78 if (p_stat(full_path
, &filest
) < GIT_SUCCESS
)
79 return git__throw(GIT_EOSERR
, "Failed to determine status of file '%s'. Can't read file", full_path
);
81 if (e
->mtime
.seconds
== (git_time_t
)filest
.st_mtime
)
82 git_oid_cpy(&e
->wt_oid
, &e
->index_oid
);
84 git_odb_hashfile(&e
->wt_oid
, full_path
, GIT_OBJ_BLOB
);
89 static int status_entry_update_flags(struct status_entry
*e
)
92 int head_zero
, index_zero
, wt_zero
;
94 memset(&zero
, 0x0, sizeof(git_oid
));
96 head_zero
= git_oid_cmp(&zero
, &e
->head_oid
);
97 index_zero
= git_oid_cmp(&zero
, &e
->index_oid
);
98 wt_zero
= git_oid_cmp(&zero
, &e
->wt_oid
);
100 if (head_zero
== 0 && index_zero
== 0 && wt_zero
== 0)
101 return GIT_ENOTFOUND
;
103 if (head_zero
== 0 && index_zero
!= 0)
104 e
->status_flags
|= GIT_STATUS_INDEX_NEW
;
105 else if (index_zero
== 0 && head_zero
!= 0)
106 e
->status_flags
|= GIT_STATUS_INDEX_DELETED
;
107 else if (git_oid_cmp(&e
->head_oid
, &e
->index_oid
) != 0)
108 e
->status_flags
|= GIT_STATUS_INDEX_MODIFIED
;
110 if (index_zero
== 0 && wt_zero
!= 0)
111 e
->status_flags
|= GIT_STATUS_WT_NEW
;
112 else if (wt_zero
== 0 && index_zero
!= 0)
113 e
->status_flags
|= GIT_STATUS_WT_DELETED
;
114 else if (git_oid_cmp(&e
->index_oid
, &e
->wt_oid
) != 0)
115 e
->status_flags
|= GIT_STATUS_WT_MODIFIED
;
125 int workdir_path_len
;
126 git_buf head_tree_relative_path
;
127 int head_tree_relative_path_len
;
128 unsigned int tree_position
;
129 unsigned int index_position
;
133 static int retrieve_head_tree(git_tree
**tree_out
, git_repository
*repo
)
135 git_reference
*resolved_head_ref
;
136 git_commit
*head_commit
= NULL
;
138 int error
= GIT_SUCCESS
;
142 error
= git_repository_head(&resolved_head_ref
, repo
);
144 * We assume that a situation where HEAD exists but can not be resolved is valid.
145 * A new repository fits this description for instance.
147 if (error
== GIT_ENOTFOUND
)
149 if (error
< GIT_SUCCESS
)
150 return git__rethrow(error
, "HEAD can't be resolved");
152 if ((error
= git_commit_lookup(&head_commit
, repo
, git_reference_oid(resolved_head_ref
))) < GIT_SUCCESS
)
153 return git__rethrow(error
, "The tip of HEAD can't be retrieved");
155 git_reference_free(resolved_head_ref
);
157 if ((error
= git_commit_tree(&tree
, head_commit
)) < GIT_SUCCESS
) {
158 error
= git__rethrow(error
, "The tree of HEAD can't be retrieved");
165 git_commit_free(head_commit
);
170 GIT_STATUS_PATH_NULL
,
171 GIT_STATUS_PATH_IGNORE
,
172 GIT_STATUS_PATH_FILE
,
173 GIT_STATUS_PATH_FOLDER
,
176 static int dirent_cb(void *state
, git_buf
*full_path
);
177 static int alphasorted_futils_direach(
178 git_buf
*path
, int (*fn
)(void *, git_buf
*), void *arg
);
180 static int process_folder(
181 struct status_st
*st
,
182 const git_tree_entry
*tree_entry
,
184 enum path_type path_type
)
186 git_object
*subtree
= NULL
;
187 git_tree
*pushed_tree
= NULL
;
188 int error
, pushed_tree_position
= 0;
189 git_otype tree_entry_type
= GIT_OBJ_BAD
;
191 if (tree_entry
!= NULL
) {
192 tree_entry_type
= git_tree_entry_type(tree_entry
);
194 switch (tree_entry_type
) {
196 error
= git_tree_entry_2object(&subtree
, ((git_object
*)(st
->tree
))->repo
, tree_entry
);
197 pushed_tree
= st
->tree
;
198 pushed_tree_position
= st
->tree_position
;
199 st
->tree
= (git_tree
*)subtree
;
200 st
->tree_position
= 0;
201 st
->head_tree_relative_path_len
+= 1 + tree_entry
->filename_len
; /* path + '/' + name */
209 error
= git__throw(GIT_EINVALIDTYPE
, "Unexpected tree entry type"); /* TODO: How should we deal with submodules? */
213 if (full_path
!= NULL
&& path_type
== GIT_STATUS_PATH_FOLDER
)
214 error
= alphasorted_futils_direach(full_path
, dirent_cb
, st
);
216 error
= dirent_cb(st
, NULL
);
218 if (tree_entry_type
== GIT_OBJ_TREE
) {
219 git_object_free(subtree
);
220 st
->head_tree_relative_path_len
-= 1 + tree_entry
->filename_len
;
221 st
->tree
= pushed_tree
;
222 st
->tree_position
= pushed_tree_position
;
229 static int store_if_changed(struct status_st
*st
, struct status_entry
*e
)
232 if ((error
= status_entry_update_flags(e
)) < GIT_SUCCESS
)
233 return git__throw(error
, "Failed to process the file '%s'. It doesn't exist in the workdir, in the HEAD nor in the index", e
->path
);
235 if (e
->status_flags
== GIT_STATUS_CURRENT
) {
240 return git_vector_insert(st
->vector
, e
);
243 static int determine_status(struct status_st
*st
,
244 int in_head
, int in_index
, int in_workdir
,
245 const git_tree_entry
*tree_entry
,
246 const git_index_entry
*index_entry
,
248 const char *status_path
,
249 enum path_type path_type
)
251 struct status_entry
*e
;
252 int error
= GIT_SUCCESS
;
253 git_otype tree_entry_type
= GIT_OBJ_BAD
;
255 if (tree_entry
!= NULL
)
256 tree_entry_type
= git_tree_entry_type(tree_entry
);
258 /* If we're dealing with a directory in the workdir, let's recursively tackle it first */
259 if (path_type
== GIT_STATUS_PATH_FOLDER
)
260 return process_folder(st
, tree_entry
, full_path
, path_type
);
262 /* Are we dealing with a file somewhere? */
263 if (in_workdir
|| in_index
|| (in_head
&& tree_entry_type
== GIT_OBJ_BLOB
)) {
264 e
= status_entry_new(NULL
, status_path
);
266 if (in_head
&& tree_entry_type
== GIT_OBJ_BLOB
) {
267 status_entry_update_from_tree_entry(e
, tree_entry
);
272 status_entry_update_from_index_entry(e
, index_entry
);
273 st
->index_position
++;
277 if ((error
= status_entry_update_from_workdir(e
, full_path
->ptr
279 return error
; /* The callee has already set the error message */
281 return store_if_changed(st
, e
);
284 /* Last option, we're dealing with a leftover folder tree entry */
285 assert(in_head
&& !in_index
&& !in_workdir
&& (tree_entry_type
== GIT_OBJ_TREE
));
286 return process_folder(st
, tree_entry
, full_path
, path_type
);
289 static int path_type_from(git_buf
*full_path
, int is_dir
)
291 if (full_path
== NULL
)
292 return GIT_STATUS_PATH_NULL
;
295 return GIT_STATUS_PATH_FILE
;
297 if (!git__suffixcmp(full_path
->ptr
, "/" DOT_GIT
"/"))
298 return GIT_STATUS_PATH_IGNORE
;
300 return GIT_STATUS_PATH_FOLDER
;
303 static const char *status_path(const char *first
, const char *second
, const char *third
)
305 /* At least one of them can not be NULL */
306 assert(first
!= NULL
|| second
!= NULL
|| third
!= NULL
);
308 /* TODO: Fixme. Ensure that when non null, they're all equal */
318 static int compare(const char *left
, const char *right
)
320 if (left
== NULL
&& right
== NULL
)
329 return strcmp(left
, right
);
332 /* Greatly inspired from JGit IndexTreeWalker */
333 /* https://github.com/spearce/jgit/blob/ed47e29c777accfa78c6f50685a5df2b8f5b8ff5/org.spearce.jgit/src/org/spearce/jgit/lib/IndexTreeWalker.java#L88 */
335 static int dirent_cb(void *state
, git_buf
*a
)
337 const git_tree_entry
*m
;
338 const git_index_entry
*entry
;
339 enum path_type path_type
;
340 int cmpma
, cmpmi
, cmpai
, error
;
341 const char *pm
, *pa
, *pi
;
342 const char *m_name
, *i_name
, *a_name
;
344 struct status_st
*st
= (struct status_st
*)state
;
346 path_type
= path_type_from(a
, st
->is_dir
);
348 if (path_type
== GIT_STATUS_PATH_IGNORE
)
349 return GIT_SUCCESS
; /* Let's skip the ".git" directory */
351 a_name
= (path_type
!= GIT_STATUS_PATH_NULL
) ? a
->ptr
+ st
->workdir_path_len
: NULL
;
354 if (st
->tree
== NULL
)
357 m
= git_tree_entry_byindex(st
->tree
, st
->tree_position
);
359 entry
= git_index_get(st
->index
, st
->index_position
);
361 if ((m
== NULL
) && (a
== NULL
) && (entry
== NULL
))
365 git_buf_truncate(&st
->head_tree_relative_path
,
366 st
->head_tree_relative_path_len
);
367 git_buf_joinpath(&st
->head_tree_relative_path
,
368 st
->head_tree_relative_path
.ptr
, m
->filename
);
369 /* When the tree entry is a folder, append a forward slash to its name */
370 if (git_tree_entry_type(m
) == GIT_OBJ_TREE
)
371 git_path_to_dir(&st
->head_tree_relative_path
);
373 if (error
< GIT_SUCCESS
)
374 return git__rethrow(error
, "An error occured while determining the status of '%s'", a
->ptr
);
376 m_name
= st
->head_tree_relative_path
.ptr
;
380 i_name
= (entry
!= NULL
) ? entry
->path
: NULL
;
382 cmpma
= compare(m_name
, a_name
);
383 cmpmi
= compare(m_name
, i_name
);
384 cmpai
= compare(a_name
, i_name
);
386 pm
= ((cmpma
<= 0) && (cmpmi
<= 0)) ? m_name
: NULL
;
387 pa
= ((cmpma
>= 0) && (cmpai
<= 0)) ? a_name
: NULL
;
388 pi
= ((cmpmi
>= 0) && (cmpai
>= 0)) ? i_name
: NULL
;
390 if((error
= determine_status(st
, pm
!= NULL
, pi
!= NULL
, pa
!= NULL
, m
, entry
, a
, status_path(pm
, pi
, pa
), path_type
)) < GIT_SUCCESS
)
391 return git__rethrow(error
, "An error occured while determining the status of '%s'", a
->ptr
);
393 if ((pa
!= NULL
) || (path_type
== GIT_STATUS_PATH_FOLDER
))
398 static int status_cmp(const void *a
, const void *b
)
400 const struct status_entry
*entry_a
= (const struct status_entry
*)(a
);
401 const struct status_entry
*entry_b
= (const struct status_entry
*)(b
);
403 return strcmp(entry_a
->path
, entry_b
->path
);
406 #define DEFAULT_SIZE 16
408 int git_status_foreach(git_repository
*repo
, int (*callback
)(const char *, unsigned int, void *), void *payload
)
411 git_index
*index
= NULL
;
412 git_buf temp_path
= GIT_BUF_INIT
;
413 struct status_st dirent_st
= {0};
414 int error
= GIT_SUCCESS
;
417 struct status_entry
*e
;
420 if ((workdir
= git_repository_workdir(repo
)) == NULL
)
421 return git__throw(GIT_ERROR
,
422 "Cannot retrieve status on a bare repository");
424 if ((error
= git_repository_index__weakptr(&index
, repo
)) < GIT_SUCCESS
) {
425 return git__rethrow(error
,
426 "Failed to determine statuses. Index can't be opened");
429 if ((error
= retrieve_head_tree(&tree
, repo
)) < GIT_SUCCESS
) {
430 error
= git__rethrow(error
, "Failed to determine statuses");
434 git_vector_init(&entries
, DEFAULT_SIZE
, status_cmp
);
436 dirent_st
.workdir_path_len
= strlen(workdir
);
437 dirent_st
.tree_position
= 0;
438 dirent_st
.index_position
= 0;
439 dirent_st
.tree
= tree
;
440 dirent_st
.index
= index
;
441 dirent_st
.vector
= &entries
;
442 git_buf_init(&dirent_st
.head_tree_relative_path
, 0);
443 dirent_st
.head_tree_relative_path_len
= 0;
444 dirent_st
.is_dir
= 1;
446 if (git_futils_isdir(workdir
)) {
447 error
= git__throw(GIT_EINVALIDPATH
,
448 "Failed to determine status of file '%s'. "
449 "The given path doesn't lead to a folder", workdir
);
453 git_buf_sets(&temp_path
, workdir
);
455 error
= alphasorted_futils_direach(
456 &temp_path
, dirent_cb
, &dirent_st
);
458 if (error
< GIT_SUCCESS
)
459 error
= git__rethrow(error
,
460 "Failed to determine statuses. "
461 "An error occured while processing the working directory");
463 if ((error
== GIT_SUCCESS
) && ((error
= dirent_cb(&dirent_st
, NULL
)) < GIT_SUCCESS
))
464 error
= git__rethrow(error
,
465 "Failed to determine statuses. "
466 "An error occured while post-processing the HEAD tree and the index");
468 for (i
= 0; i
< entries
.length
; ++i
) {
469 e
= (struct status_entry
*)git_vector_get(&entries
, i
);
471 if (error
== GIT_SUCCESS
) {
472 error
= callback(e
->path
, e
->status_flags
, payload
);
473 if (error
< GIT_SUCCESS
)
474 error
= git__rethrow(error
,
475 "Failed to determine statuses. User callback failed");
482 git_buf_free(&dirent_st
.head_tree_relative_path
);
483 git_buf_free(&temp_path
);
484 git_vector_free(&entries
);
489 static int recurse_tree_entry(git_tree
*tree
, struct status_entry
*e
, const char *path
)
492 const git_tree_entry
*tree_entry
;
494 int error
= GIT_SUCCESS
;
496 dir_sep
= strchr(path
, '/');
498 tree_entry
= git_tree_entry_byname(tree
, path
);
499 if (tree_entry
== NULL
)
500 return GIT_SUCCESS
; /* The leaf doesn't exist in the tree*/
502 status_entry_update_from_tree_entry(e
, tree_entry
);
506 /* Retrieve subtree name */
509 tree_entry
= git_tree_entry_byname(tree
, path
);
510 if (tree_entry
== NULL
)
511 return GIT_SUCCESS
; /* The subtree doesn't exist in the tree*/
515 /* Retreive subtree */
516 if ((error
= git_tree_lookup(&subtree
, tree
->object
.repo
, &tree_entry
->oid
)) < GIT_SUCCESS
)
517 return git__throw(GIT_EOBJCORRUPTED
, "Can't find tree object '%s'", tree_entry
->filename
);
519 error
= recurse_tree_entry(subtree
, e
, dir_sep
+1);
520 git_tree_free(subtree
);
524 int git_status_file(unsigned int *status_flags
, git_repository
*repo
, const char *path
)
526 struct status_entry
*e
;
527 git_index
*index
= NULL
;
528 git_buf temp_path
= GIT_BUF_INIT
;
529 int error
= GIT_SUCCESS
;
530 git_tree
*tree
= NULL
;
533 assert(status_flags
&& repo
&& path
);
535 if ((workdir
= git_repository_workdir(repo
)) == NULL
)
536 return git__throw(GIT_ERROR
,
537 "Cannot retrieve status on a bare repository");
539 if ((error
= git_buf_joinpath(&temp_path
, workdir
, path
)) < GIT_SUCCESS
)
540 return git__rethrow(error
,
541 "Failed to determine status of file '%s'", path
);
543 if (git_futils_isdir(temp_path
.ptr
) == GIT_SUCCESS
) {
544 git_buf_free(&temp_path
);
545 return git__throw(GIT_EINVALIDPATH
,
546 "Failed to determine status of file '%s'. "
547 "Given path leads to a folder, not a file", path
);
550 e
= status_entry_new(NULL
, path
);
552 git_buf_free(&temp_path
);
556 /* Find file in Workdir */
557 if (git_futils_exists(temp_path
.ptr
) == GIT_SUCCESS
) {
558 if ((error
= status_entry_update_from_workdir(e
, temp_path
.ptr
)) < GIT_SUCCESS
)
559 goto cleanup
; /* The callee has already set the error message */
562 /* Find file in Index */
563 if ((error
= git_repository_index__weakptr(&index
, repo
)) < GIT_SUCCESS
) {
565 "Failed to determine status of file '%s'."
566 "Index can't be opened", path
);
570 status_entry_update_from_index(e
, index
);
572 if ((error
= retrieve_head_tree(&tree
, repo
)) < GIT_SUCCESS
) {
574 "Failed to determine status of file '%s'", path
);
578 /* If the repository is not empty, try and locate the file in HEAD */
580 if ((error
= git_buf_sets(&temp_path
, path
)) < GIT_SUCCESS
) {
582 "Failed to determine status of file '%s'", path
);
586 error
= recurse_tree_entry(tree
, e
, temp_path
.ptr
);
587 if (error
< GIT_SUCCESS
) {
589 "Failed to determine status of file '%s'. "
590 "An error occured while processing the tree", path
);
595 /* Determine status */
596 if ((error
= status_entry_update_flags(e
)) < GIT_SUCCESS
) {
597 git__throw(error
, "Nonexistent file");
601 *status_flags
= e
->status_flags
;
604 git_buf_free(&temp_path
);
611 * git_futils_direach is not supposed to return entries in an ordered manner.
612 * alphasorted_futils_direach wraps git_futils_direach and invokes the callback
613 * function by passing it alphabeticcally sorted paths parameters.
617 struct alphasorted_dirent_info
{
619 char path
[GIT_FLEX_ARRAY
]; /* more */
622 static struct alphasorted_dirent_info
*alphasorted_dirent_info_new(const git_buf
*path
)
625 struct alphasorted_dirent_info
*di
;
627 is_dir
= git_futils_isdir(path
->ptr
) == GIT_SUCCESS
? 1 : 0;
628 size
= sizeof(*di
) + path
->size
+ is_dir
+ 1;
630 di
= git__calloc(size
, 1);
634 git_buf_copy_cstr(di
->path
, path
->size
+ 1, path
);
640 * Append a forward slash to the name to force folders
641 * to be ordered in a similar way than in a tree
643 * The file "subdir" should appear before the file "subdir.txt"
644 * The folder "subdir" should appear after the file "subdir.txt"
646 di
->path
[path
->size
] = '/';
652 static int alphasorted_dirent_info_cmp(const void *a
, const void *b
)
654 struct alphasorted_dirent_info
*stra
= (struct alphasorted_dirent_info
*)a
;
655 struct alphasorted_dirent_info
*strb
= (struct alphasorted_dirent_info
*)b
;
657 return strcmp(stra
->path
, strb
->path
);
660 static int alphasorted_dirent_cb(void *state
, git_buf
*full_path
)
662 struct alphasorted_dirent_info
*entry
;
663 git_vector
*entry_names
;
665 entry_names
= (git_vector
*)state
;
666 entry
= alphasorted_dirent_info_new(full_path
);
671 if (git_vector_insert(entry_names
, entry
) < GIT_SUCCESS
) {
679 static int alphasorted_futils_direach(
681 int (*fn
)(void *, git_buf
*),
684 struct alphasorted_dirent_info
*entry
;
685 git_vector entry_names
;
687 int error
= GIT_SUCCESS
;
688 git_buf entry_path
= GIT_BUF_INIT
;
690 if (git_vector_init(&entry_names
, 16, alphasorted_dirent_info_cmp
) < GIT_SUCCESS
)
693 error
= git_futils_direach(path
, alphasorted_dirent_cb
, &entry_names
);
695 git_vector_sort(&entry_names
);
697 for (idx
= 0; idx
< entry_names
.length
; ++idx
) {
698 entry
= (struct alphasorted_dirent_info
*)git_vector_get(&entry_names
, idx
);
700 /* We have to walk the entire vector even if there was an error,
701 * in order to free up memory, but we stop making callbacks after
704 if (error
== GIT_SUCCESS
)
705 error
= git_buf_sets(&entry_path
, entry
->path
);
707 if (error
== GIT_SUCCESS
) {
708 ((struct status_st
*)arg
)->is_dir
= entry
->is_dir
;
709 error
= fn(arg
, &entry_path
);
715 git_buf_free(&entry_path
);
716 git_vector_free(&entry_names
);