Commit 2ad51b81 by Vicent Marti

Merge pull request #2241 from libgit2/rb/stash-skip-submodules

Improve stash and checkout for ignored + untracked items
parents af9eeac9 a409acef
......@@ -56,6 +56,7 @@ typedef struct {
git_vector conflicts;
git_buf path;
size_t workdir_len;
git_buf tmp;
unsigned int strategy;
int can_symlink;
bool reload_submodules;
......@@ -259,21 +260,41 @@ static int checkout_action_no_wd(
return checkout_action_common(action, data, delta, NULL);
}
static bool wd_item_is_removable(git_iterator *iter, const git_index_entry *wd)
{
git_buf *full = NULL;
if (wd->mode != GIT_FILEMODE_TREE)
return true;
if (git_iterator_current_workdir_path(&full, iter) < 0)
return true;
return !full || !git_path_contains(full, DOT_GIT);
}
static int checkout_queue_remove(checkout_data *data, const char *path)
{
char *copy = git_pool_strdup(&data->pool, path);
GITERR_CHECK_ALLOC(copy);
return git_vector_insert(&data->removes, copy);
}
/* note that this advances the iterator over the wd item */
static int checkout_action_wd_only(
checkout_data *data,
git_iterator *workdir,
const git_index_entry *wd,
const git_index_entry **wditem,
git_vector *pathspec)
{
int error = 0;
bool remove = false;
git_checkout_notify_t notify = GIT_CHECKOUT_NOTIFY_NONE;
const git_index_entry *wd = *wditem;
if (!git_pathspec__match(
pathspec, wd->path,
(data->strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) != 0,
git_iterator_ignore_case(workdir), NULL, NULL))
return 0;
return git_iterator_advance(wditem, workdir);
/* check if item is tracked in the index but not in the checkout diff */
if (data->index != NULL) {
......@@ -303,24 +324,49 @@ static int checkout_action_wd_only(
}
}
if (notify != GIT_CHECKOUT_NOTIFY_NONE)
/* found in index */;
else if (git_iterator_current_is_ignored(workdir)) {
notify = GIT_CHECKOUT_NOTIFY_IGNORED;
remove = ((data->strategy & GIT_CHECKOUT_REMOVE_IGNORED) != 0);
}
else {
notify = GIT_CHECKOUT_NOTIFY_UNTRACKED;
remove = ((data->strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0);
}
if (notify != GIT_CHECKOUT_NOTIFY_NONE) {
/* if we found something in the index, notify and advance */
if ((error = checkout_notify(data, notify, NULL, wd)) != 0)
return error;
if (remove && wd_item_is_removable(workdir, wd))
error = checkout_queue_remove(data, wd->path);
if (!error)
error = git_iterator_advance(wditem, workdir);
} else {
/* untracked or ignored - can't know which until we advance through */
bool over = false, removable = wd_item_is_removable(workdir, wd);
git_iterator_status_t untracked_state;
/* copy the entry for issuing notification callback later */
git_index_entry saved_wd = *wd;
git_buf_sets(&data->tmp, wd->path);
saved_wd.path = data->tmp.ptr;
error = git_iterator_advance_over_with_status(
wditem, &untracked_state, workdir);
if (error == GIT_ITEROVER)
over = true;
else if (error < 0)
return error;
error = checkout_notify(data, notify, NULL, wd);
if (untracked_state == GIT_ITERATOR_STATUS_IGNORED) {
notify = GIT_CHECKOUT_NOTIFY_IGNORED;
remove = ((data->strategy & GIT_CHECKOUT_REMOVE_IGNORED) != 0);
} else {
notify = GIT_CHECKOUT_NOTIFY_UNTRACKED;
remove = ((data->strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0);
}
if (!error && remove) {
char *path = git_pool_strdup(&data->pool, wd->path);
GITERR_CHECK_ALLOC(path);
if ((error = checkout_notify(data, notify, NULL, &saved_wd)) != 0)
return error;
error = git_vector_insert(&data->removes, path);
if (remove && removable)
error = checkout_queue_remove(data, saved_wd.path);
if (!error && over) /* restore ITEROVER if needed */
error = GIT_ITEROVER;
}
return error;
......@@ -554,11 +600,8 @@ static int checkout_action(
}
/* case 1 - handle wd item (if it matches pathspec) */
error = checkout_action_wd_only(data, workdir, wd, pathspec);
if (error)
goto done;
if ((error = git_iterator_advance(wditem, workdir)) < 0 &&
error != GIT_ITEROVER)
error = checkout_action_wd_only(data, workdir, wditem, pathspec);
if (error && error != GIT_ITEROVER)
goto done;
continue;
}
......@@ -619,10 +662,8 @@ static int checkout_remaining_wd_items(
{
int error = 0;
while (wd && !error) {
if (!(error = checkout_action_wd_only(data, workdir, wd, spec)))
error = git_iterator_advance(&wd, workdir);
}
while (wd && !error)
error = checkout_action_wd_only(data, workdir, &wd, spec);
if (error == GIT_ITEROVER)
error = 0;
......@@ -1853,6 +1894,7 @@ static void checkout_data_clear(checkout_data *data)
data->pfx = NULL;
git_buf_free(&data->path);
git_buf_free(&data->tmp);
git_index_free(data->index);
data->index = NULL;
......
......@@ -784,72 +784,6 @@ static bool entry_is_prefixed(
item->path[pathlen] == '/');
}
static int diff_scan_inside_untracked_dir(
git_diff *diff, diff_in_progress *info, git_delta_t *delta_type)
{
int error = 0;
git_buf base = GIT_BUF_INIT;
bool is_ignored;
*delta_type = GIT_DELTA_IGNORED;
git_buf_sets(&base, info->nitem->path);
/* advance into untracked directory */
if ((error = git_iterator_advance_into(&info->nitem, info->new_iter)) < 0) {
/* skip ahead if empty */
if (error == GIT_ENOTFOUND) {
giterr_clear();
error = git_iterator_advance(&info->nitem, info->new_iter);
}
goto done;
}
/* look for actual untracked file */
while (info->nitem != NULL &&
!diff->pfxcomp(info->nitem->path, git_buf_cstr(&base))) {
is_ignored = git_iterator_current_is_ignored(info->new_iter);
/* need to recurse into non-ignored directories */
if (!is_ignored && S_ISDIR(info->nitem->mode)) {
error = git_iterator_advance_into(&info->nitem, info->new_iter);
if (!error)
continue;
else if (error == GIT_ENOTFOUND) {
error = 0;
is_ignored = true; /* treat empty as ignored */
} else
break; /* real error, must stop */
}
/* found a non-ignored item - treat parent dir as untracked */
if (!is_ignored) {
*delta_type = GIT_DELTA_UNTRACKED;
break;
}
if ((error = git_iterator_advance(&info->nitem, info->new_iter)) < 0)
break;
}
/* finish off scan */
while (info->nitem != NULL &&
!diff->pfxcomp(info->nitem->path, git_buf_cstr(&base))) {
if ((error = git_iterator_advance(&info->nitem, info->new_iter)) < 0)
break;
}
done:
git_buf_free(&base);
if (error == GIT_ITEROVER)
error = 0;
return error;
}
static int handle_unmatched_new_item(
git_diff *diff, diff_in_progress *info)
{
......@@ -905,6 +839,7 @@ static int handle_unmatched_new_item(
DIFF_FLAG_ISNT_SET(diff, GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS))
{
git_diff_delta *last;
git_iterator_status_t untracked_state;
/* attempt to insert record for this directory */
if ((error = diff_delta__from_one(diff, delta_type, nitem)) != 0)
......@@ -916,11 +851,14 @@ static int handle_unmatched_new_item(
return git_iterator_advance(&info->nitem, info->new_iter);
/* iterate into dir looking for an actual untracked file */
if (diff_scan_inside_untracked_dir(diff, info, &delta_type) < 0)
return -1;
if ((error = git_iterator_advance_over_with_status(
&info->nitem, &untracked_state, info->new_iter)) < 0 &&
error != GIT_ITEROVER)
return error;
/* it iteration changed delta type, the update the record */
if (delta_type == GIT_DELTA_IGNORED) {
/* if we found nothing or just ignored items, update the record */
if (untracked_state == GIT_ITERATOR_STATUS_IGNORED ||
untracked_state == GIT_ITERATOR_STATUS_EMPTY) {
last->status = GIT_DELTA_IGNORED;
/* remove the record if we don't want ignored records */
......
......@@ -1528,3 +1528,77 @@ int git_iterator_current_workdir_path(git_buf **path, git_iterator *iter)
return 0;
}
int git_iterator_advance_over_with_status(
const git_index_entry **entryptr,
git_iterator_status_t *status,
git_iterator *iter)
{
int error = 0;
workdir_iterator *wi = (workdir_iterator *)iter;
char *base = NULL;
const git_index_entry *entry;
*status = GIT_ITERATOR_STATUS_NORMAL;
if (iter->type != GIT_ITERATOR_TYPE_WORKDIR)
return git_iterator_advance(entryptr, iter);
if ((error = git_iterator_current(&entry, iter)) < 0)
return error;
if (!S_ISDIR(entry->mode)) {
if (git_ignore__lookup(
&wi->ignores, wi->fi.entry.path, &wi->is_ignored) < 0)
wi->is_ignored = true;
if (wi->is_ignored)
*status = GIT_ITERATOR_STATUS_IGNORED;
return git_iterator_advance(entryptr, iter);
}
*status = GIT_ITERATOR_STATUS_EMPTY;
base = git__strdup(entry->path);
GITERR_CHECK_ALLOC(base);
/* scan inside directory looking for a non-ignored item */
while (entry && !iter->prefixcomp(entry->path, base)) {
if (git_ignore__lookup(
&wi->ignores, wi->fi.entry.path, &wi->is_ignored) < 0)
wi->is_ignored = true;
/* if we found an explicitly ignored item, then update from
* EMPTY to IGNORED
*/
if (wi->is_ignored)
*status = GIT_ITERATOR_STATUS_IGNORED;
else if (S_ISDIR(entry->mode)) {
error = git_iterator_advance_into(&entry, iter);
if (!error)
continue;
else if (error == GIT_ENOTFOUND) {
error = 0;
wi->is_ignored = true; /* mark empty directories as ignored */
} else
break; /* real error, stop here */
} else {
/* we found a non-ignored item, treat parent as untracked */
*status = GIT_ITERATOR_STATUS_NORMAL;
break;
}
if ((error = git_iterator_advance(&entry, iter)) < 0)
break;
}
/* wrap up scan back to base directory */
while (entry && !iter->prefixcomp(entry->path, base))
if ((error = git_iterator_advance(&entry, iter)) < 0)
break;
*entryptr = entry;
git__free(base);
return error;
}
......@@ -258,4 +258,23 @@ extern int git_iterator_current_workdir_path(
/* Return index pointer if index iterator, else NULL */
extern git_index *git_iterator_get_index(git_iterator *iter);
typedef enum {
GIT_ITERATOR_STATUS_NORMAL = 0,
GIT_ITERATOR_STATUS_IGNORED = 1,
GIT_ITERATOR_STATUS_EMPTY = 2
} git_iterator_status_t;
/* Advance over a directory and check if it contains no files or just
* ignored files.
*
* In a tree or the index, all directories will contain files, but in the
* working directory it is possible to have an empty directory tree or a
* tree that only contains ignored files. Many Git operations treat these
* cases specially. This advances over a directory (presumably an
* untracked directory) but checks during the scan if there are any files
* and any non-ignored files.
*/
extern int git_iterator_advance_over_with_status(
const git_index_entry **entry, git_iterator_status_t *status, git_iterator *iter);
#endif
......@@ -178,7 +178,8 @@ static int stash_update_index_from_diff(
break;
case GIT_DELTA_UNTRACKED:
if (data->include_untracked)
if (data->include_untracked &&
delta->new_file.mode != GIT_FILEMODE_TREE)
add_path = delta->new_file.path;
break;
......
......@@ -148,6 +148,25 @@ void test_stash_save__can_include_untracked_files(void)
assert_blob_oid("refs/stash^3:just.ignore", NULL);
}
void test_stash_save__untracked_skips_ignored(void)
{
cl_git_append2file("stash/.gitignore", "bundle/vendor/\n");
cl_must_pass(p_mkdir("stash/bundle", 0777));
cl_must_pass(p_mkdir("stash/bundle/vendor", 0777));
cl_git_mkfile("stash/bundle/vendor/blah", "contents\n");
cl_assert(git_path_exists("stash/when")); /* untracked */
cl_assert(git_path_exists("stash/just.ignore")); /* ignored */
cl_assert(git_path_exists("stash/bundle/vendor/blah")); /* ignored */
cl_git_pass(git_stash_save(
&stash_tip_oid, repo, signature, NULL, GIT_STASH_INCLUDE_UNTRACKED));
cl_assert(!git_path_exists("stash/when"));
cl_assert(git_path_exists("stash/bundle/vendor/blah"));
cl_assert(git_path_exists("stash/just.ignore"));
}
void test_stash_save__can_include_untracked_and_ignored_files(void)
{
cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED));
......@@ -342,7 +361,7 @@ void test_stash_save__can_stage_normal_then_stage_untracked(void)
void test_stash_save__including_untracked_without_any_untracked_file_creates_an_empty_tree(void)
{
cl_git_pass(p_unlink("stash/when"));
cl_must_pass(p_unlink("stash/when"));
assert_status(repo, "what", GIT_STATUS_WT_MODIFIED | GIT_STATUS_INDEX_MODIFIED);
assert_status(repo, "how", GIT_STATUS_INDEX_MODIFIED);
......@@ -354,3 +373,18 @@ void test_stash_save__including_untracked_without_any_untracked_file_creates_an_
assert_object_oid("stash^3^{tree}", EMPTY_TREE, GIT_OBJ_TREE);
}
void test_stash_save__skip_submodules(void)
{
git_repository *untracked_repo;
cl_git_pass(git_repository_init(&untracked_repo, "stash/untracked_repo", false));
cl_git_mkfile("stash/untracked_repo/content", "stuff");
git_repository_free(untracked_repo);
assert_status(repo, "untracked_repo/", GIT_STATUS_WT_NEW);
cl_git_pass(git_stash_save(
&stash_tip_oid, repo, signature, NULL, GIT_STASH_INCLUDE_UNTRACKED));
assert_status(repo, "untracked_repo/", GIT_STATUS_WT_NEW);
}
......@@ -42,15 +42,11 @@ void assert_status(
int status_flags)
{
unsigned int status;
int error;
error = git_status_file(&status, repo, path);
if (status_flags < 0) {
cl_assert_equal_i(status_flags, error);
return;
if (status_flags < 0)
cl_assert_equal_i(status_flags, git_status_file(&status, repo, path));
else {
cl_git_pass(git_status_file(&status, repo, path));
cl_assert_equal_i((unsigned int)status_flags, status);
}
cl_assert_equal_i(0, error);
cl_assert_equal_i((unsigned int)status_flags, status);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment