Fix checkout bug rmv untracked trees from index When checking out with the GIT_CHECKOUT_REMOVE_UNTRACKED option and there was an entire tree in the working directory and in the index that is not in the baseline nor target commit, the tree was correctly(?) removed from the working directory but was not successfully removed from the index. This fixes that and adds a test of the functionality.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
diff --git a/src/checkout.c b/src/checkout.c
index a10507a..261dee1 100644
--- a/src/checkout.c
+++ b/src/checkout.c
@@ -418,6 +418,8 @@ static int checkout_action_with_wd_dir(
return checkout_action_common(data, action, delta, wd);
}
+#define EXPAND_DIRS_FOR_STRATEGY (GIT_CHECKOUT_FORCE | GIT_CHECKOUT_REMOVE_UNTRACKED | GIT_CHECKOUT_REMOVE_IGNORED)
+
static int checkout_action(
checkout_data *data,
git_diff_delta *delta,
@@ -429,6 +431,7 @@ static int checkout_action(
int cmp = -1, act;
int (*strcomp)(const char *, const char *) = data->diff->strcomp;
int (*pfxcomp)(const char *str, const char *pfx) = data->diff->pfxcomp;
+ bool expand_dirs = (data->strategy & EXPAND_DIRS_FOR_STRATEGY) != 0;
/* move workdir iterator to follow along with deltas */
@@ -449,14 +452,14 @@ static int checkout_action(
if (cmp < 0) {
cmp = pfxcomp(delta->old_file.path, wd->path);
- if (cmp == 0) {
- if (wd->mode == GIT_FILEMODE_TREE) {
- /* case 2 - descend in wd */
- if (git_iterator_advance_into_directory(workdir, &wd) < 0)
- goto fail;
- continue;
- }
+ if (wd->mode == GIT_FILEMODE_TREE && (cmp == 0 || expand_dirs)) {
+ /* case 2 or untracked wd item that might need removal */
+ if (git_iterator_advance_into_directory(workdir, &wd) < 0)
+ goto fail;
+ continue;
+ }
+ if (cmp == 0) {
/* case 3 - wd contains non-dir where dir expected */
act = checkout_action_with_wd_blocker(data, delta, wd);
*wditem_ptr = git_iterator_advance(workdir, &wd) ? NULL : wd;
@@ -519,6 +522,26 @@ fail:
return -1;
}
+static int checkout_remaining_wd_items(
+ checkout_data *data,
+ git_iterator *workdir,
+ const git_index_entry *wd,
+ git_vector *spec)
+{
+ int error = 0;
+ bool expand_dirs = (data->strategy & EXPAND_DIRS_FOR_STRATEGY) != 0;
+
+ while (wd && !error) {
+ if (wd->mode == GIT_FILEMODE_TREE && expand_dirs)
+ error = git_iterator_advance_into_directory(workdir, &wd);
+
+ else if (!(error = checkout_action_wd_only(data, workdir, wd, spec)))
+ error = git_iterator_advance(workdir, &wd);
+ }
+
+ return error;
+}
+
static int checkout_get_actions(
uint32_t **actions_ptr,
size_t **counts_ptr,
@@ -570,13 +593,9 @@ static int checkout_get_actions(
counts[CHECKOUT_ACTION__CONFLICT]++;
}
- while (wditem != NULL) {
- error = checkout_action_wd_only(data, workdir, wditem, &pathspec);
- if (!error)
- error = git_iterator_advance(workdir, &wditem);
- if (error < 0)
- goto fail;
- }
+ error = checkout_remaining_wd_items(data, workdir, wditem, &pathspec);
+ if (error < 0)
+ goto fail;
counts[CHECKOUT_ACTION__REMOVE] += data->removes.length;
diff --git a/tests-clar/checkout/head.c b/tests-clar/checkout/head.c
index aed203a..8b30993 100644
--- a/tests-clar/checkout/head.c
+++ b/tests-clar/checkout/head.c
@@ -1,6 +1,8 @@
#include "clar_libgit2.h"
#include "refs.h"
#include "repo/repo_helpers.h"
+#include "path.h"
+#include "fileops.h"
static git_repository *g_repo;
@@ -20,3 +22,42 @@ void test_checkout_head__orphaned_head_returns_GIT_EORPHANEDHEAD(void)
cl_assert_equal_i(GIT_EORPHANEDHEAD, git_checkout_head(g_repo, NULL));
}
+
+void test_checkout_head__with_index_only_tree(void)
+{
+ git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
+ git_index *index;
+
+ /* let's start by getting things into a known state */
+
+ opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+ cl_git_pass(git_checkout_head(g_repo, &opts));
+
+ /* now let's stage some new stuff including a new directory */
+
+ cl_git_pass(git_repository_index(&index, g_repo));
+
+ p_mkdir("testrepo/newdir", 0777);
+ cl_git_mkfile("testrepo/newdir/newfile.txt", "new file\n");
+
+ cl_git_pass(git_index_add_from_workdir(index, "newdir/newfile.txt"));
+ cl_git_pass(git_index_write(index));
+
+ cl_assert(git_path_isfile("testrepo/newdir/newfile.txt"));
+ cl_assert(git_index_get_bypath(index, "newdir/newfile.txt", 0) != NULL);
+
+ git_index_free(index);
+
+ /* okay, so now we have staged this new file; let's see if we can remove */
+
+ opts.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_REMOVE_UNTRACKED;
+ cl_git_pass(git_checkout_head(g_repo, &opts));
+
+ cl_git_pass(git_repository_index(&index, g_repo));
+ cl_git_pass(git_index_read(index)); /* reload if needed */
+
+ cl_assert(!git_path_isfile("testrepo/newdir/newfile.txt"));
+ cl_assert(git_index_get_bypath(index, "newdir/newfile.txt", 0) == NULL);
+
+ git_index_free(index);
+}