mirror of
https://git.proxmox.com/git/libgit2
synced 2025-05-05 20:33:41 +00:00
Merge pull request #2241 from libgit2/rb/stash-skip-submodules
Improve stash and checkout for ignored + untracked items
This commit is contained in:
commit
2ad51b81d2
@ -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;
|
||||
|
||||
error = checkout_notify(data, notify, NULL, wd);
|
||||
if (remove && wd_item_is_removable(workdir, wd))
|
||||
error = checkout_queue_remove(data, wd->path);
|
||||
|
||||
if (!error && remove) {
|
||||
char *path = git_pool_strdup(&data->pool, wd->path);
|
||||
GITERR_CHECK_ALLOC(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;
|
||||
|
||||
error = git_vector_insert(&data->removes, path);
|
||||
/* 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;
|
||||
|
||||
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 = checkout_notify(data, notify, NULL, &saved_wd)) != 0)
|
||||
return error;
|
||||
|
||||
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;
|
||||
|
78
src/diff.c
78
src/diff.c
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user