mv: add check_dir_in_index() and solve general dir check issue

Originally, moving a <source> directory which is not on-disk due to its existence outside of sparse-checkout cone, "giv mv" command errors out with "bad source". Add a helper check_dir_in_index() function to see if a directory name exists in the index. Also add a SKIP_WORKTREE_DIR bit to mark such directories. Change the checking logic, so that such <source> directory makes "giv mv" command warns with "advise_on_updating_sparse_paths()" instead of "bad source"; also user now can supply a "--sparse" flag so this operation can be carried out successfully. Helped-by: Victoria Dye <vdye@github.com> Helped-by: Derrick Stolee <derrickstolee@github.com> Signed-off-by: Shaoxuan Yuan <shaoxuan.yuan02@gmail.com> Acked-by: Derrick Stolee <derrickstolee@github.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Shaoxuan Yuan committed Jun 30, 2022 at 10:37 UTC b91a2b6594a1da7d4d2f936f7db25bfa5a3d775e
3 files changed +47 -9
builtin/mv.c
+44 -6
@@ -25,6 +25,7 @@ enum update_mode {
25 WORKING_DIRECTORY = (1 << 1),
26 INDEX = (1 << 2),
27 SPARSE = (1 << 3),
28 + SKIP_WORKTREE_DIR = (1 << 4),
29 };
30
31 #define DUP_BASENAME 1
@@ -123,6 +124,36 @@ static int index_range_of_same_dir(const char *src, int length,
124 return last - first;
125 }
126
127 +/*
128 + * Check if an out-of-cone directory should be in the index. Imagine this case
129 + * that all the files under a directory are marked with 'CE_SKIP_WORKTREE' bit
130 + * and thus the directory is sparsified.
131 + *
132 + * Return 0 if such directory exist (i.e. with any of its contained files not
133 + * marked with CE_SKIP_WORKTREE, the directory would be present in working tree).
134 + * Return 1 otherwise.
135 + */
136 +static int check_dir_in_index(const char *name)
137 +{
138 + const char *with_slash = add_slash(name);
139 + int length = strlen(with_slash);
140 +
141 + int pos = cache_name_pos(with_slash, length);
142 + const struct cache_entry *ce;
143 +
144 + if (pos < 0) {
145 + pos = -pos - 1;
146 + if (pos >= the_index.cache_nr)
147 + return 1;
148 + ce = active_cache[pos];
149 + if (strncmp(with_slash, ce->name, length))
150 + return 1;
151 + if (ce_skip_worktree(ce))
152 + return 0;
153 + }
154 + return 1;
155 +}
156 +
157 int cmd_mv(int argc, const char **argv, const char *prefix)
158 {
159 int i, flags, gitmodules_modified = 0;
@@ -184,7 +215,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
215 /* Checking */
216 for (i = 0; i < argc; i++) {
217 const char *src = source[i], *dst = destination[i];
187 - int length, src_is_dir;
218 + int length;
219 const char *bad = NULL;
220 int skip_sparse = 0;
221
@@ -198,12 +229,17 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
229
230 pos = cache_name_pos(src, length);
231 if (pos < 0) {
232 + const char *src_w_slash = add_slash(src);
233 + if (!path_in_sparse_checkout(src_w_slash, &the_index) &&
234 + !check_dir_in_index(src)) {
235 + modes[i] |= SKIP_WORKTREE_DIR;
236 + goto dir_check;
237 + }
238 /* only error if existence is expected. */
239 if (!(modes[i] & SPARSE))
240 bad = _("bad source");
241 goto act_on_entry;
242 }
206 -
243 ce = active_cache[pos];
244 if (!ce_skip_worktree(ce)) {
245 bad = _("bad source");
@@ -230,12 +266,14 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
266 bad = _("can not move directory into itself");
267 goto act_on_entry;
268 }
233 - if ((src_is_dir = S_ISDIR(st.st_mode))
269 + if (S_ISDIR(st.st_mode)
270 && lstat(dst, &st) == 0) {
271 bad = _("cannot move directory over file");
272 goto act_on_entry;
273 }
238 - if (src_is_dir) {
274 +
275 +dir_check:
276 + if (S_ISDIR(st.st_mode)) {
277 int j, dst_len, n;
278 int first = cache_name_pos(src, length), last;
279
@@ -369,7 +407,7 @@ remove_entry:
407 printf(_("Renaming %s to %s\n"), src, dst);
408 if (show_only)
409 continue;
372 - if (!(mode & (INDEX | SPARSE)) &&
410 + if (!(mode & (INDEX | SPARSE | SKIP_WORKTREE_DIR)) &&
411 rename(src, dst) < 0) {
412 if (ignore_errors)
413 continue;
@@ -384,7 +422,7 @@ remove_entry:
422 1);
423 }
424
387 - if (mode & (WORKING_DIRECTORY))
425 + if (mode & (WORKING_DIRECTORY | SKIP_WORKTREE_DIR))
426 continue;
427
428 pos = cache_name_pos(src, strlen(src));
t/t1092-sparse-checkout-compatibility.sh
+1 -1
@@ -1828,7 +1828,7 @@ test_expect_success 'checkout behaves oddly with df-conflict-2' '
1828 test_cmp full-checkout-err sparse-index-err
1829 '
1830
1831 -test_expect_failure 'mv directory from out-of-cone to in-cone' '
1831 +test_expect_success 'mv directory from out-of-cone to in-cone' '
1832 init_repos &&
1833
1834 # <source> as a sparse directory (or SKIP_WORKTREE_DIR without enabling
t/t7002-mv-sparse-checkout.sh
+2 -2
@@ -219,7 +219,7 @@ test_expect_success 'refuse to move file to non-skip-worktree sparse path' '
219 test_cmp expect stderr
220 '
221
222 -test_expect_failure 'refuse to move out-of-cone directory without --sparse' '
222 +test_expect_success 'refuse to move out-of-cone directory without --sparse' '
223 test_when_finished "cleanup_sparse_checkout" &&
224 setup_sparse_checkout &&
225
@@ -230,7 +230,7 @@ test_expect_failure 'refuse to move out-of-cone directory without --sparse' '
230 test_cmp expect stderr
231 '
232
233 -test_expect_failure 'can move out-of-cone directory with --sparse' '
233 +test_expect_success 'can move out-of-cone directory with --sparse' '
234 test_when_finished "cleanup_sparse_checkout" &&
235 setup_sparse_checkout &&
236