Commit 37da3685 by Russell Belfer

Make checkout match diff for untracked/ignored dir

When diff finds an untracked directory, it emulates Git behavior
by looking inside the directory to see if there are any untracked
items inside it. If there are only ignored items inside the dir,
then diff considers it ignored, even if there is no direct ignore
rule for it.

Checkout was not copying this behavior - when it found an untracked
directory, it just treated it as untracked.  Unfortunately, when
combined with GIT_CHECKOUT_REMOVE_UNTRACKED, this made is seem that
checkout (and stash, which uses checkout) was removing ignored
items when you had only asked it to remove untracked ones.

This commit moves the logic for advancing past an untracked dir
while scanning for non-ignored items into an iterator helper fn,
and uses that for both diff and checkout.
parent 3c1aa4c1
......@@ -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;
......@@ -270,21 +271,30 @@ static bool wd_item_is_removable(git_iterator *iter, const git_index_entry *wd)
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) {
......@@ -314,26 +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)) {
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 ignored, over = false;
bool removable = wd_item_is_removable(workdir, wd);
/* 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_and_check_ignored(
wditem, &ignored, workdir);
if (error == GIT_ITEROVER)
over = true;
else if (error < 0)
return error;
if (ignored) {
notify = GIT_CHECKOUT_NOTIFY_IGNORED;
remove = ((data->strategy & GIT_CHECKOUT_REMOVE_IGNORED) != 0) &&
wd_item_is_removable(workdir, wd);
}
else {
remove = ((data->strategy & GIT_CHECKOUT_REMOVE_IGNORED) != 0);
} else {
notify = GIT_CHECKOUT_NOTIFY_UNTRACKED;
remove = ((data->strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0) &&
wd_item_is_removable(workdir, wd);
remove = ((data->strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0);
}
error = checkout_notify(data, notify, NULL, wd);
if ((error = checkout_notify(data, notify, NULL, &saved_wd)) != 0)
return error;
if (!error && remove) {
char *path = git_pool_strdup(&data->pool, wd->path);
GITERR_CHECK_ALLOC(path);
if (remove && removable)
error = checkout_queue_remove(data, saved_wd.path);
error = git_vector_insert(&data->removes, path);
if (!error && over) /* restore ITEROVER if needed */
error = GIT_ITEROVER;
}
return error;
......@@ -567,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;
}
......@@ -632,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;
......@@ -1866,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;
bool ignored;
/* attempt to insert record for this directory */
if ((error = diff_delta__from_one(diff, delta_type, nitem)) != 0)
......@@ -916,11 +851,13 @@ 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_and_check_ignored(
&info->nitem, &ignored, info->new_iter)) < 0 &&
error != GIT_ITEROVER)
return error;
/* it iteration changed delta type, the update the record */
if (delta_type == GIT_DELTA_IGNORED) {
/* it iteration only found ignored items, update the record */
if (ignored) {
last->status = GIT_DELTA_IGNORED;
/* remove the record if we don't want ignored records */
......
......@@ -1528,3 +1528,71 @@ int git_iterator_current_workdir_path(git_buf **path, git_iterator *iter)
return 0;
}
int git_iterator_advance_over_and_check_ignored(
const git_index_entry **entryptr, bool *ignored, git_iterator *iter)
{
int error = 0;
workdir_iterator *wi = (workdir_iterator *)iter;
char *base = NULL;
const git_index_entry *entry;
*ignored = false;
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;
*ignored = wi->is_ignored;
return git_iterator_advance(entryptr, iter);
}
*ignored = true;
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 (!wi->is_ignored && 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; /* treat empty directories as ignored */
} else
break; /* real error, stop here */
}
/* if we found a non-ignored item, treat parent as untracked */
if (!wi->is_ignored) {
*ignored = false;
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,12 @@ 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);
/* Special type of advance that can be called when looking at a tree in
* the working directory that leaves the iterator on the next item after
* the tree, but also scans the tree contents looking for any items that
* are not ignored.
*/
extern int git_iterator_advance_over_and_check_ignored(
const git_index_entry **entry, bool *ignored, git_iterator *iter);
#endif
......@@ -155,10 +155,16 @@ void test_stash_save__untracked_skips_ignored(void)
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)
......
......@@ -42,7 +42,6 @@ void assert_status(
int status_flags)
{
unsigned int status;
int error;
if (status_flags < 0)
cl_assert_equal_i(status_flags, git_status_file(&status, repo, path));
......
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