mv: refuse to move sparse paths

Since cmd_mv() does not operate on cache entries and instead directly checks the filesystem, we can only use path_in_sparse_checkout() as a mechanism for seeing if a path is sparse or not. Be sure to skip returning a failure if '-k' is specified. To ensure that the advice around sparse paths is the only reason a move failed, be sure to check this as the very last thing before inserting into the src_for_dst list. The tests cover a variety of cases such as whether the target is tracked or untracked, and whether the source or destination are in or outside of the sparse-checkout definition. Helped-by: Matheus Tavares Bernardino <matheus.bernardino@usp.br> Signed-off-by: Derrick Stolee <dstolee@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Sep 24, 2021 at 15:39 UTC 93d2c160411e2a3309a8e2f55eb53bd6c78b7262
2 files changed +229 -9
builtin/mv.c
+43 -9
@@ -118,21 +118,23 @@ static int index_range_of_same_dir(const char *src, int length,
118 int cmd_mv(int argc, const char **argv, const char *prefix)
119 {
120 int i, flags, gitmodules_modified = 0;
121 - int verbose = 0, show_only = 0, force = 0, ignore_errors = 0;
121 + int verbose = 0, show_only = 0, force = 0, ignore_errors = 0, ignore_sparse = 0;
122 struct option builtin_mv_options[] = {
123 OPT__VERBOSE(&verbose, N_("be verbose")),
124 OPT__DRY_RUN(&show_only, N_("dry run")),
125 OPT__FORCE(&force, N_("force move/rename even if target exists"),
126 PARSE_OPT_NOCOMPLETE),
127 OPT_BOOL('k', NULL, &ignore_errors, N_("skip move/rename errors")),
128 + OPT_BOOL(0, "sparse", &ignore_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
129 OPT_END(),
130 };
131 const char **source, **destination, **dest_path, **submodule_gitfile;
131 - enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes;
132 + enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX, SPARSE } *modes;
133 struct stat st;
134 struct string_list src_for_dst = STRING_LIST_INIT_NODUP;
135 struct lock_file lock_file = LOCK_INIT;
136 struct cache_entry *ce;
137 + struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
138
139 git_config(git_default_config, NULL);
140
@@ -176,14 +178,17 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
178 const char *src = source[i], *dst = destination[i];
179 int length, src_is_dir;
180 const char *bad = NULL;
181 + int skip_sparse = 0;
182
183 if (show_only)
184 printf(_("Checking rename of '%s' to '%s'\n"), src, dst);
185
186 length = strlen(src);
184 - if (lstat(src, &st) < 0)
185 - bad = _("bad source");
186 - else if (!strncmp(src, dst, length) &&
187 + if (lstat(src, &st) < 0) {
188 + /* only error if existence is expected. */
189 + if (modes[i] != SPARSE)
190 + bad = _("bad source");
191 + } else if (!strncmp(src, dst, length) &&
192 (dst[length] == 0 || dst[length] == '/')) {
193 bad = _("can not move directory into itself");
194 } else if ((src_is_dir = S_ISDIR(st.st_mode))
@@ -212,11 +217,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
217 dst_len = strlen(dst);
218
219 for (j = 0; j < last - first; j++) {
215 - const char *path = active_cache[first + j]->name;
220 + const struct cache_entry *ce = active_cache[first + j];
221 + const char *path = ce->name;
222 source[argc + j] = path;
223 destination[argc + j] =
224 prefix_path(dst, dst_len, path + length + 1);
219 - modes[argc + j] = INDEX;
225 + modes[argc + j] = ce_skip_worktree(ce) ? SPARSE : INDEX;
226 submodule_gitfile[argc + j] = NULL;
227 }
228 argc += last - first;
@@ -244,14 +250,36 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
250 bad = _("multiple sources for the same target");
251 else if (is_dir_sep(dst[strlen(dst) - 1]))
252 bad = _("destination directory does not exist");
247 - else
253 + else {
254 + /*
255 + * We check if the paths are in the sparse-checkout
256 + * definition as a very final check, since that
257 + * allows us to point the user to the --sparse
258 + * option as a way to have a successful run.
259 + */
260 + if (!ignore_sparse &&
261 + !path_in_sparse_checkout(src, &the_index)) {
262 + string_list_append(&only_match_skip_worktree, src);
263 + skip_sparse = 1;
264 + }
265 + if (!ignore_sparse &&
266 + !path_in_sparse_checkout(dst, &the_index)) {
267 + string_list_append(&only_match_skip_worktree, dst);
268 + skip_sparse = 1;
269 + }
270 +
271 + if (skip_sparse)
272 + goto remove_entry;
273 +
274 string_list_insert(&src_for_dst, dst);
275 + }
276
277 if (!bad)
278 continue;
279 if (!ignore_errors)
280 die(_("%s, source=%s, destination=%s"),
281 bad, src, dst);
282 +remove_entry:
283 if (--argc > 0) {
284 int n = argc - i;
285 memmove(source + i, source + i + 1,
@@ -266,6 +294,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
294 }
295 }
296
297 + if (only_match_skip_worktree.nr) {
298 + advise_on_updating_sparse_paths(&only_match_skip_worktree);
299 + if (!ignore_errors)
300 + return 1;
301 + }
302 +
303 for (i = 0; i < argc; i++) {
304 const char *src = source[i], *dst = destination[i];
305 enum update_mode mode = modes[i];
@@ -274,7 +308,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
308 printf(_("Renaming %s to %s\n"), src, dst);
309 if (show_only)
310 continue;
277 - if (mode != INDEX && rename(src, dst) < 0) {
311 + if (mode != INDEX && mode != SPARSE && rename(src, dst) < 0) {
312 if (ignore_errors)
313 continue;
314 die_errno(_("renaming '%s' failed"), src);
t/t7002-mv-sparse-checkout.sh new
+186
@@ -0,0 +1,186 @@
1 +#!/bin/sh
2 +
3 +test_description='git mv in sparse working trees'
4 +
5 +. ./test-lib.sh
6 +
7 +test_expect_success 'setup' "
8 + mkdir -p sub/dir sub/dir2 &&
9 + touch a b c sub/d sub/dir/e sub/dir2/e &&
10 + git add -A &&
11 + git commit -m files &&
12 +
13 + cat >sparse_error_header <<-EOF &&
14 + The following pathspecs didn't match any eligible path, but they do match index
15 + entries outside the current sparse checkout:
16 + EOF
17 +
18 + cat >sparse_hint <<-EOF
19 + hint: Disable or modify the sparsity rules if you intend to update such entries.
20 + hint: Disable this message with \"git config advice.updateSparsePath false\"
21 + EOF
22 +"
23 +
24 +test_expect_success 'mv refuses to move sparse-to-sparse' '
25 + test_when_finished rm -f e &&
26 + git reset --hard &&
27 + git sparse-checkout set a &&
28 + touch b &&
29 + test_must_fail git mv b e 2>stderr &&
30 + cat sparse_error_header >expect &&
31 + echo b >>expect &&
32 + echo e >>expect &&
33 + cat sparse_hint >>expect &&
34 + test_cmp expect stderr &&
35 + git mv --sparse b e 2>stderr &&
36 + test_must_be_empty stderr
37 +'
38 +
39 +test_expect_success 'mv refuses to move sparse-to-sparse, ignores failure' '
40 + test_when_finished rm -f b c e &&
41 + git reset --hard &&
42 + git sparse-checkout set a &&
43 +
44 + # tracked-to-untracked
45 + touch b &&
46 + git mv -k b e 2>stderr &&
47 + test_path_exists b &&
48 + test_path_is_missing e &&
49 + cat sparse_error_header >expect &&
50 + echo b >>expect &&
51 + echo e >>expect &&
52 + cat sparse_hint >>expect &&
53 + test_cmp expect stderr &&
54 +
55 + git mv --sparse b e 2>stderr &&
56 + test_must_be_empty stderr &&
57 + test_path_is_missing b &&
58 + test_path_exists e &&
59 +
60 + # tracked-to-tracked
61 + git reset --hard &&
62 + touch b &&
63 + git mv -k b c 2>stderr &&
64 + test_path_exists b &&
65 + test_path_is_missing c &&
66 + cat sparse_error_header >expect &&
67 + echo b >>expect &&
68 + echo c >>expect &&
69 + cat sparse_hint >>expect &&
70 + test_cmp expect stderr &&
71 +
72 + git mv --sparse b c 2>stderr &&
73 + test_must_be_empty stderr &&
74 + test_path_is_missing b &&
75 + test_path_exists c
76 +'
77 +
78 +test_expect_success 'mv refuses to move non-sparse-to-sparse' '
79 + test_when_finished rm -f b c e &&
80 + git reset --hard &&
81 + git sparse-checkout set a &&
82 +
83 + # tracked-to-untracked
84 + test_must_fail git mv a e 2>stderr &&
85 + test_path_exists a &&
86 + test_path_is_missing e &&
87 + cat sparse_error_header >expect &&
88 + echo e >>expect &&
89 + cat sparse_hint >>expect &&
90 + test_cmp expect stderr &&
91 + git mv --sparse a e 2>stderr &&
92 + test_must_be_empty stderr &&
93 + test_path_is_missing a &&
94 + test_path_exists e &&
95 +
96 + # tracked-to-tracked
97 + rm e &&
98 + git reset --hard &&
99 + test_must_fail git mv a c 2>stderr &&
100 + test_path_exists a &&
101 + test_path_is_missing c &&
102 + cat sparse_error_header >expect &&
103 + echo c >>expect &&
104 + cat sparse_hint >>expect &&
105 + test_cmp expect stderr &&
106 + git mv --sparse a c 2>stderr &&
107 + test_must_be_empty stderr &&
108 + test_path_is_missing a &&
109 + test_path_exists c
110 +'
111 +
112 +test_expect_success 'mv refuses to move sparse-to-non-sparse' '
113 + test_when_finished rm -f b c e &&
114 + git reset --hard &&
115 + git sparse-checkout set a e &&
116 +
117 + # tracked-to-untracked
118 + touch b &&
119 + test_must_fail git mv b e 2>stderr &&
120 + cat sparse_error_header >expect &&
121 + echo b >>expect &&
122 + cat sparse_hint >>expect &&
123 + test_cmp expect stderr &&
124 + git mv --sparse b e 2>stderr &&
125 + test_must_be_empty stderr
126 +'
127 +
128 +test_expect_success 'recursive mv refuses to move (possible) sparse' '
129 + test_when_finished rm -rf b c e sub2 &&
130 + git reset --hard &&
131 + # Without cone mode, "sub" and "sub2" do not match
132 + git sparse-checkout set sub/dir sub2/dir &&
133 +
134 + # Add contained contents to ensure we avoid non-existence errors
135 + mkdir sub/dir2 &&
136 + touch sub/d sub/dir2/e &&
137 +
138 + test_must_fail git mv sub sub2 2>stderr &&
139 + cat sparse_error_header >expect &&
140 + cat >>expect <<-\EOF &&
141 + sub/d
142 + sub2/d
143 + sub/dir/e
144 + sub2/dir/e
145 + sub/dir2/e
146 + sub2/dir2/e
147 + EOF
148 + cat sparse_hint >>expect &&
149 + test_cmp expect stderr &&
150 + git mv --sparse sub sub2 2>stderr &&
151 + test_must_be_empty stderr &&
152 + git commit -m "moved sub to sub2" &&
153 + git rev-parse HEAD~1:sub >expect &&
154 + git rev-parse HEAD:sub2 >actual &&
155 + test_cmp expect actual &&
156 + git reset --hard HEAD~1
157 +'
158 +
159 +test_expect_success 'recursive mv refuses to move sparse' '
160 + git reset --hard &&
161 + # Use cone mode so "sub/" matches the sparse-checkout patterns
162 + git sparse-checkout init --cone &&
163 + git sparse-checkout set sub/dir sub2/dir &&
164 +
165 + # Add contained contents to ensure we avoid non-existence errors
166 + mkdir sub/dir2 &&
167 + touch sub/dir2/e &&
168 +
169 + test_must_fail git mv sub sub2 2>stderr &&
170 + cat sparse_error_header >expect &&
171 + cat >>expect <<-\EOF &&
172 + sub/dir2/e
173 + sub2/dir2/e
174 + EOF
175 + cat sparse_hint >>expect &&
176 + test_cmp expect stderr &&
177 + git mv --sparse sub sub2 2>stderr &&
178 + test_must_be_empty stderr &&
179 + git commit -m "moved sub to sub2" &&
180 + git rev-parse HEAD~1:sub >expect &&
181 + git rev-parse HEAD:sub2 >actual &&
182 + test_cmp expect actual &&
183 + git reset --hard HEAD~1
184 +'
185 +
186 +test_done