Skip to content

Commit 93d2c16

Browse files
derrickstoleegitster
authored andcommitted
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 <[email protected]> Signed-off-by: Derrick Stolee <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent d7c4415 commit 93d2c16

File tree

2 files changed

+229
-9
lines changed

2 files changed

+229
-9
lines changed

builtin/mv.c

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,21 +118,23 @@ static int index_range_of_same_dir(const char *src, int length,
118118
int cmd_mv(int argc, const char **argv, const char *prefix)
119119
{
120120
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;
122122
struct option builtin_mv_options[] = {
123123
OPT__VERBOSE(&verbose, N_("be verbose")),
124124
OPT__DRY_RUN(&show_only, N_("dry run")),
125125
OPT__FORCE(&force, N_("force move/rename even if target exists"),
126126
PARSE_OPT_NOCOMPLETE),
127127
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")),
128129
OPT_END(),
129130
};
130131
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;
132133
struct stat st;
133134
struct string_list src_for_dst = STRING_LIST_INIT_NODUP;
134135
struct lock_file lock_file = LOCK_INIT;
135136
struct cache_entry *ce;
137+
struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
136138

137139
git_config(git_default_config, NULL);
138140

@@ -176,14 +178,17 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
176178
const char *src = source[i], *dst = destination[i];
177179
int length, src_is_dir;
178180
const char *bad = NULL;
181+
int skip_sparse = 0;
179182

180183
if (show_only)
181184
printf(_("Checking rename of '%s' to '%s'\n"), src, dst);
182185

183186
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) &&
187192
(dst[length] == 0 || dst[length] == '/')) {
188193
bad = _("can not move directory into itself");
189194
} 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)
212217
dst_len = strlen(dst);
213218

214219
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;
216222
source[argc + j] = path;
217223
destination[argc + j] =
218224
prefix_path(dst, dst_len, path + length + 1);
219-
modes[argc + j] = INDEX;
225+
modes[argc + j] = ce_skip_worktree(ce) ? SPARSE : INDEX;
220226
submodule_gitfile[argc + j] = NULL;
221227
}
222228
argc += last - first;
@@ -244,14 +250,36 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
244250
bad = _("multiple sources for the same target");
245251
else if (is_dir_sep(dst[strlen(dst) - 1]))
246252
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+
248274
string_list_insert(&src_for_dst, dst);
275+
}
249276

250277
if (!bad)
251278
continue;
252279
if (!ignore_errors)
253280
die(_("%s, source=%s, destination=%s"),
254281
bad, src, dst);
282+
remove_entry:
255283
if (--argc > 0) {
256284
int n = argc - i;
257285
memmove(source + i, source + i + 1,
@@ -266,6 +294,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
266294
}
267295
}
268296

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+
269303
for (i = 0; i < argc; i++) {
270304
const char *src = source[i], *dst = destination[i];
271305
enum update_mode mode = modes[i];
@@ -274,7 +308,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
274308
printf(_("Renaming %s to %s\n"), src, dst);
275309
if (show_only)
276310
continue;
277-
if (mode != INDEX && rename(src, dst) < 0) {
311+
if (mode != INDEX && mode != SPARSE && rename(src, dst) < 0) {
278312
if (ignore_errors)
279313
continue;
280314
die_errno(_("renaming '%s' failed"), src);

t/t7002-mv-sparse-checkout.sh

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)