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 error
= git_buf_lasterror(&st
->head_tree_relative_path
);
374 if (error
< GIT_SUCCESS
)
375 return git__rethrow(error
, "An error occured while determining the status of '%s'", a
->ptr
);
377 m_name
= st
->head_tree_relative_path
.ptr
;
381 i_name
= (entry
!= NULL
) ? entry
->path
: NULL
;
383 cmpma
= compare(m_name
, a_name
);
384 cmpmi
= compare(m_name
, i_name
);
385 cmpai
= compare(a_name
, i_name
);
387 pm
= ((cmpma
<= 0) && (cmpmi
<= 0)) ? m_name
: NULL
;
388 pa
= ((cmpma
>= 0) && (cmpai
<= 0)) ? a_name
: NULL
;
389 pi
= ((cmpmi
>= 0) && (cmpai
>= 0)) ? i_name
: NULL
;
391 if((error
= determine_status(st
, pm
!= NULL
, pi
!= NULL
, pa
!= NULL
, m
, entry
, a
, status_path(pm
, pi
, pa
), path_type
)) < GIT_SUCCESS
)
392 return git__rethrow(error
, "An error occured while determining the status of '%s'", a
->ptr
);
394 if ((pa
!= NULL
) || (path_type
== GIT_STATUS_PATH_FOLDER
))
399 static int status_cmp(const void *a
, const void *b
)
401 const struct status_entry
*entry_a
= (const struct status_entry
*)(a
);
402 const struct status_entry
*entry_b
= (const struct status_entry
*)(b
);
404 return strcmp(entry_a
->path
, entry_b
->path
);
407 #define DEFAULT_SIZE 16
409 int git_status_foreach(git_repository
*repo
, int (*callback
)(const char *, unsigned int, void *), void *payload
)
412 git_index
*index
= NULL
;
413 git_buf temp_path
= GIT_BUF_INIT
;
414 struct status_st dirent_st
= {0};
415 int error
= GIT_SUCCESS
;
418 struct status_entry
*e
;
421 if ((workdir
= git_repository_workdir(repo
)) == NULL
)
422 return git__throw(GIT_ERROR
,
423 "Cannot retrieve status on a bare repository");
425 if ((error
= git_repository_index__weakptr(&index
, repo
)) < GIT_SUCCESS
) {
426 return git__rethrow(error
,
427 "Failed to determine statuses. Index can't be opened");
430 if ((error
= retrieve_head_tree(&tree
, repo
)) < GIT_SUCCESS
) {
431 error
= git__rethrow(error
, "Failed to determine statuses");
435 git_vector_init(&entries
, DEFAULT_SIZE
, status_cmp
);
437 dirent_st
.workdir_path_len
= strlen(workdir
);
438 dirent_st
.tree_position
= 0;
439 dirent_st
.index_position
= 0;
440 dirent_st
.tree
= tree
;
441 dirent_st
.index
= index
;
442 dirent_st
.vector
= &entries
;
443 git_buf_init(&dirent_st
.head_tree_relative_path
, 0);
444 dirent_st
.head_tree_relative_path_len
= 0;
445 dirent_st
.is_dir
= 1;
447 if (git_futils_isdir(workdir
)) {
448 error
= git__throw(GIT_EINVALIDPATH
,
449 "Failed to determine status of file '%s'. "
450 "The given path doesn't lead to a folder", workdir
);
454 git_buf_sets(&temp_path
, workdir
);
456 error
= alphasorted_futils_direach(
457 &temp_path
, dirent_cb
, &dirent_st
);
459 if (error
< GIT_SUCCESS
)
460 error
= git__rethrow(error
,
461 "Failed to determine statuses. "
462 "An error occured while processing the working directory");
464 if ((error
== GIT_SUCCESS
) && ((error
= dirent_cb(&dirent_st
, NULL
)) < GIT_SUCCESS
))
465 error
= git__rethrow(error
,
466 "Failed to determine statuses. "
467 "An error occured while post-processing the HEAD tree and the index");
469 for (i
= 0; i
< entries
.length
; ++i
) {
470 e
= (struct status_entry
*)git_vector_get(&entries
, i
);
472 if (error
== GIT_SUCCESS
) {
473 error
= callback(e
->path
, e
->status_flags
, payload
);
474 if (error
< GIT_SUCCESS
)
475 error
= git__rethrow(error
,
476 "Failed to determine statuses. User callback failed");
483 git_buf_free(&dirent_st
.head_tree_relative_path
);
484 git_buf_free(&temp_path
);
485 git_vector_free(&entries
);
490 static int recurse_tree_entry(git_tree
*tree
, struct status_entry
*e
, const char *path
)
493 const git_tree_entry
*tree_entry
;
495 int error
= GIT_SUCCESS
;
497 dir_sep
= strchr(path
, '/');
499 tree_entry
= git_tree_entry_byname(tree
, path
);
500 if (tree_entry
== NULL
)
501 return GIT_SUCCESS
; /* The leaf doesn't exist in the tree*/
503 status_entry_update_from_tree_entry(e
, tree_entry
);
507 /* Retrieve subtree name */
510 tree_entry
= git_tree_entry_byname(tree
, path
);
511 if (tree_entry
== NULL
)
512 return GIT_SUCCESS
; /* The subtree doesn't exist in the tree*/
516 /* Retreive subtree */
517 if ((error
= git_tree_lookup(&subtree
, tree
->object
.repo
, &tree_entry
->oid
)) < GIT_SUCCESS
)
518 return git__throw(GIT_EOBJCORRUPTED
, "Can't find tree object '%s'", tree_entry
->filename
);
520 error
= recurse_tree_entry(subtree
, e
, dir_sep
+1);
521 git_tree_free(subtree
);
525 int git_status_file(unsigned int *status_flags
, git_repository
*repo
, const char *path
)
527 struct status_entry
*e
;
528 git_index
*index
= NULL
;
529 git_buf temp_path
= GIT_BUF_INIT
;
530 int error
= GIT_SUCCESS
;
531 git_tree
*tree
= NULL
;
534 assert(status_flags
&& repo
&& path
);
536 if ((workdir
= git_repository_workdir(repo
)) == NULL
)
537 return git__throw(GIT_ERROR
,
538 "Cannot retrieve status on a bare repository");
540 if ((error
= git_buf_joinpath(&temp_path
, workdir
, path
)) < GIT_SUCCESS
)
541 return git__rethrow(error
,
542 "Failed to determine status of file '%s'", path
);
544 if (git_futils_isdir(temp_path
.ptr
) == GIT_SUCCESS
) {
545 git_buf_free(&temp_path
);
546 return git__throw(GIT_EINVALIDPATH
,
547 "Failed to determine status of file '%s'. "
548 "Given path leads to a folder, not a file", path
);
551 e
= status_entry_new(NULL
, path
);
553 git_buf_free(&temp_path
);
557 /* Find file in Workdir */
558 if (git_futils_exists(temp_path
.ptr
) == GIT_SUCCESS
) {
559 if ((error
= status_entry_update_from_workdir(e
, temp_path
.ptr
)) < GIT_SUCCESS
)
560 goto cleanup
; /* The callee has already set the error message */
563 /* Find file in Index */
564 if ((error
= git_repository_index__weakptr(&index
, repo
)) < GIT_SUCCESS
) {
566 "Failed to determine status of file '%s'."
567 "Index can't be opened", path
);
571 status_entry_update_from_index(e
, index
);
573 if ((error
= retrieve_head_tree(&tree
, repo
)) < GIT_SUCCESS
) {
575 "Failed to determine status of file '%s'", path
);
579 /* If the repository is not empty, try and locate the file in HEAD */
581 if ((error
= git_buf_sets(&temp_path
, path
)) < GIT_SUCCESS
) {
583 "Failed to determine status of file '%s'", path
);
587 error
= recurse_tree_entry(tree
, e
, temp_path
.ptr
);
588 if (error
< GIT_SUCCESS
) {
590 "Failed to determine status of file '%s'. "
591 "An error occured while processing the tree", path
);
596 /* Determine status */
597 if ((error
= status_entry_update_flags(e
)) < GIT_SUCCESS
) {
598 git__throw(error
, "Nonexistent file");
602 *status_flags
= e
->status_flags
;
605 git_buf_free(&temp_path
);
612 * git_futils_direach is not supposed to return entries in an ordered manner.
613 * alphasorted_futils_direach wraps git_futils_direach and invokes the callback
614 * function by passing it alphabeticcally sorted paths parameters.
618 struct alphasorted_dirent_info
{
620 char path
[GIT_FLEX_ARRAY
]; /* more */
623 static struct alphasorted_dirent_info
*alphasorted_dirent_info_new(const git_buf
*path
)
626 struct alphasorted_dirent_info
*di
;
628 is_dir
= git_futils_isdir(path
->ptr
) == GIT_SUCCESS
? 1 : 0;
629 size
= sizeof(*di
) + path
->size
+ is_dir
+ 1;
631 di
= git__calloc(size
, 1);
635 git_buf_copy_cstr(di
->path
, path
->size
+ 1, path
);
641 * Append a forward slash to the name to force folders
642 * to be ordered in a similar way than in a tree
644 * The file "subdir" should appear before the file "subdir.txt"
645 * The folder "subdir" should appear after the file "subdir.txt"
647 di
->path
[path
->size
] = '/';
653 static int alphasorted_dirent_info_cmp(const void *a
, const void *b
)
655 struct alphasorted_dirent_info
*stra
= (struct alphasorted_dirent_info
*)a
;
656 struct alphasorted_dirent_info
*strb
= (struct alphasorted_dirent_info
*)b
;
658 return strcmp(stra
->path
, strb
->path
);
661 static int alphasorted_dirent_cb(void *state
, git_buf
*full_path
)
663 struct alphasorted_dirent_info
*entry
;
664 git_vector
*entry_names
;
666 entry_names
= (git_vector
*)state
;
667 entry
= alphasorted_dirent_info_new(full_path
);
672 if (git_vector_insert(entry_names
, entry
) < GIT_SUCCESS
) {
680 static int alphasorted_futils_direach(
682 int (*fn
)(void *, git_buf
*),
685 struct alphasorted_dirent_info
*entry
;
686 git_vector entry_names
;
688 int error
= GIT_SUCCESS
;
689 git_buf entry_path
= GIT_BUF_INIT
;
691 if (git_vector_init(&entry_names
, 16, alphasorted_dirent_info_cmp
) < GIT_SUCCESS
)
694 error
= git_futils_direach(path
, alphasorted_dirent_cb
, &entry_names
);
696 git_vector_sort(&entry_names
);
698 for (idx
= 0; idx
< entry_names
.length
; ++idx
) {
699 entry
= (struct alphasorted_dirent_info
*)git_vector_get(&entry_names
, idx
);
701 /* We have to walk the entire vector even if there was an error,
702 * in order to free up memory, but we stop making callbacks after
705 if (error
== GIT_SUCCESS
)
706 error
= git_buf_sets(&entry_path
, entry
->path
);
708 if (error
== GIT_SUCCESS
) {
709 ((struct status_st
*)arg
)->is_dir
= entry
->is_dir
;
710 error
= fn(arg
, &entry_path
);
716 git_buf_free(&entry_path
);
717 git_vector_free(&entry_names
);